mirror of
https://github.com/penpot/penpot.git
synced 2026-01-21 21:00:26 -05:00
Compare commits
723 Commits
2.6.1
...
add-numeri
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3dfccdaf9b | ||
|
|
e5bc369e56 | ||
|
|
5e8929e504 | ||
|
|
3ee3ee2059 | ||
|
|
9eacde567d | ||
|
|
9638fd274f | ||
|
|
7c072abe28 | ||
|
|
603e41bbfd | ||
|
|
b561ad033c | ||
|
|
7373056037 | ||
|
|
a9173f672d | ||
|
|
44829ff1ae | ||
|
|
927ee9e55e | ||
|
|
066b252522 | ||
|
|
556a68a78f | ||
|
|
f9bbf2d524 | ||
|
|
eaaca5629e | ||
|
|
0df2a12814 | ||
|
|
df27db1996 | ||
|
|
7fc0d15418 | ||
|
|
99fb905070 | ||
|
|
413fc6de16 | ||
|
|
d54a7d0401 | ||
|
|
ed53793d9d | ||
|
|
faa68784af | ||
|
|
58b1cf6b0c | ||
|
|
f9c9e865b5 | ||
|
|
ebe321d9d3 | ||
|
|
0683fbd17c | ||
|
|
09a7ef3e45 | ||
|
|
172b70d8a7 | ||
|
|
3597e5bb54 | ||
|
|
949b6d1205 | ||
|
|
c0af77faf7 | ||
|
|
8f7a674000 | ||
|
|
e4f2dfaa11 | ||
|
|
ec29c4f5fe | ||
|
|
c21f5221bb | ||
|
|
42ef2f929a | ||
|
|
2b21401368 | ||
|
|
a5c8063b2c | ||
|
|
2ad2af2aea | ||
|
|
c2ce7c6cf6 | ||
|
|
47490db4be | ||
|
|
a2ac2bc6c6 | ||
|
|
e80ca7e332 | ||
|
|
e4644ff506 | ||
|
|
662b926b4b | ||
|
|
6319ed78f9 | ||
|
|
3abc8774f6 | ||
|
|
af1c90c252 | ||
|
|
8019ae7840 | ||
|
|
6bd615ff8b | ||
|
|
c4a793d306 | ||
|
|
631b3ac062 | ||
|
|
48995850fa | ||
|
|
a5c7a2c97b | ||
|
|
3a8285bc69 | ||
|
|
02e3cc089e | ||
|
|
17e19afcbd | ||
|
|
a2b52a6408 | ||
|
|
8cc4b69291 | ||
|
|
045ddf5829 | ||
|
|
1d0335aba6 | ||
|
|
5412d72236 | ||
|
|
896ee43212 | ||
|
|
5d42b9793b | ||
|
|
6cd2c712ab | ||
|
|
a575410a29 | ||
|
|
6b5703c1fe | ||
|
|
22c3d4d807 | ||
|
|
b0701f6bb4 | ||
|
|
0748ef7267 | ||
|
|
9bad9b8e91 | ||
|
|
9ca4fa752c | ||
|
|
b6563f620b | ||
|
|
a63fa2944d | ||
|
|
fd89c9d82c | ||
|
|
71bb2556f9 | ||
|
|
f36aa30525 | ||
|
|
8f7c63d6e2 | ||
|
|
965b22718f | ||
|
|
48a3d38d82 | ||
|
|
31f642ed25 | ||
|
|
9f414b6ecd | ||
|
|
334d7833d5 | ||
|
|
ff9c8f5929 | ||
|
|
f7311cbb6b | ||
|
|
e4c563f917 | ||
|
|
2d3ad5a88f | ||
|
|
1334d733cd | ||
|
|
004a9f17d3 | ||
|
|
c87fa4f723 | ||
|
|
9378a5786f | ||
|
|
3224ba26f1 | ||
|
|
d33a5e6df1 | ||
|
|
0d60e3d997 | ||
|
|
645c4a57db | ||
|
|
778de6adaf | ||
|
|
29d23577d2 | ||
|
|
1fea1e8f5b | ||
|
|
c8a211742a | ||
|
|
2da8747485 | ||
|
|
6803c78e80 | ||
|
|
d8daea72de | ||
|
|
36b162b4fa | ||
|
|
4c487834f0 | ||
|
|
dc7e53881a | ||
|
|
1a01c9ee4a | ||
|
|
b6be416c7b | ||
|
|
4a27e8d1dd | ||
|
|
1bc97f9ad0 | ||
|
|
aaa57cb17f | ||
|
|
b2d6342422 | ||
|
|
ba1e16b55b | ||
|
|
ef95e3ecb0 | ||
|
|
55d21761fc | ||
|
|
0b4a367e9e | ||
|
|
8f2ca15ec0 | ||
|
|
21041eb925 | ||
|
|
53cfc29a1f | ||
|
|
96d44e0631 | ||
|
|
8afd217a80 | ||
|
|
330e49db56 | ||
|
|
aa39170d47 | ||
|
|
8fa7a69094 | ||
|
|
33d989feb2 | ||
|
|
e309a57223 | ||
|
|
0b289153cb | ||
|
|
cf274099c4 | ||
|
|
6524e75770 | ||
|
|
9b80f7c9b3 | ||
|
|
bf76f328c8 | ||
|
|
051c2a7e99 | ||
|
|
887fa6b77b | ||
|
|
d9f98008f4 | ||
|
|
0cb6e0dee2 | ||
|
|
ad87e9842d | ||
|
|
e22a55334e | ||
|
|
f5e81debbc | ||
|
|
300e24b403 | ||
|
|
a00e7c1061 | ||
|
|
ba25ce3098 | ||
|
|
968ea56197 | ||
|
|
2635873b9a | ||
|
|
f5f1316f0b | ||
|
|
79a164be6d | ||
|
|
ecb85778bc | ||
|
|
676c4d2dfe | ||
|
|
2cdc241e68 | ||
|
|
057bf9bf25 | ||
|
|
2ddcd0ce15 | ||
|
|
fef08dfa18 | ||
|
|
831422feaf | ||
|
|
d01e3085f4 | ||
|
|
d9ca82dc15 | ||
|
|
1e7127d98a | ||
|
|
002ae8b91a | ||
|
|
6831acb71d | ||
|
|
1f44d53f6b | ||
|
|
ca2891d441 | ||
|
|
91fbe8f8ef | ||
|
|
69cc4fb4c2 | ||
|
|
37abb7b237 | ||
|
|
5fc2208c16 | ||
|
|
c2b67d7c67 | ||
|
|
294ce7bb1b | ||
|
|
a558bfdb2f | ||
|
|
86bcd1b681 | ||
|
|
33c260c35b | ||
|
|
94312bb35c | ||
|
|
70b1989f10 | ||
|
|
eb76d16b3b | ||
|
|
c0eaa75232 | ||
|
|
dbb9971482 | ||
|
|
0828994840 | ||
|
|
e6b5618bd3 | ||
|
|
9c24d3a521 | ||
|
|
480e0887e3 | ||
|
|
e0e381bdfc | ||
|
|
5199b306aa | ||
|
|
8febfaa21e | ||
|
|
69062f03ee | ||
|
|
eb04fa19e1 | ||
|
|
03b4fe3558 | ||
|
|
b349d08155 | ||
|
|
15e9d92094 | ||
|
|
5e675dbf0b | ||
|
|
a5660819de | ||
|
|
d277fefc87 | ||
|
|
1383010826 | ||
|
|
59982c9056 | ||
|
|
afcff84e38 | ||
|
|
fc5d9659d6 | ||
|
|
8fa7fa8c4b | ||
|
|
23bde76192 | ||
|
|
ca7a80fb83 | ||
|
|
cf0d9a433d | ||
|
|
bc20598b3d | ||
|
|
9de8ebb52c | ||
|
|
568af52ebc | ||
|
|
eddabc0d68 | ||
|
|
6b300d516b | ||
|
|
e271caa32b | ||
|
|
9be569c54c | ||
|
|
7e6a621484 | ||
|
|
66b47f9444 | ||
|
|
694a2084e2 | ||
|
|
fef19a3c80 | ||
|
|
3da8b945ca | ||
|
|
8f27b82edd | ||
|
|
8b529d308c | ||
|
|
71aa8e5a86 | ||
|
|
ab01f0b274 | ||
|
|
e203536506 | ||
|
|
b71b9edee7 | ||
|
|
bd514c0594 | ||
|
|
36e1ad287c | ||
|
|
92f5b5f92b | ||
|
|
0b7b6e2c23 | ||
|
|
46709fb02e | ||
|
|
61eb2f4a19 | ||
|
|
3fe16bd8f9 | ||
|
|
a9725a1aac | ||
|
|
8f9298fac8 | ||
|
|
c3e76817cd | ||
|
|
8bdec66927 | ||
|
|
66ee9edaf8 | ||
|
|
ffd7bc883d | ||
|
|
1bcfa4b8dc | ||
|
|
99e325acaf | ||
|
|
8badd1f2eb | ||
|
|
44bf276c49 | ||
|
|
0f3a4db71e | ||
|
|
751bed4117 | ||
|
|
ea095a98ba | ||
|
|
348a9c82bf | ||
|
|
e2918f4148 | ||
|
|
c45187eedd | ||
|
|
eeea5f2cc8 | ||
|
|
05b6aeef3e | ||
|
|
6323031b40 | ||
|
|
6ccb6cafaa | ||
|
|
be26985ca5 | ||
|
|
2aa2525d0e | ||
|
|
7cb2f307d8 | ||
|
|
f1a557c372 | ||
|
|
202337b135 | ||
|
|
4e3abcbd45 | ||
|
|
122e5a4b57 | ||
|
|
1981946480 | ||
|
|
7d327d23a2 | ||
|
|
500c27859b | ||
|
|
c6f68e6ed1 | ||
|
|
b48faf8fe0 | ||
|
|
fa24ced3a3 | ||
|
|
b9ea2425b9 | ||
|
|
1abaff9c52 | ||
|
|
6f2ccabaa2 | ||
|
|
1c77126fe6 | ||
|
|
7196be2a23 | ||
|
|
d509b840dc | ||
|
|
61c23877c1 | ||
|
|
0e61398d67 | ||
|
|
f12656463d | ||
|
|
ba9fc37226 | ||
|
|
60f754f172 | ||
|
|
3a22545158 | ||
|
|
1d0020f6e6 | ||
|
|
f3c3f3e2d8 | ||
|
|
9ba0ae5532 | ||
|
|
db73c2eea0 | ||
|
|
753823c0b3 | ||
|
|
44e8eacb8d | ||
|
|
33bcbd89f1 | ||
|
|
b0cbe3cec8 | ||
|
|
3ca76c9ef7 | ||
|
|
93199e1a70 | ||
|
|
93a601a1e7 | ||
|
|
3d864c4ff1 | ||
|
|
da2f519805 | ||
|
|
230e330eb2 | ||
|
|
4f6dffabb4 | ||
|
|
09c3490cae | ||
|
|
1fc0203c38 | ||
|
|
f545d7b3ea | ||
|
|
b242eb5b32 | ||
|
|
be9e3fa355 | ||
|
|
fac93e4ff8 | ||
|
|
8609db2182 | ||
|
|
ec73bd640c | ||
|
|
cba65972dd | ||
|
|
e62231cfed | ||
|
|
3249fb43c3 | ||
|
|
ee0ba15f9e | ||
|
|
784aecd1a1 | ||
|
|
173d6c23b0 | ||
|
|
abc1241402 | ||
|
|
f30441626e | ||
|
|
5ae125db94 | ||
|
|
093fa18839 | ||
|
|
81f18ad7f4 | ||
|
|
875e019d4f | ||
|
|
8e18a0880e | ||
|
|
36b78e5e21 | ||
|
|
86a498fc29 | ||
|
|
aae81b8a04 | ||
|
|
486f036a11 | ||
|
|
a2c9d307df | ||
|
|
e52fd90963 | ||
|
|
f8602810eb | ||
|
|
219ddfabaf | ||
|
|
d8b3b216e9 | ||
|
|
88e5209856 | ||
|
|
c2b13a6d5d | ||
|
|
9eefe13e8b | ||
|
|
7eab6a2f1d | ||
|
|
2306df5fb7 | ||
|
|
56ecacee21 | ||
|
|
a60b3d4b08 | ||
|
|
b14798b405 | ||
|
|
8382a88efe | ||
|
|
53057c4bf2 | ||
|
|
3e0f38e8c3 | ||
|
|
a5bbe765b9 | ||
|
|
4455adc813 | ||
|
|
abca763aa5 | ||
|
|
5c74349de0 | ||
|
|
4a7b72dae1 | ||
|
|
23e17d7f30 | ||
|
|
37cf829188 | ||
|
|
f213ffabe1 | ||
|
|
a1921bb767 | ||
|
|
213c04bc8a | ||
|
|
916eb530a0 | ||
|
|
1f0644ea91 | ||
|
|
b20147255a | ||
|
|
38728eb342 | ||
|
|
18c7890f65 | ||
|
|
1c224609b9 | ||
|
|
4b81468c9c | ||
|
|
cffac2a56a | ||
|
|
05c0f8d69f | ||
|
|
5db5bc65de | ||
|
|
952ab032f9 | ||
|
|
2df6f2b8b1 | ||
|
|
62a12a64a3 | ||
|
|
6935d54870 | ||
|
|
049427c6ca | ||
|
|
65e8526ee2 | ||
|
|
8ce71e792e | ||
|
|
202762027f | ||
|
|
d95551e651 | ||
|
|
44d68ad723 | ||
|
|
9e4c9d3101 | ||
|
|
050692952e | ||
|
|
c96fbfdcd6 | ||
|
|
ab90d9d01c | ||
|
|
281c0068d9 | ||
|
|
e7b74939cb | ||
|
|
c2ae58bf08 | ||
|
|
14e8026e30 | ||
|
|
eb29a42209 | ||
|
|
6fdaad1bf4 | ||
|
|
dfa6c502dc | ||
|
|
b958dcb891 | ||
|
|
6e5d64d403 | ||
|
|
3e0c2bf1a1 | ||
|
|
9c4896d72b | ||
|
|
283cdee5d6 | ||
|
|
ab5e01e54a | ||
|
|
01fec1a0cf | ||
|
|
caf13eb774 | ||
|
|
373248e304 | ||
|
|
fef342b489 | ||
|
|
6e9adece1f | ||
|
|
80308ceafa | ||
|
|
f65518f865 | ||
|
|
c0315e2c30 | ||
|
|
2f20ccf289 | ||
|
|
1a7d60bb88 | ||
|
|
90b1895f19 | ||
|
|
7945a95522 | ||
|
|
40fe6369cb | ||
|
|
f155042958 | ||
|
|
1dd23a3f47 | ||
|
|
55da3ee275 | ||
|
|
1194e40222 | ||
|
|
05fac41534 | ||
|
|
3f85e89f62 | ||
|
|
ee0f8ad19a | ||
|
|
b7d7cf233a | ||
|
|
38a708e12b | ||
|
|
53dcd94f0d | ||
|
|
a3ccc3dfef | ||
|
|
bd208c31e2 | ||
|
|
151dc352c8 | ||
|
|
77d8504baf | ||
|
|
ccbf17106d | ||
|
|
95c4d95fd3 | ||
|
|
484772e3b2 | ||
|
|
5c7a1fb407 | ||
|
|
064981ff3d | ||
|
|
fe003d7496 | ||
|
|
fae1df7f4b | ||
|
|
a72c07b657 | ||
|
|
0bff76e5f1 | ||
|
|
c7b062f483 | ||
|
|
83f72f3e41 | ||
|
|
d8b71d76dd | ||
|
|
a6d2f385af | ||
|
|
db9e397531 | ||
|
|
18dea6c3a3 | ||
|
|
8ebaecc472 | ||
|
|
58e0b26493 | ||
|
|
c75380e063 | ||
|
|
708492afeb | ||
|
|
1305ab3cc6 | ||
|
|
29cc6b4f9c | ||
|
|
cc7f0b145c | ||
|
|
e69c0c3e27 | ||
|
|
a209966427 | ||
|
|
d5abbd4220 | ||
|
|
43a75b64b4 | ||
|
|
70a23a14c4 | ||
|
|
93c81ea49c | ||
|
|
3a741d1c14 | ||
|
|
ba442e1549 | ||
|
|
8c15296d07 | ||
|
|
d26464c810 | ||
|
|
8ee202e5a1 | ||
|
|
689cddfd0c | ||
|
|
25950bb5a5 | ||
|
|
1da623e63f | ||
|
|
4bf9e24d43 | ||
|
|
b41a7b8547 | ||
|
|
f500a00d04 | ||
|
|
64a2a08d24 | ||
|
|
1f58f96e88 | ||
|
|
dc3d802d3d | ||
|
|
5765d1c56c | ||
|
|
abcd050c69 | ||
|
|
f40ef26c69 | ||
|
|
fccd1a5bd7 | ||
|
|
16012a3881 | ||
|
|
ddc41027ab | ||
|
|
4f931fbe6a | ||
|
|
7ada3692cf | ||
|
|
1ab5d5027f | ||
|
|
1f16816fe4 | ||
|
|
daf048e258 | ||
|
|
f3d13005b2 | ||
|
|
25a44e1387 | ||
|
|
9e9612cf1f | ||
|
|
304c44048f | ||
|
|
99e64ad387 | ||
|
|
0f0c45af8e | ||
|
|
f2977cf938 | ||
|
|
bb80da137d | ||
|
|
f4b16a255c | ||
|
|
b49a4734ff | ||
|
|
ec8c30f060 | ||
|
|
7990400c7a | ||
|
|
2aaa2f3033 | ||
|
|
202b9f3075 | ||
|
|
eee5cf5fb4 | ||
|
|
be0814cdac | ||
|
|
f5c699ab7a | ||
|
|
80d719353c | ||
|
|
fa3fc12594 | ||
|
|
422a9db07b | ||
|
|
a4145a30f5 | ||
|
|
3d67c7930c | ||
|
|
b55ec38c35 | ||
|
|
02a1cfb457 | ||
|
|
b2ba38b5de | ||
|
|
dcf1a593f7 | ||
|
|
785b61be2f | ||
|
|
39c7782019 | ||
|
|
1c5c51907a | ||
|
|
e004671346 | ||
|
|
a59014cad0 | ||
|
|
38e5c161e7 | ||
|
|
87650de9bc | ||
|
|
ee5596067e | ||
|
|
870fec6bbd | ||
|
|
686ab14b43 | ||
|
|
fced0cf3b1 | ||
|
|
25dd53290c | ||
|
|
a7c1f7ba69 | ||
|
|
de8e27feb8 | ||
|
|
e7144142e5 | ||
|
|
b6f2a434cf | ||
|
|
caf558f8dd | ||
|
|
6f2f1b7a76 | ||
|
|
744ef1958b | ||
|
|
08b44e1857 | ||
|
|
580b60550c | ||
|
|
e9755d437e | ||
|
|
20f695e8d7 | ||
|
|
1d7ff1f9e4 | ||
|
|
5b18f1d76d | ||
|
|
d880307a9b | ||
|
|
e5db66351e | ||
|
|
89153eef23 | ||
|
|
97c24c8b9c | ||
|
|
43535ae573 | ||
|
|
61643f676c | ||
|
|
9b8c8c4971 | ||
|
|
033ca0d56b | ||
|
|
28a6797595 | ||
|
|
953db56a0d | ||
|
|
68ce13368e | ||
|
|
82cf474863 | ||
|
|
edfcac3d5c | ||
|
|
774e11c827 | ||
|
|
43f77376b6 | ||
|
|
c0ba92f503 | ||
|
|
90cb0357c6 | ||
|
|
d55e55ebcc | ||
|
|
c2522329fd | ||
|
|
2470c1788e | ||
|
|
230d259551 | ||
|
|
a55db1d52b | ||
|
|
cb533335c4 | ||
|
|
a8890e4b13 | ||
|
|
0281e0dba4 | ||
|
|
1c209f49fc | ||
|
|
a4701866a4 | ||
|
|
12f72c8ca9 | ||
|
|
c1165bd12d | ||
|
|
215fb53c52 | ||
|
|
8df780b237 | ||
|
|
79679cbb16 | ||
|
|
fb2db4b918 | ||
|
|
05b66f1dcf | ||
|
|
0f1b2003be | ||
|
|
6f91da9461 | ||
|
|
63666fca48 | ||
|
|
d279b6c232 | ||
|
|
17f7f920c4 | ||
|
|
b55c86544b | ||
|
|
af1d5b86e1 | ||
|
|
137e8d042f | ||
|
|
ee96c5599c | ||
|
|
4ceaedcbe8 | ||
|
|
f375cc9a82 | ||
|
|
5937ed57ce | ||
|
|
2e3ed0c23f | ||
|
|
5d1d2ef289 | ||
|
|
480c224250 | ||
|
|
cd731c3ad2 | ||
|
|
9bc49e3381 | ||
|
|
f961b75bba | ||
|
|
1e16fb8ca2 | ||
|
|
c332528185 | ||
|
|
387c5e67f3 | ||
|
|
2ed780e14d | ||
|
|
1b8714fe7f | ||
|
|
e28f8cae74 | ||
|
|
87ef98dad5 | ||
|
|
e6e71e9278 | ||
|
|
02220d02ed | ||
|
|
ff7b77bda7 | ||
|
|
f8ffae75c4 | ||
|
|
cb350b26a1 | ||
|
|
dccebb0bea | ||
|
|
4cefbb52e1 | ||
|
|
d757009b48 | ||
|
|
ca202711e1 | ||
|
|
f04229d8cb | ||
|
|
076d64df8f | ||
|
|
3d7479f9aa | ||
|
|
76ffc2d268 | ||
|
|
d0d118b31e | ||
|
|
895b5b2ee1 | ||
|
|
21702c090d | ||
|
|
9fd0e9af66 | ||
|
|
648a8f9237 | ||
|
|
c4254106e8 | ||
|
|
4e1ae1bc1a | ||
|
|
b6ac1dea4d | ||
|
|
219d9af885 | ||
|
|
c6bba54573 | ||
|
|
f53cae0faa | ||
|
|
981336ed5e | ||
|
|
3864ce6855 | ||
|
|
6953a57333 | ||
|
|
a109f11926 | ||
|
|
45c9904e05 | ||
|
|
08fc32cdc6 | ||
|
|
6c10f1e364 | ||
|
|
e8549ffb79 | ||
|
|
8a8d89dfc0 | ||
|
|
b6c4376217 | ||
|
|
bd5e47f5fc | ||
|
|
2aa756af38 | ||
|
|
78c2840b22 | ||
|
|
af0a516a79 | ||
|
|
651beb4b9c | ||
|
|
f4d04a3dcb | ||
|
|
d573da55b0 | ||
|
|
3c4be537d9 | ||
|
|
9800331505 | ||
|
|
7728d5b317 | ||
|
|
8f47ed8b0a | ||
|
|
c137e682dc | ||
|
|
14c639a425 | ||
|
|
06bfb1ad26 | ||
|
|
33c3611345 | ||
|
|
e012046f62 | ||
|
|
ebf3730454 | ||
|
|
ec0183ce94 | ||
|
|
3cf823ffb3 | ||
|
|
6231a9f931 | ||
|
|
dd30e939ae | ||
|
|
6f2e1d3794 | ||
|
|
2e41bd7607 | ||
|
|
b9907ec401 | ||
|
|
416e9e8e1d | ||
|
|
83d41dba6f | ||
|
|
7284fb539f | ||
|
|
f932f3efb1 | ||
|
|
60bc88a075 | ||
|
|
6eb686c06b | ||
|
|
f587ed4ade | ||
|
|
065b50f5a2 | ||
|
|
85b24e1e8d | ||
|
|
9653e72e47 | ||
|
|
a80f114d66 | ||
|
|
74ae4743d8 | ||
|
|
d1d30e7eb5 | ||
|
|
e83be01475 | ||
|
|
22efd6574d | ||
|
|
bb5a103944 | ||
|
|
34b3520fb2 | ||
|
|
3217ba5a77 | ||
|
|
a91caded9e | ||
|
|
05ba1c3e64 | ||
|
|
77f025eb8d | ||
|
|
aacec1809b | ||
|
|
0435f560a4 | ||
|
|
766f034e5e | ||
|
|
8502d9d21b | ||
|
|
6c874b2bb7 | ||
|
|
f8a2291a55 | ||
|
|
8c302e314f | ||
|
|
a830c27ceb | ||
|
|
4c12af957c | ||
|
|
9ea3c54b92 | ||
|
|
4620764111 | ||
|
|
ca86137d0f | ||
|
|
b299a732c0 | ||
|
|
7a4c9d9933 | ||
|
|
91d15ea221 | ||
|
|
b043fec0d5 | ||
|
|
0bab46eb5c | ||
|
|
82bff09373 | ||
|
|
329b2d30d0 | ||
|
|
0d65b652d4 | ||
|
|
9d3c19e86a | ||
|
|
56a7800519 | ||
|
|
ba0cebd713 | ||
|
|
e28628d148 | ||
|
|
40bc860dc6 | ||
|
|
decf32fdd5 | ||
|
|
d9d6ee9922 | ||
|
|
903609a38f | ||
|
|
4504903b4c | ||
|
|
4bf4972b6e | ||
|
|
47e4b41dd2 | ||
|
|
af413ff1c0 | ||
|
|
5fcf0808c6 | ||
|
|
fb956b3aa1 | ||
|
|
93986af181 | ||
|
|
37a8bf7bfc | ||
|
|
e60e36a0e2 | ||
|
|
199e182399 | ||
|
|
a9f4b29f32 | ||
|
|
22cd43b8a2 | ||
|
|
2d61644b05 | ||
|
|
084816fb9f | ||
|
|
b5ea90f740 | ||
|
|
e6839e4983 | ||
|
|
82d3e466be | ||
|
|
b0dacf6b11 | ||
|
|
64d090839d | ||
|
|
12d3994f45 | ||
|
|
e3b6b24c5f | ||
|
|
1eb7205c12 | ||
|
|
92f4bdae03 | ||
|
|
bd63a460eb | ||
|
|
02e975f594 | ||
|
|
fdeabc15ab | ||
|
|
8363cb7449 | ||
|
|
da43a4d3b4 | ||
|
|
69d21e20c9 | ||
|
|
a925d6710a | ||
|
|
f5218e207b | ||
|
|
7fb6a095c6 | ||
|
|
6a0bb2f452 | ||
|
|
d02f85315a | ||
|
|
fbd5c404d2 | ||
|
|
32b65e8dbc | ||
|
|
bf153eb96b | ||
|
|
f16fcf25e2 | ||
|
|
0048f9725d | ||
|
|
4bb5592e75 | ||
|
|
d7fbd3c3bc | ||
|
|
0b62bee90d | ||
|
|
a4a88769af | ||
|
|
2dce8d09b8 | ||
|
|
e6e6401702 | ||
|
|
e83ece392c | ||
|
|
bb161f9da8 | ||
|
|
07360efd17 | ||
|
|
7c8eaaa4f9 | ||
|
|
01e04843bf | ||
|
|
3907f39c29 | ||
|
|
c570c0929f | ||
|
|
f53473f9e9 | ||
|
|
39e6d28826 | ||
|
|
0b56d07a67 |
2
.github/workflows/commit-checker.yml
vendored
2
.github/workflows/commit-checker.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
- name: Check Commit Type
|
||||
uses: gsactions/commit-message-checker@v2
|
||||
with:
|
||||
pattern: '^:(lipstick|globe_with_meridians|wrench|books|arrow_up|arrow_down|zap|ambulance|construction|boom|fire|whale|bug|sparkles|paperclip|tada|recycle):\s[A-Z].*[^.]$'
|
||||
pattern: '^(Merge|:(lipstick|globe_with_meridians|wrench|books|arrow_up|arrow_down|zap|ambulance|construction|boom|fire|whale|bug|sparkles|paperclip|tada|recycle):)\s[A-Z].*[^.]$'
|
||||
flags: 'gm'
|
||||
error: 'Commit should match CONTRIBUTING.md guideline'
|
||||
checkAllCommitMessages: 'true' # optional: this checks all commits associated with a pull request
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -68,6 +68,8 @@
|
||||
/vendor/**/target
|
||||
/vendor/svgclean/bundle*.js
|
||||
/web
|
||||
/library/target/
|
||||
|
||||
clj-profiler/
|
||||
node_modules
|
||||
/test-results/
|
||||
|
||||
161
CHANGES.md
161
CHANGES.md
@@ -1,5 +1,165 @@
|
||||
# CHANGELOG
|
||||
|
||||
## 2.8.0 (Next / Unreleased)
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
**Breaking changes on penpot library:**
|
||||
|
||||
The library entrypoint API object has been changed. From now you start creating a new
|
||||
build context, from where you can add multiple files and attach media. This change add the
|
||||
ability to build more than one file at same time and export them in an unique .penpot
|
||||
file.
|
||||
|
||||
```js
|
||||
const context = penpot.createBuildContext()
|
||||
|
||||
context.addFile({name:"aa"})
|
||||
context.addPage({name:"aa"})
|
||||
context.closePage()
|
||||
context.closeFile()
|
||||
|
||||
;; barray is instance of Uint8Array
|
||||
const barray = penpot.exportAsBytes(context);
|
||||
```
|
||||
|
||||
The previous `file.export()` method has been removed and several alternatives are
|
||||
added as first level functions on penpot library API entrypoint:
|
||||
|
||||
- `exportAsBytes(BuildContext context) -> Promise<Uint8Array>`
|
||||
- `exportAsBlob(BuildContext context) -> Promise<Blob>`
|
||||
- `exportStream(BuildContext context, WritableStream stream) -> Promise<Void>`
|
||||
|
||||
The stream variant allows writting data as it is generated to the stream, without the need
|
||||
to store the generated output entirelly in the memory.
|
||||
|
||||
There are also relevant semantic changes in how components should be created: this
|
||||
refactor removes all notions of the old components (v1). Since v2, the shapes that are
|
||||
part of a component live on a page. So, from now on, to create a component, you should
|
||||
first create a frame, then add shapes and/or groups to that frame, and then create a
|
||||
component by declaring that frame as the component root.
|
||||
|
||||
A non exhaustive list of changes:
|
||||
|
||||
- Change the signature of the `addPage` method: it now accepts an object (as a single argument) where you can pass `id`,
|
||||
`name`, and `background` props (instead of the previous positional arguments)
|
||||
- Rename the `createRect` method to `addRect`
|
||||
- Rename the `createCircle` method to `addCircle`
|
||||
- Rename the `createPath` method to `addPath`
|
||||
- Rename the `createText` method to `addText`
|
||||
- Rename the `addArtboard` method to `addBoard`
|
||||
- Rename `startComponent` to `addComponent` (to preserve the naming style)
|
||||
- Rename `createComponentInstance` to `addComponentInstance` (to preserve the naming style)
|
||||
- Remove `lookupShape`
|
||||
- Remove `asMap`
|
||||
- Remove `updateLibraryColor` (use `addLibraryColor` if you just need to replace a color)
|
||||
- Remove `deleteLibraryColor` (this library is intended to build files)
|
||||
- Remove `updateLibraryTypography` (use `addLibraryTypography` if you just need to replace a typography)
|
||||
- Remove `deleteLibraryTypography` (this library is intended to build files)
|
||||
- Remove `add/update/deleteLibraryMedia` (they are no longer supported by Penpot and have been replaced by components)
|
||||
- Remove `deleteObject` (this library is intended to build files)
|
||||
- Remove `updateObject` (this library is intended to build files)
|
||||
- Remove `finishComponent` (it is no longer necessary; see below for more details on component creation changes)
|
||||
|
||||
- Change the `getCurrentPageId` function to a read-only `currentPageId` property
|
||||
- Add `currentFileId` read-only property
|
||||
- Add `currentFrameId` read-only property
|
||||
- Add `lastId` read-only property
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
- Optimize profile setup flow for better user experience [Taiga #10028](https://tree.taiga.io/project/penpot/us/10028)
|
||||
- Update base image for Docker Backend and Exporter to Ubuntu 24.04
|
||||
- Update base image for Docker Frontend to Nginx 1.28.0
|
||||
- Allow multi file token import [Github #27](https://github.com/tokens-studio/penpot/issues/27)
|
||||
- Create `input*` wrapper component, and `label*`, `input-field*` and `hint-message*` components [Taiga #10713](https://tree.taiga.io/project/penpot/us/10713)
|
||||
- Deselect layers (and path nodes) with Ctrl+Shift+Drag [Github #2509](https://github.com/penpot/penpot/issues/2509)
|
||||
- Copy to SVG from contextual menu [Github #838](https://github.com/penpot/penpot/issues/838)
|
||||
- Add styles for Inkeep Chat at workspace [Taiga #10708](https://tree.taiga.io/project/penpot/us/10708)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix spacing / sizes of different elements in the measurements section of the design tab [Taiga #11076](https://tree.taiga.io/project/penpot/issue/11076)
|
||||
- Fix selection of short paths [Github #4472](https://github.com/penpot/penpot/issues/4472)
|
||||
- Fix element positioning on the right side to adjust to grid [#11073](https://tree.taiga.io/project/penpot/issue/11073)
|
||||
|
||||
## 2.7.0 (Unreleased)
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
|
||||
- Design improvements to the Invitations page with an empty state [GitHub #2608](https://github.com/penpot/penpot/issues/2608) by [@iprithvitharun](https://github.com/iprithvitharun)
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
- Update board presets with a newer devices [Taiga #10610](https://tree.taiga.io/project/penpot/us/10610)
|
||||
- Propagate "sharing a prototype" to editors and viewers [Taiga #8853](https://tree.taiga.io/project/penpot/us/8853)
|
||||
- Design improvements to the Invitations page with an empty state [Taiga #4554](https://tree.taiga.io/project/penpot/us/4554)
|
||||
- Duplicate token sets [Taiga #10694](https://tree.taiga.io/project/penpot/issue/10694)
|
||||
- Add set selection in create Token themes flow [Taiga #10746](https://tree.taiga.io/project/penpot/issue/10746)
|
||||
- Display indicator on not active sets [Taiga #10668](https://tree.taiga.io/project/penpot/issue/10668)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix "at" icon to match all icons on app [Taiga #11136](https://tree.taiga.io/project/penpot/issue/11136)
|
||||
- Fix problem in viewer with the back button [Taiga #10907](https://tree.taiga.io/project/penpot/issue/10907)
|
||||
- Fix resize bar background on tokens panel [Taiga #10811](https://tree.taiga.io/project/penpot/issue/10811)
|
||||
- Fix shortcut for history version panel [Taiga #11006](https://tree.taiga.io/project/penpot/issue/11006)
|
||||
- Fix positioning of comment drafts when near the right / bottom edges of viewport [Taiga #10534](https://tree.taiga.io/project/penpot/issue/10534)
|
||||
- Fix path having a wrong selrect [Taiga #10257](https://tree.taiga.io/project/penpot/issue/10257)
|
||||
- Fix SVG `stroke-linecap` property when importing SVGs [Taiga #9489](https://tree.taiga.io/project/penpot/issue/9489)
|
||||
- Fix position problems cutting-pasting a component [Taiga #10677](https://tree.taiga.io/project/penpot/issue/10677)
|
||||
- Fix design tab has a horizontal scroll [Taiga #10660](https://tree.taiga.io/project/penpot/issue/10660)
|
||||
- Fix long file names being clipped when longer than allowed length [Taiga #10662](https://tree.taiga.io/project/penpot/issue/10662)
|
||||
- Fix problem with error detail in toast [Taiga #10519](https://tree.taiga.io/project/penpot/issue/10519)
|
||||
- Fix view mode error when an external user tries to export something from a prototype using a shared link [Taiga #10251](https://tree.taiga.io/project/penpot/issue/10251)
|
||||
- Fix merge path nodes with only one node selected [Taiga #9626](https://tree.taiga.io/project/penpot/issue/9626)
|
||||
- Fix problem with import errors [Taiga #10040](https://tree.taiga.io/project/penpot/issue/10040)
|
||||
- Fix color gradient on texts [Taiga Issue #7488](https://tree.taiga.io/project/penpot/issue/7488)
|
||||
- Add support for self mentions [Taiga #10809](https://tree.taiga.io/project/penpot/issue/10809)
|
||||
- Fix team info settings alignment [Taiga #10869](https://tree.taiga.io/project/penpot/issue/10869)
|
||||
- Fix left sidebar horizontal scroll on nested layers [Taiga #10791](https://tree.taiga.io/project/penpot/issue/10791)
|
||||
- Improve error message details importing tokens [Taiga Issue #10772](https://tree.taiga.io/project/penpot/issue/10772)
|
||||
- Fix no selected set after Drag & Drop [Github #71](https://github.com/tokens-studio/penpot/issues/71)
|
||||
- Styledictionary v5 Update [Github #6283](https://github.com/penpot/penpot/pull/6283)
|
||||
- Fix Rename a set throws an internal error [Github #78](https://github.com/tokens-studio/penpot/issues/78)
|
||||
- Fix Out of Sync Token Value & Color Picker [Github #102](https://github.com/tokens-studio/penpot/issues/102)
|
||||
- Fix Color should preserve color space [Github #69](https://github.com/tokens-studio/penpot/issues/69)
|
||||
- Fix cannot rename Design Token Sets when group of same name exists [Taiga Issue #10773](https://tree.taiga.io/project/penpot/issue/10773)
|
||||
- Fix problem when duplicating grid layout [Github #6391](https://github.com/penpot/penpot/issues/6391)
|
||||
- Fix issue that makes workspace shortcuts stop working [Taiga #11062](https://tree.taiga.io/project/penpot/issue/11062)
|
||||
- Fix problem while syncing library colors and typographies [Taiga #11068](https://tree.taiga.io/project/penpot/issue/11068)
|
||||
- Fix problem with path edition of shapes [Taiga #9496](https://tree.taiga.io/project/penpot/issue/9496)
|
||||
- Fix exception on paste invalid html [Taiga #11047](https://tree.taiga.io/project/penpot/issue/11047)
|
||||
- Fix share button being displayed with no permissions [Taiga #11086](https://tree.taiga.io/project/penpot/issue/11086)
|
||||
- Fix inline styles in code tab [Taiga Issue #7583](https://tree.taiga.io/project/penpot/issue/7583)
|
||||
- Fix exception on returning openapi.json
|
||||
- Fix json encoding of TokensLib [Taiga #10994](https://tree.taiga.io/project/penpot/issue/10994)
|
||||
|
||||
## 2.6.2
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Increase the height of the right sidebar dropdowns [Taiga #10615](https://tree.taiga.io/project/penpot/issue/10615)
|
||||
- Fix scroll on token themes modal [Taiga #10745](https://tree.taiga.io/project/penpot/issue/10745)
|
||||
- Fix collapsing grouped sets in "edit Theme" closes the dialog [Taiga #10771](https://tree.taiga.io/project/penpot/issue/10771)
|
||||
- Fix unexpected exception on path editor on merge segments when undo stack is empty
|
||||
- Fix pricing CTA to be under a config flag [Taiga #10808](https://tree.taiga.io/project/penpot/issue/10808)
|
||||
- Fix allow moving a main component into another [Taiga #10818](https://tree.taiga.io/project/penpot/issue/10818)
|
||||
- Fix several issues with internal srepl helpers
|
||||
- Fix unexpected exception on template import from libraries
|
||||
- Fix incorrect uuid parsing from different parts of code
|
||||
- Fix update layout on component restore [Taiga #10637](https://tree.taiga.io/project/penpot/issue/10637)
|
||||
- Fix horizontal scroll in viewer [Github #6290](https://github.com/penpot/penpot/issues/6290)
|
||||
- Fix detach component in a particular case [Taiga #10837](https://tree.taiga.io/project/penpot/issue/10837)
|
||||
|
||||
## 2.6.1
|
||||
|
||||
### :bug: Bugs fixed
|
||||
@@ -58,6 +218,7 @@
|
||||
- Add character limitation to asset inputs [Taiga #10669](https://tree.taiga.io/project/penpot/issue/10669)
|
||||
- Fix Storybook link 'list of all available icons' wrong path [Taiga #10705](https://tree.taiga.io/project/penpot/issue/10705)
|
||||
|
||||
|
||||
## 2.5.4
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
|
||||
133
CONTRIBUTING.md
133
CONTRIBUTING.md
@@ -1,62 +1,59 @@
|
||||
# Contributing Guide #
|
||||
|
||||
Thank you for your interest in contributing to Penpot. This is a
|
||||
generic guide that details how to contribute to Penpot in a way that
|
||||
is efficient for everyone. If you want a specific documentation for
|
||||
different parts of the platform, please refer to `docs/` directory.
|
||||
|
||||
generic guide that details how to contribute to the project in a way that
|
||||
is efficient for everyone. If you are looking for specific documentation on
|
||||
different parts of the platform, please refer to the `docs/` directory,
|
||||
or the rendered version at the [Help Center](https://help.penpot.app/).
|
||||
|
||||
## Reporting Bugs ##
|
||||
|
||||
We are using [GitHub Issues](https://github.com/penpot/penpot/issues)
|
||||
for our public bugs. We keep a close eye on this and try to make it
|
||||
for our public bugs. We keep a close eye on them and try to make it
|
||||
clear when we have an internal fix in progress. Before filing a new
|
||||
task, try to make sure your problem doesn't already exist.
|
||||
|
||||
If you found a bug, please report it, as far as possible with:
|
||||
If you found a bug, please report it, as far as possible, with:
|
||||
|
||||
- a detailed explanation of steps to reproduce the error
|
||||
- a browser and the browser version used
|
||||
- a dev tools console exception stack trace (if it is available)
|
||||
- the browser and browser version used
|
||||
- a dev tools console exception stack trace (if available)
|
||||
|
||||
If you found a bug that you consider better discuss in private (for
|
||||
example: security bugs), consider first send an email to
|
||||
If you found a bug which you think is better to discuss in private (for
|
||||
example, security bugs), consider first sending an email to
|
||||
`support@penpot.app`.
|
||||
|
||||
**We don't have formal bug bounty program for security reports; this
|
||||
is an open source application and your contribution will be recognized
|
||||
**We don't have a formal bug bounty program for security reports; this
|
||||
is an open source application, and your contribution will be recognized
|
||||
in the changelog.**
|
||||
|
||||
|
||||
## Pull requests ##
|
||||
## Pull Requests ##
|
||||
|
||||
If you want propose a change or bug fix with the Pull-Request system
|
||||
firstly you should carefully read the **DCO** section and format your
|
||||
commits accordingly.
|
||||
If you want to propose a change or bug fix via a pull request (PR),
|
||||
you should first carefully read the section **Developer's Certificate of
|
||||
Origin**. You must also format your code and commits according to the
|
||||
instructions below.
|
||||
|
||||
If you intend to fix a bug it's fine to submit a pull request right
|
||||
away but we still recommend to file an issue detailing what you're
|
||||
If you intend to fix a bug, it's fine to submit a pull request right
|
||||
away, but we still recommend filing an issue detailing what you're
|
||||
fixing. This is helpful in case we don't accept that specific fix but
|
||||
want to keep track of the issue.
|
||||
|
||||
If you want to implement or start working in a new feature, please
|
||||
open a **question** / **discussion** issue for it. No pull-request
|
||||
will be accepted without previous chat about the changes,
|
||||
independently if it is a new feature, already planned feature or small
|
||||
quick win.
|
||||
If you want to implement or start working on a new feature, please
|
||||
open a **question*- / **discussion*- issue for it. No PR
|
||||
will be accepted without a prior discussion about the changes,
|
||||
whether it is a new feature, an already planned one, or a quick win.
|
||||
|
||||
If is going to be your first pull request, You can learn how from this
|
||||
free video series:
|
||||
|
||||
https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github
|
||||
|
||||
We will use the `easy fix` mark for tag for indicate issues that are
|
||||
easy for beginners.
|
||||
If it is your first PR, you can learn how to proceed from
|
||||
[this free video
|
||||
series](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github)
|
||||
|
||||
We use the `easy fix` tag to indicate issues that are appropriate for beginners.
|
||||
|
||||
## Commit Guidelines ##
|
||||
|
||||
We have very precise rules over how our git commit messages can be formatted.
|
||||
We have very precise rules on how our git commit messages must be formatted.
|
||||
|
||||
The commit message format is:
|
||||
|
||||
@@ -71,34 +68,37 @@ The commit message format is:
|
||||
Where type is:
|
||||
|
||||
- :bug: `:bug:` a commit that fixes a bug
|
||||
- :sparkles: `:sparkles:` a commit that an improvement
|
||||
- :tada: `:tada:` a commit with new feature
|
||||
- :sparkles: `:sparkles:` a commit that adds an improvement
|
||||
- :tada: `:tada:` a commit with a new feature
|
||||
- :recycle: `:recycle:` a commit that introduces a refactor
|
||||
- :lipstick: `:lipstick:` a commit with cosmetic changes
|
||||
- :ambulance: `:ambulance:` a commit that fixes critical bug
|
||||
- :ambulance: `:ambulance:` a commit that fixes a critical bug
|
||||
- :books: `:books:` a commit that improves or adds documentation
|
||||
- :construction: `:construction:`: a wip commit
|
||||
- :construction: `:construction:` a WIP commit
|
||||
- :boom: `:boom:` a commit with breaking changes
|
||||
- :wrench: `:wrench:` a commit for config updates
|
||||
- :zap: `:zap:` a commit with performance improvements
|
||||
- :whale: `:whale:` a commit for docker related stuff
|
||||
- :paperclip: `:paperclip:` a commit with other not relevant changes
|
||||
- :arrow_up: `:arrow_up:` a commit with dependencies updates
|
||||
- :arrow_down: `:arrow_down:` a commit with dependencies downgrades
|
||||
- :whale: `:whale:` a commit for Docker-related stuff
|
||||
- :paperclip: `:paperclip:` a commit with other non-relevant changes
|
||||
- :arrow_up: `:arrow_up:` a commit with dependency updates
|
||||
- :arrow_down: `:arrow_down:` a commit with dependency downgrades
|
||||
- :fire: `:fire:` a commit that removes files or code
|
||||
- :globe_with_meridians: `:globe_with_meridians:` a commit that adds or updates
|
||||
translations
|
||||
|
||||
More info:
|
||||
|
||||
- https://gist.github.com/parmentf/035de27d6ed1dce0b36a
|
||||
- https://gist.github.com/rxaviers/7360908
|
||||
|
||||
Each commit should have:
|
||||
|
||||
- A concise subject using imperative mood.
|
||||
- The subject should have capitalized the first letter, without period
|
||||
at the end and no larger than 65 characters.
|
||||
- A concise subject using the imperative mood.
|
||||
- The subject should capitalize the first letter, omit the period
|
||||
at the end, and be no longer than 65 characters.
|
||||
- A blank line between the subject line and the body.
|
||||
- An entry on the CHANGES.md file if applicable, referencing the
|
||||
github or taiga issue/user-story using the these same rules.
|
||||
- An entry in the CHANGES.md file if applicable, referencing the
|
||||
GitHub or Taiga issue/user story using these same rules.
|
||||
|
||||
Examples of good commit messages:
|
||||
|
||||
@@ -111,8 +111,30 @@ Examples of good commit messages:
|
||||
- `:ambulance: Fix critical bug on user registration process`
|
||||
- `:tada: Add new approach for user registration`
|
||||
|
||||
## Formatting and Linting ##
|
||||
|
||||
## Code of conduct ##
|
||||
You will want to make sure your code is formatted and linted before submitting
|
||||
a PR. We use [cljfmt](https://github.com/weavejester/cljfmt) and
|
||||
[clj-kondo](https://github.com/clj-kondo/clj-kondo) for this. After installing
|
||||
them on your system, you can run them with:
|
||||
|
||||
```bash
|
||||
# Check formatting
|
||||
yarn fmt:clj:check
|
||||
|
||||
# Check and fix formatting
|
||||
yarn fmt:clj
|
||||
|
||||
# Run the linter
|
||||
yarn lint:clj
|
||||
```
|
||||
|
||||
There are more choices in `package.json`.
|
||||
|
||||
Ideally, you should run these commands as git pre-commit hooks. A convenient way
|
||||
of defining them is to use [Husky](https://typicode.github.io/husky/#/).
|
||||
|
||||
## Code of Conduct ##
|
||||
|
||||
As contributors and maintainers of this project, we pledge to respect
|
||||
all people who contribute through reporting issues, posting feature
|
||||
@@ -132,11 +154,11 @@ unprofessional conduct.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit,
|
||||
or reject comments, commits, code, wiki edits, issues, and other
|
||||
contributions that are not aligned to this Code of Conduct. Project
|
||||
contributions that are not aligned with this Code of Conduct. Project
|
||||
maintainers who do not follow the Code of Conduct may be removed from
|
||||
the project team.
|
||||
|
||||
This code of conduct applies both within project spaces and in public
|
||||
This Code of Conduct applies both within project spaces and in public
|
||||
spaces when an individual is representing the project or its
|
||||
community.
|
||||
|
||||
@@ -145,12 +167,11 @@ may be reported by opening an issue or contacting one or more of the
|
||||
project maintainers.
|
||||
|
||||
This Code of Conduct is adapted from the Contributor Covenant, version
|
||||
1.1.0, available from http://contributor-covenant.org/version/1/1/0/
|
||||
1.1.0, available from [http://contributor-covenant.org/version/1/1/0/](http://contributor-covenant.org/version/1/1/0/)
|
||||
|
||||
## Developer's Certificate of Origin (DCO)
|
||||
|
||||
## Developer's Certificate of Origin (DCO) ##
|
||||
|
||||
By submitting code you are agree and can certify the below:
|
||||
By submitting code you agree to and can certify the following:
|
||||
|
||||
Developer's Certificate of Origin 1.1
|
||||
|
||||
@@ -178,13 +199,15 @@ By submitting code you are agree and can certify the below:
|
||||
maintained indefinitely and may be redistributed consistent with
|
||||
this project or the open source license(s) involved.
|
||||
|
||||
Then, all your code patches (**documentation are excluded**) should
|
||||
Then, all your code patches (**documentation is excluded**) should
|
||||
contain a sign-off at the end of the patch/commit description body. It
|
||||
can be automatically added on adding `-s` parameter to `git commit`.
|
||||
can be automatically added by adding the `-s` parameter to `git commit`.
|
||||
|
||||
This is an example of the aspect of the line:
|
||||
This is an example of what the line should look like:
|
||||
|
||||
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
|
||||
```
|
||||
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
|
||||
```
|
||||
|
||||
Please, use your real name (sorry, no pseudonyms or anonymous
|
||||
contributions are allowed).
|
||||
|
||||
41
README.md
41
README.md
@@ -16,36 +16,37 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://penpot.app/"><b>Website</b></a> •
|
||||
<a href="https://help.penpot.app/technical-guide/getting-started/"><b>Getting Started</b></a> •
|
||||
<a href="https://help.penpot.app/user-guide/"><b>User Guide</b></a> •
|
||||
<a href="https://help.penpot.app/user-guide/introduction/info/"><b>Tutorials & Info</b></a> •
|
||||
<a href="https://penpot.app/"><b>Website</b></a> •
|
||||
<a href="https://help.penpot.app/user-guide/"><b>User Guide</b></a> •
|
||||
<a href="https://penpot.app/learning-center"><b>Learning Center</b></a> •
|
||||
<a href="https://community.penpot.app/"><b>Community</b></a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://www.youtube.com/@Penpot"><b>Youtube</b></a> •
|
||||
<a href="https://peertube.kaleidos.net/a/penpot_app/video-channels"><b>Peertube</b></a> •
|
||||
<a href="https://www.linkedin.com/company/penpot/"><b>Linkedin</b></a> •
|
||||
<a href="https://instagram.com/penpot.app"><b>Instagram</b></a> •
|
||||
<a href="https://fosstodon.org/@penpot/"><b>Mastodon</b></a> •
|
||||
<a href="https://www.youtube.com/@Penpot"><b>Youtube</b></a> •
|
||||
<a href="https://peertube.kaleidos.net/a/penpot_app/video-channels"><b>Peertube</b></a> •
|
||||
<a href="https://www.linkedin.com/company/penpot/"><b>Linkedin</b></a> •
|
||||
<a href="https://instagram.com/penpot.app"><b>Instagram</b></a> •
|
||||
<a href="https://fosstodon.org/@penpot/"><b>Mastodon</b></a> •
|
||||
<a href="https://bsky.app/profile/penpot.app"><b>Bluesky</b></a> •
|
||||
<a href="https://twitter.com/penpotapp"><b>X</b></a>
|
||||
|
||||
</p>
|
||||
|
||||
<br />
|
||||
|
||||
[Penpot video](https://github.com/penpot/penpot/assets/5446186/b8ad0764-585e-4ddc-b098-9b4090d337cc)
|
||||
[Penpot video](https://github.com/user-attachments/assets/08b83119-c090-4a74-86ed-7bfbdda9a793)
|
||||
|
||||
<br />
|
||||
|
||||
Penpot is the first **open-source** design tool for design and code collaboration. Designers can create stunning designs, interactive prototypes, design systems at scale, while developers enjoy ready-to-use code and make their workflow easy and fast. And all of this with no handoff drama.
|
||||
|
||||
Penpot is available on browser and [self host](https://penpot.app/self-host). It’s web-based and works with open standards (SVG, CSS and HTML). And last but not least, it’s free!
|
||||
Available on browser or self-hosted, Penpot works with open standards like SVG, CSS, HTML and JSON, and it’s free!
|
||||
|
||||
Penpot’s latest [huge release 2.0](https://penpot.app/dev-diaries), takes the platform to a whole new level. This update introduces the ground-breaking [CSS Grid Layout feature](https://penpot.app/penpot-2.0), a complete UI redesign, a new Components system, and much more. Plus, it's faster and more accessible.
|
||||
The latest updates take Penpot even further. It’s the first design tool to integrate native [design tokens](https://penpot.dev/collaboration/design-tokens)—a single source of truth to improve efficiency and collaboration between product design and development.
|
||||
With the [huge 2.0 release](https://penpot.app/dev-diaries), Penpot took the platform to a whole new level. This update introduces the ground-breaking [CSS Grid Layout feature](https://penpot.app/penpot-2.0), a complete UI redesign, a new Components system, and much more.
|
||||
For organizations that need extra service for its teams, [get in touch](https://cal.com/team/penpot/talk-to-us)
|
||||
|
||||
|
||||
🎇 **Penpot Fest** is our design, code & Open Source event. Check out the highlights from [Penpot Fest 2023 edition](https://www.youtube.com/watch?v=sOpLZaK5mDc)!
|
||||
🎇 Design, code, and Open Source meet at [Penpot Fest](https://penpot.app/penpotfest)! Be part of the 2025 edition in Madrid, Spain, on October 9-10.
|
||||
|
||||
## Table of contents ##
|
||||
|
||||
@@ -61,7 +62,7 @@ Penpot’s latest [huge release 2.0](https://penpot.app/dev-diaries), takes the
|
||||
Penpot expresses designs as code. Designers can do their best work and see it will be beautifully implemented by developers in a two-way collaboration.
|
||||
|
||||
### Plugin system ###
|
||||
[Penpot plugins](https://penpot.app/penpothub/plugins) let you expand the platform's capabilities, give you the flexibility to integrate it with other apps, and design custom solutions.
|
||||
[Penpot plugins](https://penpot.app/penpothub/plugins) let you expand the platform's capabilities, give you the flexibility to integrate it with other apps, and design custom solutions.
|
||||
|
||||
### Designed for developers ###
|
||||
Penpot was built to serve both designers and developers and create a fluid design-code process. You have the choice to enjoy real-time collaboration or play "solo".
|
||||
@@ -78,6 +79,10 @@ Penpot offers integration into the development toolchain, thanks to its support
|
||||
### What’s great for design ###
|
||||
With Penpot you can design libraries to share and reuse; turn design elements into components and tokens to allow reusability and scalability; and build realistic user flows and interactions.
|
||||
|
||||
### Design Tokens ###
|
||||
With Penpot’s standardized [design tokens](https://penpot.dev/collaboration/design-tokens) format, you can easily reuse and sync tokens across different platforms, workflows, and disciplines.
|
||||
|
||||
|
||||
<br />
|
||||
|
||||
<p align="center">
|
||||
@@ -125,13 +130,13 @@ You will find the following categories:
|
||||
|
||||
## Contributing ##
|
||||
|
||||
Any contribution will make a difference to improve Penpot. How can you get involved?
|
||||
Any contribution will make a difference to improve Penpot. How can you get involved?
|
||||
|
||||
Choose your way:
|
||||
Choose your way:
|
||||
|
||||
- Create and [share Libraries & Templates](https://penpot.app/libraries-templates.html) that will be helpful for the community
|
||||
- Invite your [team to join](https://design.penpot.app/#/auth/register)
|
||||
- Star this repo and follow us on Social Media: [Mastodon](https://fosstodon.org/@penpot/), [Youtube](https://www.youtube.com/c/Penpot), [Instagram](https://instagram.com/penpot.app), [Linkedin](https://www.linkedin.com/company/penpotdesign), [Peertube](https://peertube.kaleidos.net/a/penpot_app) and [X](https://twitter.com/penpotapp).
|
||||
- Give this repo a star and follow us on Social Media: [Mastodon](https://fosstodon.org/@penpot/), [Youtube](https://www.youtube.com/c/Penpot), [Instagram](https://instagram.com/penpot.app), [Linkedin](https://www.linkedin.com/company/penpotdesign), [Peertube](https://peertube.kaleidos.net/a/penpot_app), [X](https://twitter.com/penpotapp) and [BlueSky](https://bsky.app/profile/penpot.app)
|
||||
- Participate in the [Community](https://community.penpot.app/) space by asking and answering questions; reacting to others’ articles; opening your own conversations and following along on decisions affecting the project.
|
||||
- Report bugs with our easy [guide for bugs hunting](https://help.penpot.app/contributing-guide/reporting-bugs/) or [GitHub issues](https://github.com/penpot/penpot/issues)
|
||||
- Become a [translator](https://help.penpot.app/contributing-guide/translations)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
org.clojure/clojure {:mvn/version "1.12.0"}
|
||||
org.clojure/tools.namespace {:mvn/version "1.5.0"}
|
||||
|
||||
com.github.luben/zstd-jni {:mvn/version "1.5.6-9"}
|
||||
com.github.luben/zstd-jni {:mvn/version "1.5.7-3"}
|
||||
|
||||
io.prometheus/simpleclient {:mvn/version "0.16.0"}
|
||||
io.prometheus/simpleclient_hotspot {:mvn/version "0.16.0"}
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
io.prometheus/simpleclient_httpserver {:mvn/version "0.16.0"}
|
||||
|
||||
io.lettuce/lettuce-core {:mvn/version "6.5.2.RELEASE"}
|
||||
io.lettuce/lettuce-core {:mvn/version "6.6.0.RELEASE"}
|
||||
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
|
||||
|
||||
funcool/yetti
|
||||
@@ -27,15 +27,15 @@
|
||||
:exclusions [org.slf4j/slf4j-api]}
|
||||
|
||||
com.github.seancorfield/next.jdbc
|
||||
{:mvn/version "1.3.994"}
|
||||
metosin/reitit-core {:mvn/version "0.7.2"}
|
||||
{:mvn/version "1.3.1002"}
|
||||
metosin/reitit-core {:mvn/version "0.8.0"}
|
||||
nrepl/nrepl {:mvn/version "1.3.1"}
|
||||
cider/cider-nrepl {:mvn/version "0.52.0"}
|
||||
cider/cider-nrepl {:mvn/version "0.55.7"}
|
||||
|
||||
org.postgresql/postgresql {:mvn/version "42.7.5"}
|
||||
org.xerial/sqlite-jdbc {:mvn/version "3.48.0.0"}
|
||||
org.xerial/sqlite-jdbc {:mvn/version "3.49.1.0"}
|
||||
|
||||
com.zaxxer/HikariCP {:mvn/version "6.2.1"}
|
||||
com.zaxxer/HikariCP {:mvn/version "6.3.0"}
|
||||
|
||||
io.whitfin/siphash {:mvn/version "2.0.0"}
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
|
||||
com.github.ben-manes.caffeine/caffeine {:mvn/version "3.2.0"}
|
||||
|
||||
org.jsoup/jsoup {:mvn/version "1.18.3"}
|
||||
org.jsoup/jsoup {:mvn/version "1.20.1"}
|
||||
org.im4java/im4java
|
||||
{:git/tag "1.4.0-penpot-2"
|
||||
:git/sha "e2b3e16"
|
||||
@@ -55,11 +55,11 @@
|
||||
org.clojars.pntblnk/clj-ldap {:mvn/version "0.0.17"}
|
||||
|
||||
dawran6/emoji {:mvn/version "0.1.5"}
|
||||
markdown-clj/markdown-clj {:mvn/version "1.12.2"}
|
||||
markdown-clj/markdown-clj {:mvn/version "1.12.3"}
|
||||
|
||||
;; Pretty Print specs
|
||||
pretty-spec/pretty-spec {:mvn/version "0.1.4"}
|
||||
software.amazon.awssdk/s3 {:mvn/version "2.28.26"}}
|
||||
software.amazon.awssdk/s3 {:mvn/version "2.31.48"}}
|
||||
|
||||
:paths ["src" "resources" "target/classes"]
|
||||
:aliases
|
||||
@@ -74,7 +74,7 @@
|
||||
|
||||
:build
|
||||
{:extra-deps
|
||||
{io.github.clojure/tools.build {:git/tag "v0.10.6" :git/sha "52cf7d6"}}
|
||||
{io.github.clojure/tools.build {:git/tag "v0.10.9" :git/sha "e405aac"}}
|
||||
:ns-default build}
|
||||
|
||||
:test
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"license": "MPL-2.0",
|
||||
"author": "Kaleidos INC",
|
||||
"private": true,
|
||||
"packageManager": "yarn@4.8.1+sha512.bc946f2a022d7a1a38adfc15b36a66a3807a67629789496c3714dd1703d2e6c6b1c69ff9ec3b43141ac7a1dd853b7685638eb0074300386a59c18df351ef8ff6",
|
||||
"packageManager": "yarn@4.9.1+sha512.f95ce356460e05be48d66401c1ae64ef84d163dd689964962c6888a9810865e39097a5e9de748876c2e0bf89b232d583c33982773e9903ae7a76257270986538",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/penpot/penpot"
|
||||
|
||||
@@ -35,40 +35,35 @@ def get_prepl_conninfo():
|
||||
|
||||
return host, port
|
||||
|
||||
def send_eval(expr):
|
||||
def send(data):
|
||||
host, port = get_prepl_conninfo()
|
||||
with socket.create_connection((host, port)) as s:
|
||||
f = s.makefile(mode="rw")
|
||||
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
s.connect((host, port))
|
||||
s.send(expr.encode("utf-8"))
|
||||
s.send(b":repl/quit\n\n")
|
||||
json.dump(data, f)
|
||||
f.write("\n")
|
||||
f.flush()
|
||||
|
||||
with s.makefile() as f:
|
||||
while True:
|
||||
line = f.readline()
|
||||
result = json.loads(line)
|
||||
tag = result.get("tag", None)
|
||||
if tag == "ret":
|
||||
return result.get("val", None), result.get("exception", None)
|
||||
elif tag == "out":
|
||||
print(result.get("val"), end="")
|
||||
else:
|
||||
raise RuntimeError("unexpected response from PREPL")
|
||||
while True:
|
||||
line = f.readline()
|
||||
result = json.loads(line)
|
||||
tag = result.get("tag", None)
|
||||
|
||||
def encode(val):
|
||||
return json.dumps(json.dumps(val))
|
||||
if tag == "ret":
|
||||
return result.get("val", None), result.get("err", None)
|
||||
elif tag == "out":
|
||||
print(result.get("val"), end="")
|
||||
else:
|
||||
raise RuntimeError("unexpected response from PREPL")
|
||||
|
||||
def print_error(res):
|
||||
for error in res["via"]:
|
||||
print("ERR:", error["message"])
|
||||
break
|
||||
def print_error(error):
|
||||
print("ERR:", error["hint"])
|
||||
|
||||
def run_cmd(params):
|
||||
try:
|
||||
expr = "(app.srepl.cli/exec {})".format(encode(params))
|
||||
res, failed = send_eval(expr)
|
||||
if failed:
|
||||
print_error(res)
|
||||
res, err = send(params)
|
||||
if err:
|
||||
print_error(err)
|
||||
sys.exit(-1)
|
||||
|
||||
return res
|
||||
@@ -96,7 +91,7 @@ def update_profile(email, fullname, password, is_active):
|
||||
"email": email,
|
||||
"fullname": fullname,
|
||||
"password": password,
|
||||
"is_active": is_active
|
||||
"isActive": is_active
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,7 +133,7 @@ def derive_password(password):
|
||||
params = {
|
||||
"cmd": "derive-password",
|
||||
"params": {
|
||||
"password": password,
|
||||
"password": password
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,9 @@ export PENPOT_FLAGS="\
|
||||
enable-access-tokens \
|
||||
enable-tiered-file-data-storage \
|
||||
enable-file-validation \
|
||||
enable-file-schema-validation";
|
||||
enable-file-schema-validation \
|
||||
enable-subscriptions \
|
||||
enable-subscriptions-old";
|
||||
|
||||
# Default deletion delay for devenv
|
||||
export PENPOT_DELETION_DELAY="24h"
|
||||
@@ -70,15 +72,18 @@ export PENPOT_OBJECTS_STORAGE_S3_ENDPOINT=http://minio:9000
|
||||
export PENPOT_OBJECTS_STORAGE_S3_BUCKET=penpot
|
||||
export PENPOT_OBJECTS_STORAGE_FS_DIRECTORY="assets"
|
||||
|
||||
export JAVA_OPTS="--enable-preview \
|
||||
export JAVA_OPTS="\
|
||||
-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
|
||||
-Djdk.attach.allowAttachSelf \
|
||||
-Dlog4j2.configurationFile=log4j2-devenv-repl.xml \
|
||||
-Djdk.tracePinnedThreads=full \
|
||||
-XX:+EnableDynamicAgentLoading \
|
||||
-XX:-OmitStackTraceInFastThrow \
|
||||
-XX:-OmitStackTraceInFastThrow \
|
||||
-XX:+UnlockDiagnosticVMOptions \
|
||||
-XX:+DebugNonSafepoints";
|
||||
-XX:+DebugNonSafepoints \
|
||||
--sun-misc-unsafe-memory-access=allow \
|
||||
--enable-preview \
|
||||
--enable-native-access=ALL-UNNAMED";
|
||||
|
||||
export OPTIONS="-A:jmx-remote -A:dev"
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ if [ -f ./environ ]; then
|
||||
source ./environ
|
||||
fi
|
||||
|
||||
export JVM_OPTS="-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager -Dlog4j2.configurationFile=log4j2.xml -XX:-OmitStackTraceInFastThrow --enable-preview $JVM_OPTS"
|
||||
export JVM_OPTS="-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager -Dlog4j2.configurationFile=log4j2.xml -XX:-OmitStackTraceInFastThrow --enable-native-access=ALL-UNNAMED --enable-preview $JVM_OPTS"
|
||||
|
||||
ENTRYPOINT=${1:-app.main};
|
||||
|
||||
|
||||
@@ -23,18 +23,9 @@ export PENPOT_FLAGS="\
|
||||
enable-access-tokens \
|
||||
enable-tiered-file-data-storage \
|
||||
enable-file-validation \
|
||||
enable-file-schema-validation";
|
||||
|
||||
export OPTIONS="
|
||||
-A:jmx-remote -A:dev \
|
||||
-J-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
|
||||
-J-Djdk.attach.allowAttachSelf \
|
||||
-J-Dpolyglot.engine.WarnInterpreterOnly=false \
|
||||
-J-Dlog4j2.configurationFile=log4j2-devenv.xml \
|
||||
-J-XX:+EnableDynamicAgentLoading \
|
||||
-J-XX:-OmitStackTraceInFastThrow \
|
||||
-J-XX:+UnlockDiagnosticVMOptions \
|
||||
-J-XX:+DebugNonSafepoints"
|
||||
enable-file-schema-validation \
|
||||
enable-subscriptions \
|
||||
enable-subscriptions-old ";
|
||||
|
||||
# Default deletion delay for devenv
|
||||
export PENPOT_DELETION_DELAY="24h"
|
||||
@@ -65,6 +56,20 @@ export PENPOT_OBJECTS_STORAGE_S3_BUCKET=penpot
|
||||
|
||||
entrypoint=${1:-app.main};
|
||||
|
||||
set -ex
|
||||
export JAVA_OPTS="\
|
||||
-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
|
||||
-Djdk.attach.allowAttachSelf \
|
||||
-Dlog4j2.configurationFile=log4j2-devenv.xml \
|
||||
-Djdk.tracePinnedThreads=full \
|
||||
-XX:+EnableDynamicAgentLoading \
|
||||
-XX:-OmitStackTraceInFastThrow \
|
||||
-XX:+UnlockDiagnosticVMOptions \
|
||||
-XX:+DebugNonSafepoints \
|
||||
--sun-misc-unsafe-memory-access=allow \
|
||||
--enable-preview \
|
||||
--enable-native-access=ALL-UNNAMED";
|
||||
|
||||
clojure $OPTIONS -A:dev -M -m $entrypoint;
|
||||
export OPTIONS="-A:jmx-remote -A:dev"
|
||||
|
||||
set -ex
|
||||
clojure $OPTIONS -M -m $entrypoint;
|
||||
|
||||
107
backend/src/app/binfile/cleaner.clj
Normal file
107
backend/src/app/binfile/cleaner.clj
Normal file
@@ -0,0 +1,107 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.binfile.cleaner
|
||||
"A collection of helpers for perform cleaning of artifacts; mainly
|
||||
for recently imported shapes."
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.uuid :as uuid]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; PRE DECODE
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn clean-shape-pre-decode
|
||||
"Applies a pre-decode phase migration to the shape"
|
||||
[shape]
|
||||
(if (= "bool" (:type shape))
|
||||
(if-let [content (get shape :bool-content)]
|
||||
(-> shape
|
||||
(assoc :content content)
|
||||
(dissoc :bool-content))
|
||||
shape)
|
||||
shape))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; POST DECODE
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn- fix-shape-shadow-color
|
||||
"Some shapes can come with invalid `id` property on shadow colors
|
||||
caused by incorrect uuid parsing bug that should be already fixed;
|
||||
this function removes the invalid id from the data structure."
|
||||
[shape]
|
||||
(let [fix-color
|
||||
(fn [{:keys [id] :as color}]
|
||||
(if (uuid? id)
|
||||
color
|
||||
(if (and (string? id)
|
||||
(re-matches uuid/regex id))
|
||||
(assoc color :id (uuid/uuid id))
|
||||
(dissoc color :id))))
|
||||
|
||||
fix-shadow
|
||||
(fn [shadow]
|
||||
(d/update-when shadow :color fix-color))
|
||||
|
||||
xform
|
||||
(map fix-shadow)]
|
||||
|
||||
(d/update-when shape :shadow
|
||||
(fn [shadows]
|
||||
(into [] xform shadows)))))
|
||||
|
||||
(defn- fix-root-shape
|
||||
"Ensure all root objects are well formed shapes"
|
||||
[shape]
|
||||
(if (= (:id shape) uuid/zero)
|
||||
(-> shape
|
||||
(assoc :parent-id uuid/zero)
|
||||
(assoc :frame-id uuid/zero)
|
||||
;; We explicitly dissoc them and let the shape-setup
|
||||
;; to regenerate it with valid values.
|
||||
(dissoc :selrect)
|
||||
(dissoc :points)
|
||||
(cts/setup-shape))
|
||||
shape))
|
||||
|
||||
(defn- fix-legacy-flex-dir
|
||||
"This operation is only relevant to old data and it is fixed just
|
||||
for convenience."
|
||||
[shape]
|
||||
(d/update-when shape :layout-flex-dir
|
||||
(fn [dir]
|
||||
(case dir
|
||||
:reverse-row :row-reverse
|
||||
:reverse-column :column-reverse
|
||||
dir))))
|
||||
|
||||
(defn clean-shape-post-decode
|
||||
"A shape procesor that expected to be executed after schema decoding
|
||||
process but before validation."
|
||||
[shape]
|
||||
(-> shape
|
||||
(fix-shape-shadow-color)
|
||||
(fix-root-shape)
|
||||
(fix-legacy-flex-dir)))
|
||||
|
||||
(defn- fix-container
|
||||
[container]
|
||||
(-> container
|
||||
;; Remove possible `nil` keys on objects
|
||||
(d/update-when :objects dissoc nil)
|
||||
(d/update-when :objects d/update-vals clean-shape-post-decode)))
|
||||
|
||||
(defn clean-file
|
||||
[file & {:as _opts}]
|
||||
(update file :data
|
||||
(fn [data]
|
||||
(-> data
|
||||
(d/update-when :pages-index d/update-vals fix-container)
|
||||
(d/update-when :components d/update-vals fix-container)
|
||||
(d/without-nils)))))
|
||||
@@ -431,15 +431,21 @@
|
||||
(update :components relink-shapes)
|
||||
(update :media relink-media)
|
||||
(update :colors relink-colors)
|
||||
(d/without-nils))))))
|
||||
(d/without-nils))))
|
||||
|
||||
;; NOTE: this is necessary because when we just creating a new
|
||||
;; file from imported artifact or cloned file there are no
|
||||
;; migrations registered on the database, so we need to persist
|
||||
;; all of them, not only the applied
|
||||
(vary-meta dissoc ::fmg/migrated)))
|
||||
|
||||
(defn encode-file
|
||||
[{:keys [::db/conn] :as cfg} {:keys [id] :as file}]
|
||||
(let [file (if (contains? (:features file) "fdata/objects-map")
|
||||
[{:keys [::db/conn] :as cfg} {:keys [id features] :as file}]
|
||||
(let [file (if (contains? features "fdata/objects-map")
|
||||
(feat.fdata/enable-objects-map file)
|
||||
file)
|
||||
|
||||
file (if (contains? (:features file) "fdata/pointer-map")
|
||||
file (if (contains? features "fdata/pointer-map")
|
||||
(binding [pmap/*tracked* (pmap/create-tracked)]
|
||||
(let [file (feat.fdata/enable-pointer-map file)]
|
||||
(feat.fdata/persist-pointers! cfg id)
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
[app.binfile.common :as bfc]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.features :as cfeat]
|
||||
[app.features.components-v2 :as feat.compv2]
|
||||
[clojure.set :as set]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
@@ -28,13 +27,11 @@
|
||||
|
||||
(defn apply-pending-migrations!
|
||||
"Apply alredy registered pending migrations to files"
|
||||
[cfg]
|
||||
(doseq [[feature file-id] (-> bfc/*state* deref :pending-to-migrate)]
|
||||
[_cfg]
|
||||
(doseq [[feature _file-id] (-> bfc/*state* deref :pending-to-migrate)]
|
||||
(case feature
|
||||
"components/v2"
|
||||
(feat.compv2/migrate-file! cfg file-id
|
||||
:validate? (::validate cfg true)
|
||||
:skip-on-graphic-error? true)
|
||||
nil
|
||||
|
||||
"fdata/shape-data-type"
|
||||
nil
|
||||
|
||||
@@ -8,14 +8,17 @@
|
||||
"A ZIP based binary file exportation"
|
||||
(:refer-clojure :exclude [read])
|
||||
(:require
|
||||
[app.binfile.cleaner :as bfl]
|
||||
[app.binfile.common :as bfc]
|
||||
[app.binfile.migrations :as bfm]
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.features :as cfeat]
|
||||
[app.common.files.migrations :as-alias fmg]
|
||||
[app.common.json :as json]
|
||||
[app.common.logging :as l]
|
||||
[app.common.media :as cmedia]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.thumbnails :as cth]
|
||||
[app.common.types.color :as ctcl]
|
||||
@@ -71,7 +74,7 @@
|
||||
[:size ::sm/int]
|
||||
[:content-type :string]
|
||||
[:bucket [::sm/one-of {:format :string} sto/valid-buckets]]
|
||||
[:hash :string]])
|
||||
[:hash {:optional true} :string]])
|
||||
|
||||
(def ^:private schema:file-thumbnail
|
||||
[:map {:title "FileThumbnail"}
|
||||
@@ -86,13 +89,19 @@
|
||||
ctf/schema:file
|
||||
[:map [:options {:optional true} ctf/schema:options]]])
|
||||
|
||||
;; --- HELPERS
|
||||
|
||||
(defn- default-now
|
||||
[o]
|
||||
(or o (dt/now)))
|
||||
|
||||
;; --- ENCODERS
|
||||
|
||||
(def encode-file
|
||||
(sm/encoder schema:file sm/json-transformer))
|
||||
|
||||
(def encode-page
|
||||
(sm/encoder ::ctp/page sm/json-transformer))
|
||||
(sm/encoder ctp/schema:page sm/json-transformer))
|
||||
|
||||
(def encode-shape
|
||||
(sm/encoder ::cts/shape sm/json-transformer))
|
||||
@@ -127,7 +136,7 @@
|
||||
(sm/decoder schema:manifest sm/json-transformer))
|
||||
|
||||
(def decode-media
|
||||
(sm/decoder ::ctf/media sm/json-transformer))
|
||||
(sm/decoder ctf/schema:media sm/json-transformer))
|
||||
|
||||
(def decode-component
|
||||
(sm/decoder ::ctc/component sm/json-transformer))
|
||||
@@ -227,27 +236,13 @@
|
||||
:always
|
||||
(bfc/clean-file-features))))))
|
||||
|
||||
(defn- resolve-extension
|
||||
[mtype]
|
||||
(case mtype
|
||||
"image/png" ".png"
|
||||
"image/jpeg" ".jpg"
|
||||
"image/gif" ".gif"
|
||||
"image/svg+xml" ".svg"
|
||||
"image/webp" ".webp"
|
||||
"font/woff" ".woff"
|
||||
"font/woff2" ".woff2"
|
||||
"font/ttf" ".ttf"
|
||||
"font/otf" ".otf"
|
||||
"application/octet-stream" ".bin"))
|
||||
|
||||
(defn- export-storage-objects
|
||||
[{:keys [::output] :as cfg}]
|
||||
(let [storage (sto/resolve cfg)]
|
||||
(doseq [id (-> bfc/*state* deref :storage-objects not-empty)]
|
||||
(let [sobject (sto/get-object storage id)
|
||||
smeta (meta sobject)
|
||||
ext (resolve-extension (:content-type smeta))
|
||||
ext (cmedia/mtype->extension (:content-type smeta))
|
||||
path (str "objects/" id ".json")
|
||||
params (-> (meta sobject)
|
||||
(assoc :id (:id sobject))
|
||||
@@ -572,7 +567,13 @@
|
||||
(let [object (->> (read-entry input entry)
|
||||
(decode-media)
|
||||
(validate-media))
|
||||
object (assoc object :file-id file-id)]
|
||||
object (-> object
|
||||
(assoc :file-id file-id)
|
||||
(update :created-at default-now)
|
||||
;; FIXME: this is set default to true for
|
||||
;; setting a value, this prop is no longer
|
||||
;; relevant;
|
||||
(assoc :is-local true))]
|
||||
(if (= id (:id object))
|
||||
(conj result object)
|
||||
result)))
|
||||
@@ -594,16 +595,35 @@
|
||||
|
||||
(defn- read-file-components
|
||||
[{:keys [::bfc/input ::file-id ::entries]}]
|
||||
(->> (keep (match-component-entry-fn file-id) entries)
|
||||
(reduce (fn [result {:keys [id entry]}]
|
||||
(let [object (->> (read-entry input entry)
|
||||
(decode-component)
|
||||
(validate-component))]
|
||||
(if (= id (:id object))
|
||||
(assoc result id object)
|
||||
result)))
|
||||
{})
|
||||
(not-empty)))
|
||||
(let [clean-component-post-decode
|
||||
(fn [component]
|
||||
(d/update-when component :objects
|
||||
(fn [objects]
|
||||
(reduce-kv (fn [objects id shape]
|
||||
(assoc objects id (bfl/clean-shape-post-decode shape)))
|
||||
objects
|
||||
objects))))
|
||||
clean-component-pre-decode
|
||||
(fn [component]
|
||||
(d/update-when component :objects
|
||||
(fn [objects]
|
||||
(reduce-kv (fn [objects id shape]
|
||||
(assoc objects id (bfl/clean-shape-pre-decode shape)))
|
||||
objects
|
||||
objects))))]
|
||||
|
||||
(->> (keep (match-component-entry-fn file-id) entries)
|
||||
(reduce (fn [result {:keys [id entry]}]
|
||||
(let [object (->> (read-entry input entry)
|
||||
(clean-component-pre-decode)
|
||||
(decode-component)
|
||||
(clean-component-post-decode)
|
||||
(validate-component))]
|
||||
(if (= id (:id object))
|
||||
(assoc result id object)
|
||||
result)))
|
||||
{})
|
||||
(not-empty))))
|
||||
|
||||
(defn- read-file-typographies
|
||||
[{:keys [::bfc/input ::file-id ::entries]}]
|
||||
@@ -630,7 +650,9 @@
|
||||
(->> (keep (match-shape-entry-fn file-id page-id) entries)
|
||||
(reduce (fn [result {:keys [id entry]}]
|
||||
(let [object (->> (read-entry input entry)
|
||||
(bfl/clean-shape-pre-decode)
|
||||
(decode-shape)
|
||||
(bfl/clean-shape-post-decode)
|
||||
(validate-shape))]
|
||||
(if (= id (:id object))
|
||||
(assoc result id object)
|
||||
@@ -735,7 +757,6 @@
|
||||
(dissoc :options)
|
||||
(bfc/process-file))]
|
||||
|
||||
|
||||
(bfm/register-pending-migrations! cfg file)
|
||||
(bfc/save-file! cfg file ::db/return-keys false)
|
||||
|
||||
@@ -778,7 +799,7 @@
|
||||
:expected-id (str id)
|
||||
:found-id (str (:id object))))
|
||||
|
||||
(let [ext (resolve-extension (:content-type object))
|
||||
(let [ext (cmedia/mtype->extension (:content-type object))
|
||||
path (str "objects/" id ext)
|
||||
content (->> path
|
||||
(get-zip-entry input)
|
||||
@@ -792,13 +813,14 @@
|
||||
:expected-size (:size object)
|
||||
:found-size (sto/get-size content)))
|
||||
|
||||
(when (not= (:hash object) (sto/get-hash content))
|
||||
(ex/raise :type :validation
|
||||
:code :inconsistent-penpot-file
|
||||
:hint "found corrupted storage object: hash does not match"
|
||||
:path path
|
||||
:expected-hash (:hash object)
|
||||
:found-hash (sto/get-hash content)))
|
||||
(when-let [hash (get object :hash)]
|
||||
(when (not= hash (sto/get-hash content))
|
||||
(ex/raise :type :validation
|
||||
:code :inconsistent-penpot-file
|
||||
:hint "found corrupted storage object: hash does not match"
|
||||
:path path
|
||||
:expected-hash (:hash object)
|
||||
:found-hash (sto/get-hash content))))
|
||||
|
||||
(let [params (-> object
|
||||
(dissoc :id :size)
|
||||
|
||||
@@ -42,6 +42,8 @@
|
||||
org.postgresql.util.PGInterval
|
||||
org.postgresql.util.PGobject))
|
||||
|
||||
(def ^:dynamic *conn* nil)
|
||||
|
||||
(declare open)
|
||||
(declare create-pool)
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,10 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.files.migrations :as fmg]
|
||||
[app.common.logging :as l]
|
||||
[app.common.types.path :as path]
|
||||
[app.db :as db]
|
||||
[app.db.sql :as-alias sql]
|
||||
[app.storage :as sto]
|
||||
@@ -30,7 +33,7 @@
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn enable-objects-map
|
||||
[file]
|
||||
[file & _opts]
|
||||
(let [update-page
|
||||
(fn [page]
|
||||
(if (and (pmap/pointer-map? page)
|
||||
@@ -136,10 +139,56 @@
|
||||
|
||||
(defn enable-pointer-map
|
||||
"Enable the fdata/pointer-map feature on the file."
|
||||
[file]
|
||||
[file & _opts]
|
||||
(-> file
|
||||
(update :data (fn [fdata]
|
||||
(-> fdata
|
||||
(update :pages-index d/update-vals pmap/wrap)
|
||||
(d/update-when :components pmap/wrap))))
|
||||
(update :features conj "fdata/pointer-map")))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; PATH-DATA
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn enable-path-data
|
||||
"Enable the fdata/path-data feature on the file."
|
||||
[file & _opts]
|
||||
(letfn [(update-object [object]
|
||||
(if (or (cfh/path-shape? object)
|
||||
(cfh/bool-shape? object))
|
||||
(update object :content path/content)
|
||||
object))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
|
||||
(-> file
|
||||
(update :data (fn [data]
|
||||
(-> data
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
(update :features conj "fdata/path-data"))))
|
||||
|
||||
(defn disable-path-data
|
||||
[file & _opts]
|
||||
(letfn [(update-object [object]
|
||||
(if (or (cfh/path-shape? object)
|
||||
(cfh/bool-shape? object))
|
||||
(update object :content vec)
|
||||
object))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
|
||||
(when-let [conn db/*conn*]
|
||||
(db/delete! conn :file-migration {:file-id (:id file)
|
||||
:name "0003-convert-path-content"}))
|
||||
(-> file
|
||||
(update :data (fn [data]
|
||||
(-> data
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
(update :features disj "fdata/path-data")
|
||||
(update :migrations disj "0003-convert-path-content")
|
||||
(vary-meta update ::fmg/migrated disj "0003-convert-path-content"))))
|
||||
|
||||
@@ -155,9 +155,9 @@
|
||||
[["" {:middleware [[mw/server-timing]
|
||||
[mw/params]
|
||||
[mw/format-response]
|
||||
[mw/parse-request]
|
||||
[session/soft-auth cfg]
|
||||
[actoken/soft-auth cfg]
|
||||
[mw/parse-request]
|
||||
[mw/errors errors/handle]
|
||||
[mw/restrict-methods]]}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
(:refer-clojure :exclude [tap])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.transit :as t]
|
||||
[app.http.errors :as errors]
|
||||
@@ -54,18 +53,20 @@
|
||||
::yres/status 200
|
||||
::yres/body (yres/stream-body
|
||||
(fn [_ output]
|
||||
(binding [events/*channel* (sp/chan :buf buf :xf (keep encode))]
|
||||
(let [listener (events/start-listener
|
||||
(partial write! output)
|
||||
(partial pu/close! output))]
|
||||
(try
|
||||
(let [channel (sp/chan :buf buf :xf (keep encode))
|
||||
listener (events/start-listener
|
||||
channel
|
||||
(partial write! output)
|
||||
(partial pu/close! output))]
|
||||
(try
|
||||
(binding [events/*channel* channel]
|
||||
(let [result (handler)]
|
||||
(events/tap :end result))
|
||||
(catch Throwable cause
|
||||
(events/tap :error (errors/handle' cause request))
|
||||
(when-not (ex/instance? java.io.EOFException cause)
|
||||
(binding [l/*context* (errors/request->context request)]
|
||||
(l/err :hint "unexpected error on processing sse response" :cause cause))))
|
||||
(finally
|
||||
(sp/close! events/*channel*)
|
||||
(px/await! listener)))))))}))
|
||||
(events/tap :end result)))
|
||||
|
||||
(catch Throwable cause
|
||||
(let [result (errors/handle' cause request)]
|
||||
(events/tap channel :error result)))
|
||||
|
||||
(finally
|
||||
(sp/close! channel)
|
||||
(px/await! listener))))))}))
|
||||
|
||||
@@ -273,7 +273,7 @@
|
||||
|
||||
(defn- http-handler
|
||||
[cfg {:keys [params ::session/profile-id] :as request}]
|
||||
(let [session-id (some-> params :session-id sm/parse-uuid)]
|
||||
(let [session-id (some-> params :session-id uuid/parse*)]
|
||||
(when-not (uuid? session-id)
|
||||
(ex/raise :type :validation
|
||||
:code :missing-session-id
|
||||
|
||||
@@ -108,6 +108,7 @@
|
||||
[::ip-addr {:optional true} ::sm/text]
|
||||
[::props {:optional true} [:map-of :keyword :any]]
|
||||
[::context {:optional true} [:map-of :keyword :any]]
|
||||
[::tracked-at {:optional true} ::sm/inst]
|
||||
[::webhooks/event? {:optional true} ::sm/boolean]
|
||||
[::webhooks/batch-timeout {:optional true} ::dt/duration]
|
||||
[::webhooks/batch-key {:optional true}
|
||||
@@ -118,12 +119,12 @@
|
||||
|
||||
(defn prepare-event
|
||||
[cfg mdata params result]
|
||||
(let [resultm (meta result)
|
||||
request (-> params meta ::http/request)
|
||||
profile-id (or (::profile-id resultm)
|
||||
(:profile-id result)
|
||||
(::rpc/profile-id params)
|
||||
uuid/zero)
|
||||
(let [resultm (meta result)
|
||||
request (-> params meta ::http/request)
|
||||
profile-id (or (::profile-id resultm)
|
||||
(:profile-id result)
|
||||
(::rpc/profile-id params)
|
||||
uuid/zero)
|
||||
|
||||
session-id (get params ::rpc/external-session-id)
|
||||
event-origin (get params ::rpc/external-event-origin)
|
||||
@@ -135,14 +136,14 @@
|
||||
|
||||
(clean-props))
|
||||
|
||||
token-id (::actoken/id request)
|
||||
context (-> (::context resultm)
|
||||
(assoc :external-session-id session-id)
|
||||
(assoc :external-event-origin event-origin)
|
||||
(assoc :access-token-id (some-> token-id str))
|
||||
(d/without-nils))
|
||||
token-id (::actoken/id request)
|
||||
context (-> (::context resultm)
|
||||
(assoc :external-session-id session-id)
|
||||
(assoc :external-event-origin event-origin)
|
||||
(assoc :access-token-id (some-> token-id str))
|
||||
(d/without-nils))
|
||||
|
||||
ip-addr (inet/parse-request request)]
|
||||
ip-addr (inet/parse-request request)]
|
||||
|
||||
{::type (or (::type resultm)
|
||||
(::rpc/type cfg))
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.http.client :as http]
|
||||
[app.loggers.audit :as audit]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
[clojure.data.json :as json]
|
||||
@@ -67,18 +68,27 @@
|
||||
(defmethod ig/init-key ::process-event-handler
|
||||
[_ cfg]
|
||||
(fn [{:keys [props] :as task}]
|
||||
(l/dbg :hint "process webhook event" :name (:name props))
|
||||
|
||||
(when-let [items (lookup-webhooks cfg props)]
|
||||
(l/trc :hint "webhooks found for event" :total (count items))
|
||||
(db/tx-run! cfg (fn [cfg]
|
||||
(doseq [item items]
|
||||
(wrk/submit! (-> cfg
|
||||
(assoc ::wrk/task :run-webhook)
|
||||
(assoc ::wrk/queue :webhooks)
|
||||
(assoc ::wrk/max-retries 3)
|
||||
(assoc ::wrk/params {:event props
|
||||
:config item})))))))))
|
||||
(let [items (lookup-webhooks cfg props)
|
||||
event {::audit/profile-id (:profile-id props)
|
||||
::audit/name "webhook"
|
||||
::audit/type "trigger"
|
||||
::audit/props {:name (get props :name)
|
||||
:event-id (get props :id)
|
||||
:total-affected (count items)}}]
|
||||
|
||||
(audit/insert! cfg event)
|
||||
|
||||
(when items
|
||||
(l/trc :hint "webhooks found for event" :total (count items))
|
||||
(db/tx-run! cfg (fn [cfg]
|
||||
(doseq [item items]
|
||||
(wrk/submit! (-> cfg
|
||||
(assoc ::wrk/task :run-webhook)
|
||||
(assoc ::wrk/queue :webhooks)
|
||||
(assoc ::wrk/max-retries 3)
|
||||
(assoc ::wrk/params {:event props
|
||||
:config item}))))))))))
|
||||
;; --- RUN
|
||||
|
||||
(declare interpret-exception)
|
||||
|
||||
@@ -8,12 +8,11 @@
|
||||
"Media & Font postprocessing."
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.media :as cm]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.schema.openapi :as-alias oapi]
|
||||
[app.common.spec :as us]
|
||||
[app.common.svg :as csvg]
|
||||
[app.config :as cf]
|
||||
[app.db :as-alias db]
|
||||
[app.storage :as-alias sto]
|
||||
@@ -22,39 +21,38 @@
|
||||
[buddy.core.bytes :as bb]
|
||||
[buddy.core.codecs :as bc]
|
||||
[clojure.java.shell :as sh]
|
||||
[clojure.spec.alpha :as s]
|
||||
[clojure.xml :as xml]
|
||||
[cuerdas.core :as str]
|
||||
[datoteka.fs :as fs]
|
||||
[datoteka.io :as io])
|
||||
(:import
|
||||
clojure.lang.XMLHandler
|
||||
java.io.InputStream
|
||||
javax.xml.XMLConstants
|
||||
javax.xml.parsers.SAXParserFactory
|
||||
org.apache.commons.io.IOUtils
|
||||
org.im4java.core.ConvertCmd
|
||||
org.im4java.core.IMOperation
|
||||
org.im4java.core.Info))
|
||||
|
||||
(s/def ::path fs/path?)
|
||||
(s/def ::filename string?)
|
||||
(s/def ::size integer?)
|
||||
(s/def ::headers (s/map-of string? string?))
|
||||
(s/def ::mtype string?)
|
||||
(def schema:upload
|
||||
(sm/register!
|
||||
^{::sm/type ::upload}
|
||||
[:map {:title "Upload"}
|
||||
[:filename :string]
|
||||
[:size ::sm/int]
|
||||
[:path ::fs/path]
|
||||
[:mtype {:optional true} :string]
|
||||
[:headers {:optional true}
|
||||
[:map-of :string :string]]]))
|
||||
|
||||
(s/def ::upload
|
||||
(s/keys :req-un [::filename ::size ::path]
|
||||
:opt-un [::mtype ::headers]))
|
||||
(def ^:private schema:input
|
||||
[:map {:title "Input"}
|
||||
[:path ::fs/path]
|
||||
[:mtype {:optional true} ::sm/text]])
|
||||
|
||||
;; A subset of fields from the ::upload spec
|
||||
(s/def ::input
|
||||
(s/keys :req-un [::path]
|
||||
:opt-un [::mtype]))
|
||||
|
||||
(sm/register!
|
||||
^{::sm/type ::upload}
|
||||
[:map {:title "Upload"}
|
||||
[:filename :string]
|
||||
[:size ::sm/int]
|
||||
[:path ::fs/path]
|
||||
[:mtype {:optional true} :string]
|
||||
[:headers {:optional true}
|
||||
[:map-of :string :string]]])
|
||||
(def ^:private check-input
|
||||
(sm/check-fn schema:input))
|
||||
|
||||
(defn validate-media-type!
|
||||
([upload] (validate-media-type! upload cm/valid-image-types))
|
||||
@@ -97,17 +95,44 @@
|
||||
(catch Throwable e
|
||||
(process-error e))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; SVG PARSING
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn- secure-parser-factory
|
||||
[^InputStream input ^XMLHandler handler]
|
||||
(.. (doto (SAXParserFactory/newInstance)
|
||||
(.setFeature XMLConstants/FEATURE_SECURE_PROCESSING true)
|
||||
(.setFeature "http://apache.org/xml/features/disallow-doctype-decl" true))
|
||||
(newSAXParser)
|
||||
(parse input handler)))
|
||||
|
||||
(defn- strip-doctype
|
||||
[data]
|
||||
(cond-> data
|
||||
(str/includes? data "<!DOCTYPE")
|
||||
(str/replace #"<\!DOCTYPE[^>]*>" "")))
|
||||
|
||||
(defn- parse-svg
|
||||
[text]
|
||||
(let [text (strip-doctype text)]
|
||||
(dm/with-open [istream (IOUtils/toInputStream text "UTF-8")]
|
||||
(xml/parse istream secure-parser-factory))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; IMAGE THUMBNAILS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(s/def ::width integer?)
|
||||
(s/def ::height integer?)
|
||||
(s/def ::format #{:jpeg :webp :png})
|
||||
(s/def ::quality #(< 0 % 101))
|
||||
(def ^:private schema:thumbnail-params
|
||||
[:map {:title "ThumbnailParams"}
|
||||
[:input schema:input]
|
||||
[:format [:enum :jpeg :webp :png]]
|
||||
[:quality [:int {:min 1 :max 100}]]
|
||||
[:width :int]
|
||||
[:height :int]])
|
||||
|
||||
(s/def ::thumbnail-params
|
||||
(s/keys :req-un [::input ::format ::width ::height]))
|
||||
(def ^:private check-thumbnail-params
|
||||
(sm/check-fn schema:thumbnail-params))
|
||||
|
||||
;; Related info on how thumbnails generation
|
||||
;; http://www.imagemagick.org/Usage/thumbnails/
|
||||
@@ -129,30 +154,38 @@
|
||||
:data tmp)))
|
||||
|
||||
(defmethod process :generic-thumbnail
|
||||
[{:keys [quality width height] :as params}]
|
||||
(us/assert ::thumbnail-params params)
|
||||
(let [op (doto (IMOperation.)
|
||||
(.addImage)
|
||||
(.autoOrient)
|
||||
(.strip)
|
||||
(.thumbnail ^Integer (int width) ^Integer (int height) ">")
|
||||
(.quality (double quality))
|
||||
(.addImage))]
|
||||
(generic-process (assoc params :operation op))))
|
||||
[params]
|
||||
(let [{:keys [quality width height] :as params}
|
||||
(check-thumbnail-params params)
|
||||
|
||||
operation
|
||||
(doto (IMOperation.)
|
||||
(.addImage)
|
||||
(.autoOrient)
|
||||
(.strip)
|
||||
(.thumbnail ^Integer (int width) ^Integer (int height) ">")
|
||||
(.quality (double quality))
|
||||
(.addImage))]
|
||||
|
||||
(generic-process (assoc params :operation operation))))
|
||||
|
||||
(defmethod process :profile-thumbnail
|
||||
[{:keys [quality width height] :as params}]
|
||||
(us/assert ::thumbnail-params params)
|
||||
(let [op (doto (IMOperation.)
|
||||
(.addImage)
|
||||
(.autoOrient)
|
||||
(.strip)
|
||||
(.thumbnail ^Integer (int width) ^Integer (int height) "^")
|
||||
(.gravity "center")
|
||||
(.extent (int width) (int height))
|
||||
(.quality (double quality))
|
||||
(.addImage))]
|
||||
(generic-process (assoc params :operation op))))
|
||||
[params]
|
||||
(let [{:keys [quality width height] :as params}
|
||||
(check-thumbnail-params params)
|
||||
|
||||
operation
|
||||
(doto (IMOperation.)
|
||||
(.addImage)
|
||||
(.autoOrient)
|
||||
(.strip)
|
||||
(.thumbnail ^Integer (int width) ^Integer (int height) "^")
|
||||
(.gravity "center")
|
||||
(.extent (int width) (int height))
|
||||
(.quality (double quality))
|
||||
(.addImage))]
|
||||
|
||||
(generic-process (assoc params :operation operation))))
|
||||
|
||||
(defn get-basic-info-from-svg
|
||||
[{:keys [tag attrs] :as data}]
|
||||
@@ -184,10 +217,9 @@
|
||||
|
||||
(defmethod process :info
|
||||
[{:keys [input] :as params}]
|
||||
(us/assert ::input input)
|
||||
(let [{:keys [path mtype]} input]
|
||||
(let [{:keys [path mtype] :as input} (check-input input)]
|
||||
(if (= mtype "image/svg+xml")
|
||||
(let [info (some-> path slurp csvg/parse get-basic-info-from-svg)]
|
||||
(let [info (some-> path slurp parse-svg get-basic-info-from-svg)]
|
||||
(when-not info
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-svg-file
|
||||
|
||||
@@ -92,9 +92,9 @@
|
||||
[:string {:max 250}]
|
||||
[::sm/one-of {:format "string"} valid-event-types]]]
|
||||
[:props
|
||||
[:map-of :keyword :any]]
|
||||
[:map-of :keyword ::sm/any]]
|
||||
[:context {:optional true}
|
||||
[:map-of :keyword :any]]])
|
||||
[:map-of :keyword ::sm/any]]])
|
||||
|
||||
(def schema:push-audit-events
|
||||
[:map {:title "push-audit-events"}
|
||||
|
||||
@@ -231,7 +231,7 @@
|
||||
:hint "email has complaint reports")))
|
||||
|
||||
(defn prepare-register
|
||||
[{:keys [::db/pool] :as cfg} {:keys [email] :as params}]
|
||||
[{:keys [::db/pool] :as cfg} {:keys [email accept-newsletter-updates] :as params}]
|
||||
|
||||
(validate-register-attempt! cfg params)
|
||||
|
||||
@@ -243,7 +243,8 @@
|
||||
:backend "penpot"
|
||||
:iss :prepared-register
|
||||
:profile-id (:id profile)
|
||||
:exp (dt/in-future {:days 7})}
|
||||
:exp (dt/in-future {:days 7})
|
||||
:props {:newsletter-updates (or accept-newsletter-updates false)}}
|
||||
|
||||
params (d/without-nils params)
|
||||
token (tokens/generate (::setup/props cfg) params)]
|
||||
|
||||
@@ -115,7 +115,8 @@
|
||||
|
||||
(db/update! pool :project
|
||||
{:modified-at (dt/now)}
|
||||
{:id project-id})
|
||||
{:id project-id}
|
||||
{::db/return-keys false})
|
||||
|
||||
result))
|
||||
|
||||
|
||||
@@ -189,7 +189,7 @@
|
||||
[:is-shared ::sm/boolean]
|
||||
[:project-id ::sm/uuid]
|
||||
[:created-at ::dt/instant]
|
||||
[:data {:optional true} :any]])
|
||||
[:data {:optional true} ::sm/any]])
|
||||
|
||||
(def schema:permissions-mixin
|
||||
[:map {:title "PermissionsMixin"}
|
||||
@@ -208,7 +208,7 @@
|
||||
[:project-id {:optional true} ::sm/uuid]])
|
||||
|
||||
(defn- migrate-file
|
||||
[{:keys [::db/conn] :as cfg} {:keys [id] :as file}]
|
||||
[{:keys [::db/conn] :as cfg} {:keys [id] :as file} {:keys [read-only?]}]
|
||||
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)
|
||||
pmap/*tracked* (pmap/create-tracked)]
|
||||
(let [;; For avoid unnecesary overhead of creating multiple pointers and
|
||||
@@ -219,43 +219,45 @@
|
||||
file (-> file
|
||||
(update :data feat.fdata/process-pointers deref)
|
||||
(update :data feat.fdata/process-objects (partial into {}))
|
||||
(fmg/migrate-file))
|
||||
(fmg/migrate-file))]
|
||||
|
||||
;; When file is migrated, we break the rule of no perform
|
||||
;; mutations on get operations and update the file with all
|
||||
;; migrations applied
|
||||
;;
|
||||
;; WARN: he following code will not work on read-only mode,
|
||||
;; it is a known issue; we keep is not implemented until we
|
||||
;; really need this.
|
||||
file (if (contains? (:features file) "fdata/objects-map")
|
||||
(feat.fdata/enable-objects-map file)
|
||||
file)
|
||||
file (if (contains? (:features file) "fdata/pointer-map")
|
||||
(feat.fdata/enable-pointer-map file)
|
||||
file)]
|
||||
(if (or read-only? (db/read-only? conn))
|
||||
file
|
||||
(let [;; When file is migrated, we break the rule of no perform
|
||||
;; mutations on get operations and update the file with all
|
||||
;; migrations applied
|
||||
file (if (contains? (:features file) "fdata/objects-map")
|
||||
(feat.fdata/enable-objects-map file)
|
||||
file)
|
||||
file (if (contains? (:features file) "fdata/pointer-map")
|
||||
(feat.fdata/enable-pointer-map file)
|
||||
file)]
|
||||
|
||||
(db/update! conn :file
|
||||
{:data (blob/encode (:data file))
|
||||
:version (:version file)
|
||||
:features (db/create-array conn "text" (:features file))}
|
||||
{:id id})
|
||||
(db/update! conn :file
|
||||
{:data (blob/encode (:data file))
|
||||
:version (:version file)
|
||||
:features (db/create-array conn "text" (:features file))}
|
||||
{:id id}
|
||||
{::db/return-keys false})
|
||||
|
||||
(when (contains? (:features file) "fdata/pointer-map")
|
||||
(feat.fdata/persist-pointers! cfg id))
|
||||
(when (contains? (:features file) "fdata/pointer-map")
|
||||
(feat.fdata/persist-pointers! cfg id))
|
||||
|
||||
(feat.fmigr/upsert-migrations! conn file)
|
||||
(feat.fmigr/resolve-applied-migrations cfg file))))
|
||||
(feat.fmigr/upsert-migrations! conn file)
|
||||
(feat.fmigr/resolve-applied-migrations cfg file))))))
|
||||
|
||||
(defn get-file
|
||||
[{:keys [::db/conn ::wrk/executor] :as cfg} id
|
||||
& {:keys [project-id
|
||||
migrate?
|
||||
include-deleted?
|
||||
lock-for-update?]
|
||||
lock-for-update?
|
||||
preload-pointers?]
|
||||
:or {include-deleted? false
|
||||
lock-for-update? false
|
||||
migrate? true}}]
|
||||
migrate? true
|
||||
preload-pointers? false}
|
||||
:as options}]
|
||||
|
||||
(assert (db/connection? conn) "expected cfg with valid connection")
|
||||
|
||||
@@ -273,10 +275,16 @@
|
||||
;; because it has heavy and synchronous operations for
|
||||
;; decoding file body that are not very friendly with virtual
|
||||
;; threads.
|
||||
file (px/invoke! executor #(decode-row file))]
|
||||
file (px/invoke! executor #(decode-row file))
|
||||
|
||||
file (if (and migrate? (fmg/need-migration? file))
|
||||
(migrate-file cfg file options)
|
||||
file)]
|
||||
|
||||
(if preload-pointers?
|
||||
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)]
|
||||
(update file :data feat.fdata/process-pointers deref))
|
||||
|
||||
(if (and migrate? (fmg/need-migration? file))
|
||||
(migrate-file cfg file)
|
||||
file)))
|
||||
|
||||
(defn get-minimal-file
|
||||
@@ -292,7 +300,7 @@
|
||||
|
||||
(defn get-file-etag
|
||||
[{:keys [::rpc/profile-id]} {:keys [modified-at revn vern permissions]}]
|
||||
(str profile-id "/" revn "/" vern "/"
|
||||
(str profile-id "/" revn "/" vern "/" (hash fmg/available-migrations) "/"
|
||||
(dt/format-instant modified-at :iso)
|
||||
"/"
|
||||
(uri/map->query-string permissions)))
|
||||
@@ -474,7 +482,7 @@
|
||||
(update page :objects update-vals #(dissoc % :thumbnail)))
|
||||
|
||||
(defn get-page
|
||||
[{:keys [::db/conn] :as cfg} {:keys [profile-id file-id page-id object-id] :as params}]
|
||||
[{:keys [::db/conn] :as cfg} {:keys [profile-id file-id page-id object-id share-id] :as params}]
|
||||
|
||||
(when (and (uuid? object-id)
|
||||
(not (uuid? page-id)))
|
||||
@@ -482,22 +490,30 @@
|
||||
:code :params-validation
|
||||
:hint "page-id is required when object-id is provided"))
|
||||
|
||||
(let [team (teams/get-team conn
|
||||
:profile-id profile-id
|
||||
:file-id file-id)
|
||||
(let [perms (get-permissions conn profile-id file-id share-id)
|
||||
|
||||
file (get-file cfg file-id)
|
||||
file (get-file cfg file-id :read-only? true)
|
||||
|
||||
_ (-> (cfeat/get-team-enabled-features cf/flags team)
|
||||
(cfeat/check-client-features! (:features params))
|
||||
(cfeat/check-file-features! (:features file)))
|
||||
proj (db/get conn :project {:id (:project-id file)})
|
||||
|
||||
page (binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg file-id)]
|
||||
(let [page-id (or page-id (-> file :data :pages first))
|
||||
page (dm/get-in file [:data :pages-index page-id])]
|
||||
(if (pmap/pointer-map? page)
|
||||
(deref page)
|
||||
page)))]
|
||||
team (-> (db/get conn :team {:id (:team-id proj)})
|
||||
(teams/decode-row))
|
||||
|
||||
_ (-> (cfeat/get-team-enabled-features cf/flags team)
|
||||
(cfeat/check-client-features! (:features params))
|
||||
(cfeat/check-file-features! (:features file)))
|
||||
|
||||
page (binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg file-id)]
|
||||
(let [page-id (or page-id (-> file :data :pages first))
|
||||
page (dm/get-in file [:data :pages-index page-id])]
|
||||
(if (pmap/pointer-map? page)
|
||||
(deref page)
|
||||
page)))]
|
||||
|
||||
(when-not perms
|
||||
(ex/raise :type :not-found
|
||||
:code :object-not-found
|
||||
:hint "object not found"))
|
||||
|
||||
(cond-> (prune-thumbnails page)
|
||||
(some? object-id)
|
||||
@@ -733,7 +749,9 @@
|
||||
:project-id project-id
|
||||
:file-id id)
|
||||
|
||||
file (get-file cfg id :project-id project-id)]
|
||||
file (get-file cfg id
|
||||
:project-id project-id
|
||||
:read-only? true)]
|
||||
|
||||
(-> (cfeat/get-team-enabled-features cf/flags team)
|
||||
(cfeat/check-client-features! (:features params))
|
||||
|
||||
@@ -55,8 +55,8 @@
|
||||
:features features
|
||||
:ignore-sync-until ignore-sync-until
|
||||
:modified-at modified-at
|
||||
:deleted-at deleted-at
|
||||
:create-page create-page
|
||||
:deleted-at deleted-at}
|
||||
{:create-page create-page
|
||||
:page-id page-id})
|
||||
file (-> (bfc/insert-file! cfg file)
|
||||
(bfc/decode-row))]
|
||||
@@ -111,18 +111,21 @@
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/project-id project-id})
|
||||
|
||||
;; FIXME: IMPORTANT: this code can have race
|
||||
;; conditions, because we have no locks for updating
|
||||
;; team so, creating two files concurrently can lead
|
||||
;; to lost team features updating
|
||||
;; FIXME: IMPORTANT: this code can have race conditions, because
|
||||
;; we have no locks for updating team so, creating two files
|
||||
;; concurrently can lead to lost team features updating
|
||||
|
||||
;; When newly computed features does not match exactly with
|
||||
;; the features defined on team row, we update it
|
||||
(when (not= features (:features team))
|
||||
(let [features (db/create-array conn "text" features)]
|
||||
(when-let [features (-> features
|
||||
(set/difference (:features team))
|
||||
(set/difference cfeat/no-team-inheritable-features)
|
||||
(not-empty))]
|
||||
(let [features (->> features
|
||||
(set/union (:features team))
|
||||
(db/create-array conn "text"))]
|
||||
(db/update! conn :team
|
||||
{:features features}
|
||||
{:id team-id})))
|
||||
{:id (:id team)}
|
||||
{::db/return-keys false})))
|
||||
|
||||
(-> (create-file cfg params)
|
||||
(vary-meta assoc ::audit/props {:team-id team-id}))))
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.db.sql :as sql]
|
||||
[app.features.components-v2 :as feat.compv2]
|
||||
[app.features.fdata :as fdata]
|
||||
[app.loggers.audit :as audit]
|
||||
[app.rpc :as-alias rpc]
|
||||
@@ -110,7 +109,7 @@
|
||||
;; --- MUTATION COMMAND: persist-temp-file
|
||||
|
||||
(defn persist-temp-file
|
||||
[{:keys [::db/conn] :as cfg} {:keys [id ::rpc/profile-id] :as params}]
|
||||
[{:keys [::db/conn] :as cfg} {:keys [id] :as params}]
|
||||
(let [file (files/get-file cfg id
|
||||
:migrate? false
|
||||
:lock-for-update? true)]
|
||||
@@ -119,7 +118,6 @@
|
||||
(ex/raise :type :validation
|
||||
:code :cant-persist-already-persisted-file))
|
||||
|
||||
|
||||
(let [changes (->> (db/cursor conn
|
||||
(sql/select :file-change {:file-id id}
|
||||
{:order-by [[:revn :asc]]})
|
||||
@@ -147,19 +145,6 @@
|
||||
:revn 1
|
||||
:data (blob/encode (:data file))}
|
||||
{:id id})
|
||||
|
||||
(let [team (teams/get-team conn :profile-id profile-id :project-id (:project-id file))
|
||||
file-features (:features file)
|
||||
team-features (cfeat/get-team-enabled-features cf/flags team)]
|
||||
(when (and (contains? team-features "components/v2")
|
||||
(not (contains? file-features "components/v2")))
|
||||
;; Migrate components v2
|
||||
(feat.compv2/migrate-file! cfg
|
||||
(:id file)
|
||||
:max-procs 2
|
||||
:validate? true
|
||||
:throw-on-validate? true)))
|
||||
|
||||
nil)))
|
||||
|
||||
(def ^:private schema:persist-temp-file
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.features :as cfeat]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.files.migrations :as fmg]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.thumbnails :as thc]
|
||||
@@ -18,7 +17,6 @@
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.db.sql :as-alias sql]
|
||||
[app.features.fdata :as feat.fdata]
|
||||
[app.loggers.audit :as-alias audit]
|
||||
[app.loggers.webhooks :as-alias webhooks]
|
||||
[app.media :as media]
|
||||
@@ -180,8 +178,7 @@
|
||||
(def ^:private
|
||||
schema:get-file-data-for-thumbnail
|
||||
[:map {:title "get-file-data-for-thumbnail"}
|
||||
[:file-id ::sm/uuid]
|
||||
[:features {:optional true} ::cfeat/features]])
|
||||
[:file-id ::sm/uuid]])
|
||||
|
||||
(def ^:private
|
||||
schema:partial-file
|
||||
@@ -201,17 +198,15 @@
|
||||
(db/run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||
(files/check-read-permissions! conn profile-id file-id)
|
||||
|
||||
(let [team (teams/get-team conn
|
||||
:profile-id profile-id
|
||||
:file-id file-id)
|
||||
(let [team (teams/get-team conn
|
||||
:profile-id profile-id
|
||||
:file-id file-id)
|
||||
|
||||
file (binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg file-id)]
|
||||
(-> (files/get-file cfg file-id :migrate? false)
|
||||
(update :data feat.fdata/process-pointers deref)
|
||||
(fmg/migrate-file)))]
|
||||
file (files/get-file cfg file-id
|
||||
:preload-pointers? true
|
||||
:read-only? true)]
|
||||
|
||||
(-> (cfeat/get-team-enabled-features cf/flags team)
|
||||
(cfeat/check-client-features! (:features params))
|
||||
(cfeat/check-file-features! (:features file)))
|
||||
|
||||
{:file-id file-id
|
||||
|
||||
@@ -177,12 +177,19 @@
|
||||
:stored-revn (:revn file)}))
|
||||
|
||||
;; When newly computed features does not match exactly with
|
||||
;; the features defined on team row, we update it.
|
||||
(when (not= features (:features team))
|
||||
(let [features (db/create-array conn "text" features)]
|
||||
;; the features defined on team row, we update it
|
||||
(when-let [features (-> features
|
||||
(set/difference (:features team))
|
||||
(set/difference cfeat/no-team-inheritable-features)
|
||||
(not-empty))]
|
||||
(let [features (->> features
|
||||
(set/union (:features team))
|
||||
(db/create-array conn "text"))]
|
||||
(db/update! conn :team
|
||||
{:features features}
|
||||
{:id (:id team)})))
|
||||
{:id (:id team)}
|
||||
{::db/return-keys false})))
|
||||
|
||||
|
||||
(mtx/run! metrics {:id :update-file-changes :inc (count changes)})
|
||||
|
||||
|
||||
@@ -80,9 +80,9 @@
|
||||
(def ^:private schema:create-font-variant
|
||||
[:map {:title "create-font-variant"}
|
||||
[:team-id ::sm/uuid]
|
||||
[:data [:map-of :string :any]]
|
||||
[:data [:map-of ::sm/text ::sm/any]]
|
||||
[:font-id ::sm/uuid]
|
||||
[:font-family :string]
|
||||
[:font-family ::sm/text]
|
||||
[:font-weight [::sm/one-of {:format "number"} valid-weight]]
|
||||
[:font-style [::sm/one-of {:format "string"} valid-style]]])
|
||||
|
||||
|
||||
@@ -76,9 +76,10 @@
|
||||
(perms/make-check-fn has-read-permissions?))
|
||||
|
||||
(defn decode-row
|
||||
[{:keys [features] :as row}]
|
||||
[{:keys [features subscription] :as row}]
|
||||
(cond-> row
|
||||
(some? features) (assoc :features (db/decode-pgarray features #{}))))
|
||||
(some? features) (assoc :features (db/decode-pgarray features #{}))
|
||||
(some? subscription) (assoc :subscription (db/decode-transit-pgobject subscription))))
|
||||
|
||||
;; FIXME: move
|
||||
|
||||
@@ -113,29 +114,41 @@
|
||||
|
||||
;; --- Query: Teams
|
||||
|
||||
(declare get-teams)
|
||||
|
||||
(def ^:private schema:get-teams
|
||||
[:map {:title "get-teams"}])
|
||||
|
||||
(sv/defmethod ::get-teams
|
||||
{::doc/added "1.17"
|
||||
::sm/params schema:get-teams}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(get-teams conn profile-id)))
|
||||
|
||||
(def sql:get-teams-with-permissions
|
||||
"select t.*,
|
||||
"SELECT t.*,
|
||||
tp.is_owner,
|
||||
tp.is_admin,
|
||||
tp.can_edit,
|
||||
(t.id = ?) as is_default
|
||||
from team_profile_rel as tp
|
||||
join team as t on (t.id = tp.team_id)
|
||||
where t.deleted_at is null
|
||||
and tp.profile_id = ?
|
||||
order by tp.created_at asc")
|
||||
(t.id = ?) AS is_default
|
||||
FROM team_profile_rel AS tp
|
||||
JOIN team AS t ON (t.id = tp.team_id)
|
||||
WHERE t.deleted_at IS null
|
||||
AND tp.profile_id = ?
|
||||
ORDER BY tp.created_at ASC")
|
||||
|
||||
(def sql:get-teams-with-permissions-and-subscription
|
||||
"SELECT t.*,
|
||||
tp.is_owner,
|
||||
tp.is_admin,
|
||||
tp.can_edit,
|
||||
(t.id = ?) AS is_default,
|
||||
|
||||
jsonb_build_object(
|
||||
'~:type', COALESCE(p.props->'~:subscription'->>'~:type', 'professional'),
|
||||
'~:status', CASE COALESCE(p.props->'~:subscription'->>'~:type', 'professional')
|
||||
WHEN 'professional' THEN 'active'
|
||||
ELSE COALESCE(p.props->'~:subscription'->>'~:status', 'incomplete')
|
||||
END
|
||||
) AS subscription
|
||||
FROM team_profile_rel AS tp
|
||||
JOIN team AS t ON (t.id = tp.team_id)
|
||||
JOIN team_profile_rel AS tpr
|
||||
ON (tpr.team_id = t.id AND tpr.is_owner IS true)
|
||||
JOIN profile AS p
|
||||
ON (tpr.profile_id = p.id)
|
||||
WHERE t.deleted_at IS null
|
||||
AND tp.profile_id = ?
|
||||
ORDER BY tp.created_at ASC;")
|
||||
|
||||
(defn process-permissions
|
||||
[team]
|
||||
@@ -150,13 +163,52 @@
|
||||
(dissoc :is-owner :is-admin :can-edit)
|
||||
(assoc :permissions permissions))))
|
||||
|
||||
(def ^:private
|
||||
xform:process-teams
|
||||
(comp
|
||||
(map decode-row)
|
||||
(map process-permissions)))
|
||||
|
||||
(defn get-teams
|
||||
[conn profile-id]
|
||||
(let [profile (profile/get-profile conn profile-id)]
|
||||
(->> (db/exec! conn [sql:get-teams-with-permissions (:default-team-id profile) profile-id])
|
||||
(map decode-row)
|
||||
(map process-permissions)
|
||||
(vec))))
|
||||
(let [profile (profile/get-profile conn profile-id)
|
||||
sql (if (contains? cf/flags :subscriptions)
|
||||
sql:get-teams-with-permissions-and-subscription
|
||||
sql:get-teams-with-permissions)]
|
||||
|
||||
(->> (db/exec! conn [sql (:default-team-id profile) profile-id])
|
||||
(into [] xform:process-teams))))
|
||||
|
||||
(def ^:private schema:get-teams
|
||||
[:map {:title "get-teams"}])
|
||||
|
||||
(sv/defmethod ::get-teams
|
||||
{::doc/added "1.17"
|
||||
::sm/params schema:get-teams}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(get-teams conn profile-id)))
|
||||
|
||||
(def ^:private sql:get-owned-teams
|
||||
"SELECT t.id, t.name,
|
||||
(SELECT count(*) FROM team_profile_rel WHERE team_id=t.id) AS total_members
|
||||
FROM team AS t
|
||||
JOIN team_profile_rel AS tpr ON (tpr.team_id = t.id)
|
||||
WHERE t.is_default IS false
|
||||
AND tpr.is_owner IS true
|
||||
AND tpr.profile_id = ?
|
||||
AND t.deleted_at IS NULL")
|
||||
|
||||
(defn- get-owned-teams
|
||||
[cfg profile-id]
|
||||
(->> (db/exec! cfg [sql:get-owned-teams profile-id])
|
||||
(into [] (map decode-row))))
|
||||
|
||||
(sv/defmethod ::get-owned-teams
|
||||
{::doc/added "2.8.0"
|
||||
::sm/params schema:get-teams}
|
||||
[cfg {:keys [::rpc/profile-id]}]
|
||||
(get-owned-teams cfg profile-id))
|
||||
|
||||
;; --- Query: Team (by ID)
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.json :as json]
|
||||
[app.common.pprint :as pp]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.schema.desc-js-like :as smdj]
|
||||
@@ -19,7 +20,6 @@
|
||||
[app.http.sse :as-alias sse]
|
||||
[app.loggers.webhooks :as-alias webhooks]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.util.json :as json]
|
||||
[app.util.services :as sv]
|
||||
[app.util.template :as tmpl]
|
||||
[clojure.java.io :as io]
|
||||
@@ -86,7 +86,7 @@
|
||||
(fn [request]
|
||||
(let [params (:query-params request)
|
||||
pstyle (:type params "js")
|
||||
context (assoc context :param-style pstyle)]
|
||||
context (assoc @context :param-style pstyle)]
|
||||
|
||||
{::yres/status 200
|
||||
::yres/body (-> (io/resource "app/templates/api-doc.tmpl")
|
||||
@@ -178,8 +178,7 @@
|
||||
(fn [_]
|
||||
{::yres/status 200
|
||||
::yres/headers {"content-type" "application/json; charset=utf-8"}
|
||||
::yres/body (json/encode context)})
|
||||
|
||||
::yres/body (json/encode @context)})
|
||||
(fn [_]
|
||||
{::yres/status 404})))
|
||||
|
||||
@@ -209,7 +208,7 @@
|
||||
|
||||
(defmethod ig/init-key ::routes
|
||||
[_ {:keys [::rpc/methods] :as cfg}]
|
||||
[(let [context (prepare-doc-context methods)]
|
||||
[(let [context (delay (prepare-doc-context methods))]
|
||||
[["/_doc"
|
||||
{:handler (doc-handler context)
|
||||
:allowed-methods #{:get}}]
|
||||
@@ -217,7 +216,7 @@
|
||||
{:handler (doc-handler context)
|
||||
:allowed-methods #{:get}}]])
|
||||
|
||||
(let [context (prepare-openapi-context methods)]
|
||||
(let [context (delay (prepare-openapi-context methods))]
|
||||
[["/openapi"
|
||||
{:handler (openapi-handler)
|
||||
:allowed-methods #{:get}}]
|
||||
|
||||
@@ -6,13 +6,17 @@
|
||||
|
||||
(ns app.srepl
|
||||
"Server Repl."
|
||||
(:refer-clojure :exclude [read-line])
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.json :as json]
|
||||
[app.common.logging :as l]
|
||||
[app.config :as cf]
|
||||
[app.srepl.cli]
|
||||
[app.srepl.cli :as cli]
|
||||
[app.srepl.main]
|
||||
[app.util.json :as json]
|
||||
[app.util.locks :as locks]
|
||||
[app.util.time :as dt]
|
||||
[clojure.core :as c]
|
||||
[clojure.core.server :as ccs]
|
||||
[clojure.main :as cm]
|
||||
[integrant.core :as ig]))
|
||||
@@ -28,17 +32,80 @@
|
||||
:init repl-init
|
||||
:read ccs/repl-read))
|
||||
|
||||
(defn- ex->data
|
||||
[cause phase]
|
||||
(let [data (ex-data cause)
|
||||
explain (ex/explain data)]
|
||||
(cond-> {:phase phase
|
||||
:code (get data :code :unknown)
|
||||
:type (get data :type :unknown)
|
||||
:hint (or (get data :hint) (ex-message cause))}
|
||||
(some? explain)
|
||||
(assoc :explain explain))))
|
||||
|
||||
(defn read-line
|
||||
[]
|
||||
(if-let [line (c/read-line)]
|
||||
(try
|
||||
(l/dbg :hint "decode" :data line)
|
||||
(json/decode line :key-fn json/read-kebab-key)
|
||||
(catch Throwable _cause
|
||||
(l/warn :hint "unable to decode data" :data line)
|
||||
nil))
|
||||
::eof))
|
||||
|
||||
(defn json-repl
|
||||
[]
|
||||
(let [out *out*
|
||||
lock (locks/create)]
|
||||
(ccs/prepl *in*
|
||||
(fn [m]
|
||||
(binding [*out* out,
|
||||
*flush-on-newline* true,
|
||||
*print-readably* true]
|
||||
(locks/locking lock
|
||||
(println (json/encode-str m))))))))
|
||||
(let [lock (locks/create)
|
||||
out *out*
|
||||
|
||||
out-fn
|
||||
(fn [m]
|
||||
(locks/locking lock
|
||||
(binding [*out* out]
|
||||
(l/warn :hint "write" :data m)
|
||||
(println (json/encode m :key-fn json/write-camel-key)))))
|
||||
|
||||
tapfn
|
||||
(fn [val]
|
||||
(out-fn {:tag :tap :val val}))]
|
||||
|
||||
(binding [*out* (PrintWriter-on #(out-fn {:tag :out :val %1}) nil true)
|
||||
*err* (PrintWriter-on #(out-fn {:tag :err :val %1}) nil true)]
|
||||
(try
|
||||
(add-tap tapfn)
|
||||
(loop []
|
||||
(when (try
|
||||
(let [data (read-line)
|
||||
tpoint (dt/tpoint)]
|
||||
|
||||
(l/dbg :hint "received" :data (if (= data ::eof) "EOF" data))
|
||||
|
||||
(try
|
||||
(when-not (= data ::eof)
|
||||
(when-not (nil? data)
|
||||
(let [result (cli/exec data)
|
||||
elapsed (tpoint)]
|
||||
(l/warn :hint "result" :data result)
|
||||
(out-fn {:tag :ret
|
||||
:val (if (instance? Throwable result)
|
||||
(Throwable->map result)
|
||||
result)
|
||||
:elapsed (inst-ms elapsed)})))
|
||||
true)
|
||||
(catch Throwable cause
|
||||
(let [elapsed (tpoint)]
|
||||
(out-fn {:tag :ret
|
||||
:err (ex->data cause :eval)
|
||||
:elapsed (inst-ms elapsed)})
|
||||
true))))
|
||||
(catch Throwable cause
|
||||
(out-fn {:tag :ret
|
||||
:err (ex->data cause :read)})
|
||||
true))
|
||||
(recur)))
|
||||
(finally
|
||||
(remove-tap tapfn))))))
|
||||
|
||||
;; --- State initialization
|
||||
|
||||
|
||||
@@ -9,14 +9,23 @@
|
||||
(:require
|
||||
[app.auth :as auth]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.db :as db]
|
||||
[app.rpc.commands.auth :as cmd.auth]
|
||||
[app.rpc.commands.profile :as cmd.profile]
|
||||
[app.util.json :as json]
|
||||
[app.setup :as-alias setup]
|
||||
[app.tokens :as tokens]
|
||||
[app.util.time :as dt]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(defn coercer
|
||||
[schema & {:as opts}]
|
||||
(let [decode-fn (sm/decoder schema sm/json-transformer)
|
||||
check-fn (sm/check-fn schema opts)]
|
||||
(fn [data]
|
||||
(-> data decode-fn check-fn))))
|
||||
|
||||
(defn- get-current-system
|
||||
[]
|
||||
(or (deref (requiring-resolve 'app.main/system))
|
||||
@@ -24,16 +33,21 @@
|
||||
|
||||
(defmulti ^:private exec-command ::cmd)
|
||||
|
||||
(defmethod exec-command :default
|
||||
[{:keys [::cmd]}]
|
||||
(ex/raise :type :internal
|
||||
:code :not-implemented
|
||||
:hint (str/ffmt "command '%' not implemented" cmd)))
|
||||
|
||||
(defn exec
|
||||
"Entry point with external tools integrations that uses PREPL
|
||||
interface for interacting with running penpot backend."
|
||||
[data]
|
||||
(let [data (json/decode data)]
|
||||
(-> {::cmd (keyword (:cmd data "default"))}
|
||||
(merge (:params data))
|
||||
(exec-command))))
|
||||
(-> {::cmd (get data :cmd)}
|
||||
(merge (:params data))
|
||||
(exec-command)))
|
||||
|
||||
(defmethod exec-command :create-profile
|
||||
(defmethod exec-command "create-profile"
|
||||
[{:keys [fullname email password is-active]
|
||||
:or {is-active true}}]
|
||||
(some-> (get-current-system)
|
||||
@@ -49,7 +63,7 @@
|
||||
(->> (cmd.auth/create-profile! conn params)
|
||||
(cmd.auth/create-profile-rels! conn)))))))
|
||||
|
||||
(defmethod exec-command :update-profile
|
||||
(defmethod exec-command "update-profile"
|
||||
[{:keys [fullname email password is-active]}]
|
||||
(some-> (get-current-system)
|
||||
(db/tx-run!
|
||||
@@ -70,7 +84,12 @@
|
||||
:deleted-at nil})]
|
||||
(pos? (db/get-update-count res)))))))))
|
||||
|
||||
(defmethod exec-command :delete-profile
|
||||
(defmethod exec-command "echo"
|
||||
[params]
|
||||
params)
|
||||
|
||||
|
||||
(defmethod exec-command "delete-profile"
|
||||
[{:keys [email soft]}]
|
||||
(when-not email
|
||||
(ex/raise :type :assertion
|
||||
@@ -88,7 +107,7 @@
|
||||
{:email email}))]
|
||||
(pos? (db/get-update-count res)))))))
|
||||
|
||||
(defmethod exec-command :search-profile
|
||||
(defmethod exec-command "search-profile"
|
||||
[{:keys [email]}]
|
||||
(when-not email
|
||||
(ex/raise :type :assertion
|
||||
@@ -102,12 +121,130 @@
|
||||
" where email similar to ? order by created_at desc limit 100")]
|
||||
(db/exec! conn [sql email]))))))
|
||||
|
||||
(defmethod exec-command :derive-password
|
||||
(defmethod exec-command "derive-password"
|
||||
[{:keys [password]}]
|
||||
(auth/derive-password password))
|
||||
|
||||
(defmethod exec-command :default
|
||||
[{:keys [::cmd]}]
|
||||
(ex/raise :type :internal
|
||||
:code :not-implemented
|
||||
:hint (str/ffmt "command '%' not implemented" (name cmd))))
|
||||
(defmethod exec-command "authenticate"
|
||||
[{:keys [token]}]
|
||||
(when-let [system (get-current-system)]
|
||||
(let [props (get system ::setup/props)]
|
||||
(tokens/verify props {:token token :iss "authentication"}))))
|
||||
|
||||
(def ^:private schema:get-customer
|
||||
[:map [:id ::sm/uuid]])
|
||||
|
||||
(def coerce-get-customer-params
|
||||
(coercer schema:get-customer
|
||||
:type :validation
|
||||
:hint "invalid data provided for `get-customer` rpc call"))
|
||||
|
||||
(def sql:get-customer-slots
|
||||
"WITH teams AS (
|
||||
SELECT tpr.team_id AS id,
|
||||
tpr.profile_id AS profile_id
|
||||
FROM team_profile_rel AS tpr
|
||||
WHERE tpr.is_owner IS true
|
||||
AND tpr.profile_id = ?
|
||||
), teams_with_slots AS (
|
||||
SELECT tpr.team_id AS id,
|
||||
count(*) AS total
|
||||
FROM team_profile_rel AS tpr
|
||||
WHERE tpr.team_id IN (SELECT id FROM teams)
|
||||
AND tpr.can_edit IS true
|
||||
GROUP BY 1
|
||||
ORDER BY 2
|
||||
)
|
||||
SELECT max(total) AS total FROM teams_with_slots;")
|
||||
|
||||
(defn- get-customer-slots
|
||||
[system profile-id]
|
||||
(let [result (db/exec-one! system [sql:get-customer-slots profile-id])]
|
||||
(:total result)))
|
||||
|
||||
(defmethod exec-command "get-customer"
|
||||
[params]
|
||||
(when-let [system (get-current-system)]
|
||||
(let [{:keys [id] :as params} (coerce-get-customer-params params)
|
||||
{:keys [props] :as profile} (cmd.profile/get-profile system id)]
|
||||
{:id (get profile :id)
|
||||
:name (get profile :fullname)
|
||||
:email (get profile :email)
|
||||
:num-editors (get-customer-slots system id)
|
||||
:subscription (get props :subscription)})))
|
||||
|
||||
(def ^:private schema:customer-subscription
|
||||
[:map {:title "CustomerSubscription"}
|
||||
[:id ::sm/text]
|
||||
[:customer-id ::sm/text]
|
||||
[:type [:enum
|
||||
"unlimited"
|
||||
"professional"
|
||||
"enterprise"]]
|
||||
[:status [:enum
|
||||
"active"
|
||||
"canceled"
|
||||
"incomplete"
|
||||
"incomplete_expired"
|
||||
"pass_due"
|
||||
"paused"
|
||||
"trialing"
|
||||
"unpaid"]]
|
||||
|
||||
[:billing-period [:enum
|
||||
"month"
|
||||
"day"
|
||||
"week"
|
||||
"year"]]
|
||||
[:quantity :int]
|
||||
[:description [:maybe ::sm/text]]
|
||||
[:created-at ::sm/timestamp]
|
||||
[:start-date [:maybe ::sm/timestamp]]
|
||||
[:ended-at [:maybe ::sm/timestamp]]
|
||||
[:trial-end [:maybe ::sm/timestamp]]
|
||||
[:trial-start [:maybe ::sm/timestamp]]
|
||||
[:cancel-at [:maybe ::sm/timestamp]]
|
||||
[:canceled-at [:maybe ::sm/timestamp]]
|
||||
|
||||
[:current-period-end ::sm/timestamp]
|
||||
[:current-period-start ::sm/timestamp]
|
||||
[:cancel-at-period-end :boolean]
|
||||
|
||||
[:cancellation-details
|
||||
[:map {:title "CancellationDetails"}
|
||||
[:comment [:maybe ::sm/text]]
|
||||
[:reason [:maybe ::sm/text]]
|
||||
[:feedback [:maybe
|
||||
[:enum
|
||||
"customer_service"
|
||||
"low_quality"
|
||||
"missing_feature"
|
||||
"other"
|
||||
"switched_service"
|
||||
"too_complex"
|
||||
"too_expensive"
|
||||
"unused"]]]]]])
|
||||
|
||||
(def ^:private schema:update-customer-subscription
|
||||
[:map
|
||||
[:id ::sm/uuid]
|
||||
[:subscription [:maybe schema:customer-subscription]]])
|
||||
|
||||
(def coerce-update-customer-subscription-params
|
||||
(coercer schema:update-customer-subscription
|
||||
:type :validation
|
||||
:hint "invalid data provided for `update-customer-subscription` rpc call"))
|
||||
|
||||
(defmethod exec-command "update-customer-subscription"
|
||||
[params]
|
||||
(when-let [system (get-current-system)]
|
||||
(let [{:keys [id subscription]} (coerce-update-customer-subscription-params params)
|
||||
;; FIXME: locking
|
||||
{:keys [props] :as profile} (cmd.profile/get-profile system id)
|
||||
props (assoc props :subscription subscription)]
|
||||
|
||||
(db/update! system :profile
|
||||
{:props (db/tjson props)}
|
||||
{:id id}
|
||||
{::db/return-keys false})
|
||||
true)))
|
||||
|
||||
@@ -1,306 +0,0 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.srepl.components-v2
|
||||
(:require
|
||||
[app.common.fressian :as fres]
|
||||
[app.common.logging :as l]
|
||||
[app.db :as db]
|
||||
[app.features.components-v2 :as feat]
|
||||
[app.main :as main]
|
||||
[app.srepl.helpers :as h]
|
||||
[app.util.events :as events]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as-alias wrk]
|
||||
[datoteka.fs :as fs]
|
||||
[datoteka.io :as io]
|
||||
[promesa.exec :as px]
|
||||
[promesa.exec.semaphore :as ps]
|
||||
[promesa.util :as pu]))
|
||||
|
||||
(def ^:dynamic *scope* nil)
|
||||
(def ^:dynamic *semaphore* nil)
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; PRIVATE HELPERS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:private sql:get-files-by-created-at
|
||||
"SELECT id, features,
|
||||
row_number() OVER (ORDER BY created_at DESC) AS rown
|
||||
FROM file
|
||||
WHERE deleted_at IS NULL
|
||||
ORDER BY created_at DESC")
|
||||
|
||||
(defn- get-files
|
||||
[conn]
|
||||
(->> (db/cursor conn [sql:get-files-by-created-at] {:chunk-size 500})
|
||||
(map feat/decode-row)
|
||||
(remove (fn [{:keys [features]}]
|
||||
(contains? features "components/v2")))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; PUBLIC API
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn migrate-file!
|
||||
[file-id & {:keys [rollback? validate? label cache skip-on-graphic-error?]
|
||||
:or {rollback? true
|
||||
validate? false
|
||||
skip-on-graphic-error? true}}]
|
||||
(l/dbg :hint "migrate:start" :rollback rollback?)
|
||||
(let [tpoint (dt/tpoint)
|
||||
file-id (h/parse-uuid file-id)]
|
||||
|
||||
(binding [feat/*stats* (atom {})
|
||||
feat/*cache* cache]
|
||||
(try
|
||||
(-> (assoc main/system ::db/rollback rollback?)
|
||||
(feat/migrate-file! file-id
|
||||
:validate? validate?
|
||||
:skip-on-graphic-error? skip-on-graphic-error?
|
||||
:label label))
|
||||
|
||||
(-> (deref feat/*stats*)
|
||||
(assoc :elapsed (dt/format-duration (tpoint))))
|
||||
|
||||
(catch Throwable cause
|
||||
(l/wrn :hint "migrate:error" :cause cause))
|
||||
|
||||
(finally
|
||||
(let [elapsed (dt/format-duration (tpoint))]
|
||||
(l/dbg :hint "migrate:end" :rollback rollback? :elapsed elapsed)))))))
|
||||
|
||||
(defn migrate-team!
|
||||
[team-id & {:keys [rollback? skip-on-graphic-error? validate? label cache]
|
||||
:or {rollback? true
|
||||
validate? true
|
||||
skip-on-graphic-error? true}}]
|
||||
|
||||
(l/dbg :hint "migrate:start" :rollback rollback?)
|
||||
|
||||
(let [team-id (h/parse-uuid team-id)
|
||||
stats (atom {})
|
||||
tpoint (dt/tpoint)]
|
||||
|
||||
(binding [feat/*stats* stats
|
||||
feat/*cache* cache]
|
||||
(try
|
||||
(-> (assoc main/system ::db/rollback rollback?)
|
||||
(feat/migrate-team! team-id
|
||||
:label label
|
||||
:validate? validate?
|
||||
:skip-on-graphics-error? skip-on-graphic-error?))
|
||||
|
||||
(-> (deref feat/*stats*)
|
||||
(assoc :elapsed (dt/format-duration (tpoint))))
|
||||
|
||||
(catch Throwable cause
|
||||
(l/dbg :hint "migrate:error" :cause cause))
|
||||
|
||||
(finally
|
||||
(let [elapsed (dt/format-duration (tpoint))]
|
||||
(l/dbg :hint "migrate:end" :rollback rollback? :elapsed elapsed)))))))
|
||||
|
||||
(defn migrate-files!
|
||||
"A REPL helper for migrate all files.
|
||||
|
||||
This function starts multiple concurrent file migration processes
|
||||
until thw maximum number of jobs is reached which by default has the
|
||||
value of `1`. This is controled with the `:max-jobs` option.
|
||||
|
||||
If you want to run this on multiple machines you will need to specify
|
||||
the total number of partitions and the current partition.
|
||||
|
||||
In order to get the report table populated, you will need to provide
|
||||
a correct `:label`. That label is also used for persist a file
|
||||
snaphot before continue with the migration."
|
||||
[& {:keys [max-jobs max-items rollback? validate?
|
||||
cache skip-on-graphic-error?
|
||||
label partitions current-partition]
|
||||
:or {validate? false
|
||||
rollback? true
|
||||
max-jobs 1
|
||||
current-partition 1
|
||||
skip-on-graphic-error? true
|
||||
max-items Long/MAX_VALUE}}]
|
||||
|
||||
(when (int? partitions)
|
||||
(when-not (int? current-partition)
|
||||
(throw (IllegalArgumentException. "missing `current-partition` parameter")))
|
||||
(when-not (<= 0 current-partition partitions)
|
||||
(throw (IllegalArgumentException. "invalid value on `current-partition` parameter"))))
|
||||
|
||||
(let [stats (atom {})
|
||||
tpoint (dt/tpoint)
|
||||
factory (px/thread-factory :virtual false :prefix "penpot/migration/")
|
||||
executor (px/cached-executor :factory factory)
|
||||
|
||||
sjobs (ps/create :permits max-jobs)
|
||||
|
||||
migrate-file
|
||||
(fn [file-id rown]
|
||||
(try
|
||||
(db/tx-run! (assoc main/system ::db/rollback rollback?)
|
||||
(fn [system]
|
||||
(db/exec-one! system ["SET LOCAL idle_in_transaction_session_timeout = 0"])
|
||||
(feat/migrate-file! system file-id
|
||||
:rown rown
|
||||
:label label
|
||||
:validate? validate?
|
||||
:skip-on-graphic-error? skip-on-graphic-error?)))
|
||||
|
||||
(catch Throwable cause
|
||||
(l/wrn :hint "unexpected error on processing file (skiping)"
|
||||
:file-id (str file-id))
|
||||
|
||||
(events/tap :error
|
||||
(ex-info "unexpected error on processing file (skiping)"
|
||||
{:file-id file-id}
|
||||
cause))
|
||||
|
||||
(swap! stats update :errors (fnil inc 0)))
|
||||
|
||||
(finally
|
||||
(ps/release! sjobs))))
|
||||
|
||||
process-file
|
||||
(fn [{:keys [id rown]}]
|
||||
(ps/acquire! sjobs)
|
||||
(px/run! executor (partial migrate-file id rown)))]
|
||||
|
||||
(l/dbg :hint "migrate:start"
|
||||
:label label
|
||||
:rollback rollback?
|
||||
:max-jobs max-jobs
|
||||
:max-items max-items)
|
||||
|
||||
(binding [feat/*stats* stats
|
||||
feat/*cache* cache]
|
||||
(try
|
||||
(db/tx-run! main/system
|
||||
(fn [{:keys [::db/conn] :as system}]
|
||||
(db/exec! conn ["SET LOCAL statement_timeout = 0"])
|
||||
(db/exec! conn ["SET LOCAL idle_in_transaction_session_timeout = 0"])
|
||||
|
||||
(run! process-file
|
||||
(->> (get-files conn)
|
||||
(filter (fn [{:keys [rown] :as row}]
|
||||
(if (int? partitions)
|
||||
(= current-partition (inc (mod rown partitions)))
|
||||
true)))
|
||||
(take max-items)))
|
||||
|
||||
;; Close and await tasks
|
||||
(pu/close! executor)))
|
||||
|
||||
(-> (deref stats)
|
||||
(assoc :elapsed (dt/format-duration (tpoint))))
|
||||
|
||||
(catch Throwable cause
|
||||
(l/dbg :hint "migrate:error" :cause cause)
|
||||
(events/tap :error cause))
|
||||
|
||||
(finally
|
||||
(let [elapsed (dt/format-duration (tpoint))]
|
||||
(l/dbg :hint "migrate:end"
|
||||
:rollback rollback?
|
||||
:elapsed elapsed)))))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; CACHE POPULATE
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def sql:sobjects-for-cache
|
||||
"SELECT id,
|
||||
row_number() OVER (ORDER BY created_at) AS index
|
||||
FROM storage_object
|
||||
WHERE (metadata->>'~:bucket' = 'file-media-object' OR
|
||||
metadata->>'~:bucket' IS NULL)
|
||||
AND metadata->>'~:content-type' = 'image/svg+xml'
|
||||
AND deleted_at IS NULL
|
||||
AND size < 1135899
|
||||
ORDER BY created_at ASC")
|
||||
|
||||
(defn populate-cache!
|
||||
"A REPL helper for migrate all files.
|
||||
|
||||
This function starts multiple concurrent file migration processes
|
||||
until thw maximum number of jobs is reached which by default has the
|
||||
value of `1`. This is controled with the `:max-jobs` option.
|
||||
|
||||
If you want to run this on multiple machines you will need to specify
|
||||
the total number of partitions and the current partition.
|
||||
|
||||
In order to get the report table populated, you will need to provide
|
||||
a correct `:label`. That label is also used for persist a file
|
||||
snaphot before continue with the migration."
|
||||
[& {:keys [max-jobs] :or {max-jobs 1}}]
|
||||
|
||||
(let [tpoint (dt/tpoint)
|
||||
|
||||
factory (px/thread-factory :virtual false :prefix "penpot/cache/")
|
||||
executor (px/cached-executor :factory factory)
|
||||
|
||||
sjobs (ps/create :permits max-jobs)
|
||||
|
||||
retrieve-sobject
|
||||
(fn [id index]
|
||||
(let [path (feat/get-sobject-cache-path id)
|
||||
parent (fs/parent path)]
|
||||
|
||||
(try
|
||||
(when-not (fs/exists? parent)
|
||||
(fs/create-dir parent))
|
||||
|
||||
(if (fs/exists? path)
|
||||
(l/inf :hint "create cache entry" :status "exists" :index index :id (str id) :path (str path))
|
||||
(let [svg-data (feat/get-optimized-svg id)]
|
||||
(with-open [^java.lang.AutoCloseable stream (io/output-stream path)]
|
||||
(let [writer (fres/writer stream)]
|
||||
(fres/write! writer svg-data)))
|
||||
|
||||
(l/inf :hint "create cache entry" :status "created"
|
||||
:index index
|
||||
:id (str id)
|
||||
:path (str path))))
|
||||
|
||||
(catch Throwable cause
|
||||
(l/wrn :hint "create cache entry"
|
||||
:status "error"
|
||||
:index index
|
||||
:id (str id)
|
||||
:path (str path)
|
||||
:cause cause))
|
||||
|
||||
(finally
|
||||
(ps/release! sjobs)))))
|
||||
|
||||
process-sobject
|
||||
(fn [{:keys [id index]}]
|
||||
(ps/acquire! sjobs)
|
||||
(px/run! executor (partial retrieve-sobject id index)))]
|
||||
|
||||
(l/dbg :hint "migrate:start"
|
||||
:max-jobs max-jobs)
|
||||
|
||||
(try
|
||||
(binding [feat/*system* main/system]
|
||||
(run! process-sobject
|
||||
(db/exec! main/system [sql:sobjects-for-cache]))
|
||||
|
||||
;; Close and await tasks
|
||||
(pu/close! executor))
|
||||
|
||||
{:elapsed (dt/format-duration (tpoint))}
|
||||
|
||||
(catch Throwable cause
|
||||
(l/dbg :hint "populate:error" :cause cause))
|
||||
|
||||
(finally
|
||||
(let [elapsed (dt/format-duration (tpoint))]
|
||||
(l/dbg :hint "populate:end"
|
||||
:elapsed elapsed))))))
|
||||
@@ -179,7 +179,7 @@
|
||||
component-child (first component-children)]
|
||||
(if (or (nil? child) (nil? component-child))
|
||||
container
|
||||
(let [container (if (and (not (ctk/is-main-of? component-child child true))
|
||||
(let [container (if (and (not (ctk/is-main-of? component-child child))
|
||||
(nil? (ctk/get-swap-slot child))
|
||||
(ctk/instance-head? child))
|
||||
(let [slot (guess-swap-slot component-child component-container)]
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
[app.common.files.migrations :as fmg]
|
||||
[app.common.files.validate :as cfv]
|
||||
[app.db :as db]
|
||||
[app.features.components-v2 :as feat.comp-v2]
|
||||
[app.main :as main]
|
||||
[app.rpc.commands.files :as files]
|
||||
[app.rpc.commands.files-snapshot :as fsnap]
|
||||
@@ -62,6 +61,27 @@
|
||||
{:id id})
|
||||
team))
|
||||
|
||||
(def ^:private sql:get-and-lock-team-files
|
||||
"SELECT f.id
|
||||
FROM file AS f
|
||||
JOIN project AS p ON (p.id = f.project_id)
|
||||
WHERE p.team_id = ?
|
||||
AND p.deleted_at IS NULL
|
||||
AND f.deleted_at IS NULL
|
||||
FOR UPDATE")
|
||||
|
||||
(defn get-team
|
||||
[conn team-id]
|
||||
(-> (db/get conn :team {:id team-id}
|
||||
{::db/remove-deleted false
|
||||
::db/check-deleted false})
|
||||
(update :features db/decode-pgarray #{})))
|
||||
|
||||
(defn get-and-lock-team-files
|
||||
[conn team-id]
|
||||
(transduce (map :id) conj []
|
||||
(db/plan conn [sql:get-and-lock-team-files team-id])))
|
||||
|
||||
(defn reset-file-data!
|
||||
"Hardcode replace of the data of one file."
|
||||
[system id data]
|
||||
@@ -96,7 +116,7 @@
|
||||
(defn take-team-snapshot!
|
||||
[system team-id label]
|
||||
(let [conn (db/get-connection system)]
|
||||
(->> (feat.comp-v2/get-and-lock-team-files conn team-id)
|
||||
(->> (get-and-lock-team-files conn team-id)
|
||||
(reduce (fn [result file-id]
|
||||
(let [file (fsnap/get-file-snapshots system file-id)]
|
||||
(fsnap/create-file-snapshot! system file
|
||||
@@ -108,19 +128,16 @@
|
||||
(defn restore-team-snapshot!
|
||||
[system team-id label]
|
||||
(let [conn (db/get-connection system)
|
||||
ids (->> (feat.comp-v2/get-and-lock-team-files conn team-id)
|
||||
ids (->> (get-and-lock-team-files conn team-id)
|
||||
(into #{}))
|
||||
|
||||
snap (search-file-snapshots conn ids label)
|
||||
|
||||
ids' (into #{} (map :file-id) snap)
|
||||
team (-> (feat.comp-v2/get-team conn team-id)
|
||||
(update :features disj "components/v2"))]
|
||||
ids' (into #{} (map :file-id) snap)]
|
||||
|
||||
(when (not= ids ids')
|
||||
(throw (RuntimeException. "no uniform snapshot available")))
|
||||
|
||||
(feat.comp-v2/update-team! conn team)
|
||||
(reduce (fn [result {:keys [file-id id]}]
|
||||
(fsnap/restore-file-snapshot! system file-id id)
|
||||
(inc result))
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.db.sql :as-alias sql]
|
||||
[app.features.components-v2 :as feat.comp-v2]
|
||||
[app.features.fdata :as feat.fdata]
|
||||
[app.loggers.audit :as audit]
|
||||
[app.main :as main]
|
||||
@@ -156,6 +155,10 @@
|
||||
[file-id & {:as opts}]
|
||||
(process-file! file-id feat.fdata/enable-pointer-map opts))
|
||||
|
||||
(defn enable-path-data-feature-on-file!
|
||||
[file-id & {:as opts}]
|
||||
(process-file! file-id feat.fdata/enable-path-data opts))
|
||||
|
||||
(defn enable-storage-features-on-file!
|
||||
[file-id & {:as opts}]
|
||||
(enable-objects-map-feature-on-file! file-id opts)
|
||||
@@ -337,14 +340,23 @@
|
||||
(db/tx-run! main/system fsnap/create-file-snapshot! {:file-id file-id :label label})))
|
||||
|
||||
(defn restore-file-snapshot!
|
||||
[file-id label]
|
||||
(let [file-id (h/parse-uuid file-id)]
|
||||
[file-id & {:keys [label id]}]
|
||||
(let [file-id (h/parse-uuid file-id)
|
||||
snapshot-id (some-> id h/parse-uuid)]
|
||||
(db/tx-run! main/system
|
||||
(fn [{:keys [::db/conn] :as system}]
|
||||
(when-let [snapshot (->> (h/search-file-snapshots conn #{file-id} label)
|
||||
(map :id)
|
||||
(first))]
|
||||
(fsnap/restore-file-snapshot! system file-id (:id snapshot)))))))
|
||||
(cond
|
||||
(uuid? snapshot-id)
|
||||
(fsnap/restore-file-snapshot! system file-id snapshot-id)
|
||||
|
||||
(string? label)
|
||||
(->> (h/search-file-snapshots conn #{file-id} label)
|
||||
(map :id)
|
||||
(first)
|
||||
(fsnap/restore-file-snapshot! system file-id))
|
||||
|
||||
:else
|
||||
(throw (ex-info "snapshot id or label should be provided" {})))))))
|
||||
|
||||
(defn list-file-snapshots!
|
||||
[file-id & {:as _}]
|
||||
@@ -407,10 +419,12 @@
|
||||
"Apply a function to the file. Optionally save the changes or not.
|
||||
The function receives the decoded and migrated file data."
|
||||
[file-id update-fn & {:keys [rollback?] :or {rollback? true} :as opts}]
|
||||
(db/tx-run! (assoc main/system ::db/rollback rollback?)
|
||||
(fn [system]
|
||||
(binding [h/*system* system]
|
||||
(h/process-file! system file-id update-fn opts)))))
|
||||
(let [file-id (h/parse-uuid file-id)]
|
||||
(db/tx-run! (assoc main/system ::db/rollback rollback?)
|
||||
(fn [system]
|
||||
(binding [h/*system* system
|
||||
db/*conn* (db/get-connection system)]
|
||||
(h/process-file! system file-id update-fn opts))))))
|
||||
|
||||
(defn process-team-files!
|
||||
"Apply a function to each file of the specified team."
|
||||
@@ -422,8 +436,9 @@
|
||||
(when (string? label)
|
||||
(h/take-team-snapshot! system team-id label))
|
||||
|
||||
(binding [h/*system* system]
|
||||
(->> (feat.comp-v2/get-and-lock-team-files conn team-id)
|
||||
(binding [h/*system* system
|
||||
db/*conn* (db/get-connection system)]
|
||||
(->> (h/get-and-lock-team-files conn team-id)
|
||||
(reduce (fn [result file-id]
|
||||
(if (h/process-file! system file-id update-fn opts)
|
||||
(inc result)
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
file is eligible to be garbage collected after some period of
|
||||
inactivity (the default threshold is 72h)."
|
||||
(:require
|
||||
[app.binfile.cleaner :as bfl]
|
||||
[app.binfile.common :as bfc]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.files.validate :as cfv]
|
||||
@@ -258,6 +259,7 @@
|
||||
(if-let [file (get-file cfg file-id)]
|
||||
(let [file (->> file
|
||||
(bfc/decode-file cfg)
|
||||
(bfl/clean-file)
|
||||
(clean-media! cfg)
|
||||
(clean-fragments! cfg))
|
||||
file (assoc file :has-media-trimmed true)]
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
to them. Mainly used in http.sse for progress reporting."
|
||||
(:refer-clojure :exclude [tap run!])
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[promesa.exec :as px]
|
||||
@@ -18,33 +17,30 @@
|
||||
|
||||
(def ^:dynamic *channel* nil)
|
||||
|
||||
(defn channel
|
||||
[]
|
||||
(sp/chan :buf 32))
|
||||
|
||||
(defn tap
|
||||
[type data]
|
||||
(when-let [channel *channel*]
|
||||
(sp/put! channel [type data])
|
||||
nil))
|
||||
([type data]
|
||||
(when-let [channel *channel*]
|
||||
(sp/put! channel [type data])
|
||||
nil))
|
||||
([channel type data]
|
||||
(when channel
|
||||
(sp/put! channel [type data])
|
||||
nil)))
|
||||
|
||||
(defn start-listener
|
||||
[on-event on-close]
|
||||
|
||||
(dm/assert!
|
||||
"expected active events channel"
|
||||
(sp/chan? *channel*))
|
||||
[channel on-event on-close]
|
||||
(assert (sp/chan? channel) "expected active events channel")
|
||||
|
||||
(px/thread
|
||||
{:virtual true}
|
||||
(try
|
||||
(loop []
|
||||
(when-let [event (sp/take! *channel*)]
|
||||
(when-let [event (sp/take! channel)]
|
||||
(let [result (ex/try! (on-event event))]
|
||||
(if (ex/exception? result)
|
||||
(do
|
||||
(l/wrn :hint "unexpected exception" :cause result)
|
||||
(sp/close! *channel*))
|
||||
(sp/close! channel))
|
||||
(recur)))))
|
||||
(finally
|
||||
(on-close)))))
|
||||
@@ -55,7 +51,7 @@
|
||||
[f on-event]
|
||||
|
||||
(binding [*channel* (sp/chan :buf 32)]
|
||||
(let [listener (start-listener on-event (constantly nil))]
|
||||
(let [listener (start-listener *channel* on-event (constantly nil))]
|
||||
(try
|
||||
(f)
|
||||
(finally
|
||||
|
||||
@@ -1712,6 +1712,7 @@
|
||||
[{:fill-image
|
||||
{:id (:id fmedia)
|
||||
:name "test"
|
||||
:mtype "image/jpeg"
|
||||
:width 200
|
||||
:height 200}}]]
|
||||
|
||||
|
||||
@@ -449,6 +449,23 @@
|
||||
(t/is (nil? res)))))
|
||||
|
||||
|
||||
(t/deftest get-owned-teams
|
||||
(let [profile1 (th/create-profile* 1 {:is-active true})
|
||||
profile2 (th/create-profile* 2 {:is-active true})
|
||||
team1 (th/create-team* 1 {:profile-id (:id profile1)})
|
||||
team2 (th/create-team* 2 {:profile-id (:id profile2)})
|
||||
|
||||
params {::th/type :get-owned-teams
|
||||
::rpc/profile-id (:id profile1)}
|
||||
out (th/command! params)]
|
||||
|
||||
(t/is (th/success? out))
|
||||
(let [result (:result out)]
|
||||
(t/is (= 1 (count result)))
|
||||
(t/is (= (:id team1) (-> result first :id)))
|
||||
(t/is (not= (:default-team-id profile1) (-> result first :id))))))
|
||||
|
||||
|
||||
(t/deftest team-deletion-1
|
||||
(let [profile1 (th/create-profile* 1 {:is-active true})
|
||||
team (th/create-team* 1 {:profile-id (:id profile1)})
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{org.clojure/clojure {:mvn/version "1.12.0"}
|
||||
org.clojure/data.json {:mvn/version "2.5.1"}
|
||||
org.clojure/tools.cli {:mvn/version "1.1.230"}
|
||||
org.clojure/clojurescript {:mvn/version "1.11.132"}
|
||||
org.clojure/clojurescript {:mvn/version "1.12.38"}
|
||||
org.clojure/test.check {:mvn/version "1.1.1"}
|
||||
org.clojure/data.fressian {:mvn/version "1.1.0"}
|
||||
|
||||
@@ -12,14 +12,14 @@
|
||||
org.apache.logging.log4j/log4j-web {:mvn/version "2.24.3"}
|
||||
org.apache.logging.log4j/log4j-jul {:mvn/version "2.24.3"}
|
||||
org.apache.logging.log4j/log4j-slf4j2-impl {:mvn/version "2.24.3"}
|
||||
org.slf4j/slf4j-api {:mvn/version "2.0.16"}
|
||||
org.slf4j/slf4j-api {:mvn/version "2.0.17"}
|
||||
pl.tkowalcz.tjahzi/log4j2-appender {:mvn/version "0.9.32"}
|
||||
|
||||
selmer/selmer {:mvn/version "1.12.61"}
|
||||
selmer/selmer {:mvn/version "1.12.62"}
|
||||
criterium/criterium {:mvn/version "0.4.6"}
|
||||
|
||||
metosin/jsonista {:mvn/version "0.3.13"}
|
||||
metosin/malli {:mvn/version "0.17.0"}
|
||||
metosin/malli {:mvn/version "0.18.0"}
|
||||
|
||||
expound/expound {:mvn/version "0.9.0"}
|
||||
com.cognitect/transit-clj {:mvn/version "1.0.333"}
|
||||
@@ -30,7 +30,7 @@
|
||||
funcool/tubax {:mvn/version "2021.05.20-0"}
|
||||
funcool/cuerdas {:mvn/version "2023.11.09-407"}
|
||||
funcool/promesa
|
||||
{:git/sha "0c5ed6ad033515a2df4b55addea044f60e9653d0"
|
||||
{:git/sha "f52f58cfacf62f59eab717e2637f37729d0cc383"
|
||||
:git/url "https://github.com/funcool/promesa"}
|
||||
|
||||
funcool/datoteka
|
||||
@@ -59,7 +59,7 @@
|
||||
{:dev
|
||||
{:extra-deps
|
||||
{org.clojure/tools.namespace {:mvn/version "RELEASE"}
|
||||
thheller/shadow-cljs {:mvn/version "2.28.20"}
|
||||
thheller/shadow-cljs {:mvn/version "3.0.5"}
|
||||
com.clojure-goes-fast/clj-async-profiler {:mvn/version "RELEASE"}
|
||||
com.bhauman/rebel-readline {:mvn/version "RELEASE"}
|
||||
criterium/criterium {:mvn/version "RELEASE"}
|
||||
@@ -68,7 +68,7 @@
|
||||
|
||||
:build
|
||||
{:extra-deps
|
||||
{io.github.clojure/tools.build {:git/tag "v0.10.6" :git/sha "52cf7d6"}}
|
||||
{io.github.clojure/tools.build {:git/tag "v0.10.9" :git/sha "e405aac"}}
|
||||
:ns-default build}
|
||||
|
||||
:test
|
||||
@@ -76,9 +76,9 @@
|
||||
:extra-deps {lambdaisland/kaocha {:mvn/version "1.91.1392"}}}
|
||||
|
||||
:shadow-cljs
|
||||
{:main-opts ["-m" "shadow.cljs.devtools.cli"]}
|
||||
{:main-opts ["-m" "shadow.cljs.devtools.cli"]
|
||||
:jvm-opts ["--sun-misc-unsafe-memory-access=allow"]}
|
||||
|
||||
:outdated
|
||||
{:extra-deps {com.github.liquidz/antq {:mvn/version "RELEASE"}}
|
||||
:main-opts ["-m" "antq.core"]}}}
|
||||
|
||||
|
||||
@@ -4,20 +4,19 @@
|
||||
"license": "MPL-2.0",
|
||||
"author": "Kaleidos INC",
|
||||
"private": true,
|
||||
"packageManager": "yarn@4.8.1+sha512.bc946f2a022d7a1a38adfc15b36a66a3807a67629789496c3714dd1703d2e6c6b1c69ff9ec3b43141ac7a1dd853b7685638eb0074300386a59c18df351ef8ff6",
|
||||
"packageManager": "yarn@4.9.1+sha512.f95ce356460e05be48d66401c1ae64ef84d163dd689964962c6888a9810865e39097a5e9de748876c2e0bf89b232d583c33982773e9903ae7a76257270986538",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/penpot/penpot"
|
||||
},
|
||||
"dependencies": {
|
||||
"luxon": "^3.4.4",
|
||||
"sax": "^1.4.1"
|
||||
"luxon": "^3.4.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"concurrently": "^9.0.1",
|
||||
"nodemon": "^3.1.7",
|
||||
"shadow-cljs": "2.28.20",
|
||||
"shadow-cljs": "3.0.5",
|
||||
"source-map-support": "^0.5.21",
|
||||
"ws": "^8.17.0"
|
||||
},
|
||||
|
||||
@@ -2,16 +2,20 @@
|
||||
|
||||
export PENPOT_FLAGS="enable-asserts enable-audit-log $PENPOT_FLAGS"
|
||||
|
||||
export OPTIONS="
|
||||
-A:dev \
|
||||
-J-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
|
||||
-J-Djdk.attach.allowAttachSelf \
|
||||
-J-Dpolyglot.engine.WarnInterpreterOnly=false \
|
||||
-J-XX:+EnableDynamicAgentLoading \
|
||||
-J-XX:-OmitStackTraceInFastThrow \
|
||||
-J-XX:+UnlockDiagnosticVMOptions \
|
||||
-J-XX:+DebugNonSafepoints \
|
||||
-J-Djdk.tracePinnedThreads=full"
|
||||
export JAVA_OPTS="\
|
||||
-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
|
||||
-Djdk.attach.allowAttachSelf \
|
||||
-Dlog4j2.configurationFile=log4j2-devenv-repl.xml \
|
||||
-Djdk.tracePinnedThreads=full \
|
||||
-XX:+EnableDynamicAgentLoading \
|
||||
-XX:-OmitStackTraceInFastThrow \
|
||||
-XX:+UnlockDiagnosticVMOptions \
|
||||
-XX:+DebugNonSafepoints \
|
||||
--sun-misc-unsafe-memory-access=allow \
|
||||
--enable-preview \
|
||||
--enable-native-access=ALL-UNNAMED";
|
||||
|
||||
export OPTIONS="-A:dev"
|
||||
|
||||
export OPTIONS_EVAL="nil"
|
||||
# export OPTIONS_EVAL="(set! *warn-on-reflection* true)"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
(ns app.common.data.macros
|
||||
"Data retrieval & manipulation specific macros."
|
||||
(:refer-clojure :exclude [get-in select-keys str with-open min max])
|
||||
(:refer-clojure :exclude [get-in select-keys str with-open max])
|
||||
#?(:cljs (:require-macros [app.common.data.macros]))
|
||||
(:require
|
||||
#?(:clj [clojure.core :as c]
|
||||
@@ -144,3 +144,8 @@
|
||||
(str "expr assert: " (pr-str expr)))]
|
||||
(when *assert*
|
||||
`(runtime-assert ~hint (fn [] ~expr))))))
|
||||
|
||||
(defn truncate
|
||||
"Truncates a string to a certain length"
|
||||
[s max-length]
|
||||
(subs s 0 (min max-length (count s))))
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
|
||||
(defn undo
|
||||
[stack]
|
||||
(update stack :index dec))
|
||||
(update stack :index #(max 0 (dec %))))
|
||||
|
||||
(defn redo
|
||||
[{index :index items :items :as stack}]
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
#{"fdata/objects-map"
|
||||
"fdata/pointer-map"
|
||||
"fdata/shape-data-type"
|
||||
"fdata/path-data"
|
||||
"components/v2"
|
||||
"styles/v2"
|
||||
"layout/grid"
|
||||
@@ -58,12 +59,18 @@
|
||||
;; A set of features enabled by default
|
||||
(def default-features
|
||||
#{"fdata/shape-data-type"
|
||||
"fdata/path-data"
|
||||
"styles/v2"
|
||||
"layout/grid"
|
||||
"components/v2"
|
||||
"plugins/runtime"
|
||||
"design-tokens/v1"})
|
||||
|
||||
;; A set of features that should not be propagated to team on creating
|
||||
;; or modifying a file
|
||||
(def no-team-inheritable-features
|
||||
#{"fdata/path-data"})
|
||||
|
||||
;; A set of features which only affects on frontend and can be enabled
|
||||
;; and disabled freely by the user any time. This features does not
|
||||
;; persist on file features field but can be permanently enabled on
|
||||
@@ -86,8 +93,9 @@
|
||||
;; without migration applied)
|
||||
(def no-migration-features
|
||||
(-> #{"layout/grid"
|
||||
"design-tokens/v1"
|
||||
"fdata/shape-data-type"
|
||||
"design-tokens/v1"}
|
||||
"fdata/path-data"}
|
||||
(into frontend-only-features)
|
||||
(into backend-only-features)))
|
||||
|
||||
@@ -103,9 +111,7 @@
|
||||
"Translate a flag to a feature name"
|
||||
[flag]
|
||||
(case flag
|
||||
:feature-components-v2 "components/v2"
|
||||
:feature-styles-v2 "styles/v2"
|
||||
:feature-grid-layout "layout/grid"
|
||||
:feature-fdata-objects-map "fdata/objects-map"
|
||||
:feature-fdata-pointer-map "fdata/pointer-map"
|
||||
:feature-plugins "plugins/runtime"
|
||||
@@ -216,6 +222,12 @@
|
||||
|
||||
(check-supported-features! file-features)
|
||||
|
||||
;; Components v1 is deprecated
|
||||
(when-not (contains? file-features "components/v2")
|
||||
(ex/raise :type :restriction
|
||||
:code :file-in-components-v1
|
||||
:hint "components v1 is deprecated"))
|
||||
|
||||
(let [not-supported (-> file-features
|
||||
(set/difference enabled-features)
|
||||
(set/difference backend-only-features)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -26,11 +26,10 @@
|
||||
[app.common.types.pages-list :as ctpl]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.types.shape-tree :as ctst]
|
||||
[app.common.types.token :as cto]
|
||||
[app.common.types.token-theme :as ctot]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.common.types.typographies-list :as ctyl]
|
||||
[app.common.types.typography :as ctt]
|
||||
[app.common.types.variant :as ctv]
|
||||
[app.common.uuid :as uuid]
|
||||
[clojure.set :as set]))
|
||||
|
||||
@@ -48,14 +47,14 @@
|
||||
[:type [:= :assign]]
|
||||
;; NOTE: the full decoding is happening on the handler because it
|
||||
;; needs a proper context of the current shape and its type
|
||||
[:value [:map-of :keyword :any]]
|
||||
[:value [:map-of :keyword ::sm/any]]
|
||||
[:ignore-touched {:optional true} :boolean]
|
||||
[:ignore-geometry {:optional true} :boolean]]]
|
||||
[:set
|
||||
[:map {:title "SetOperation"}
|
||||
[:type [:= :set]]
|
||||
[:attr :keyword]
|
||||
[:val :any]
|
||||
[:val ::sm/any]
|
||||
[:ignore-touched {:optional true} :boolean]
|
||||
[:ignore-geometry {:optional true} :boolean]]]
|
||||
[:set-touched
|
||||
@@ -239,9 +238,9 @@
|
||||
[:component-id {:optional true} ::sm/uuid]
|
||||
[:ignore-touched {:optional true} :boolean]
|
||||
[:parent-id ::sm/uuid]
|
||||
[:shapes :any]
|
||||
[:shapes ::sm/any]
|
||||
[:index {:optional true} [:maybe :int]]
|
||||
[:after-shape {:optional true} :any]
|
||||
[:after-shape {:optional true} ::sm/any]
|
||||
[:component-swap {:optional true} :boolean]]]
|
||||
|
||||
[:reorder-children
|
||||
@@ -251,14 +250,14 @@
|
||||
[:component-id {:optional true} ::sm/uuid]
|
||||
[:ignore-touched {:optional true} :boolean]
|
||||
[:parent-id ::sm/uuid]
|
||||
[:shapes :any]]]
|
||||
[:shapes ::sm/any]]]
|
||||
|
||||
[:add-page
|
||||
[:map {:title "AddPageChange"}
|
||||
[:type [:= :add-page]]
|
||||
[:id {:optional true} ::sm/uuid]
|
||||
[:name {:optional true} :string]
|
||||
[:page {:optional true} :any]]]
|
||||
[:page {:optional true} ::sm/any]]]
|
||||
|
||||
[:mod-page
|
||||
[:map {:title "ModPageChange"}
|
||||
@@ -311,12 +310,12 @@
|
||||
[:add-media
|
||||
[:map {:title "AddMediaChange"}
|
||||
[:type [:= :add-media]]
|
||||
[:object ::ctf/media-object]]]
|
||||
[:object ctf/schema:media]]]
|
||||
|
||||
[:mod-media
|
||||
[:map {:title "ModMediaChange"}
|
||||
[:type [:= :mod-media]]
|
||||
[:object ::ctf/media-object]]]
|
||||
[:object ctf/schema:media]]]
|
||||
|
||||
[:del-media
|
||||
[:map {:title "DelMediaChange"}
|
||||
@@ -328,21 +327,25 @@
|
||||
[:type [:= :add-component]]
|
||||
[:id ::sm/uuid]
|
||||
[:name :string]
|
||||
[:shapes {:optional true} [:vector {:gen/max 3} :any]]
|
||||
[:shapes {:optional true} [:vector {:gen/max 3} ::sm/any]]
|
||||
[:path {:optional true} :string]]]
|
||||
|
||||
[:mod-component
|
||||
[:map {:title "ModCompoenentChange"}
|
||||
[:type [:= :mod-component]]
|
||||
[:id ::sm/uuid]
|
||||
[:shapes {:optional true} [:vector {:gen/max 3} :any]]
|
||||
[:name {:optional true} :string]]]
|
||||
[:shapes {:optional true} [:vector {:gen/max 3} ::sm/any]]
|
||||
[:name {:optional true} :string]
|
||||
[:variant-id {:optional true} ::sm/uuid]
|
||||
[:variant-properties {:optional true} [:vector ::ctv/variant-property]]]]
|
||||
|
||||
[:del-component
|
||||
[:map {:title "DelComponentChange"}
|
||||
[:type [:= :del-component]]
|
||||
[:id ::sm/uuid]
|
||||
[:main-instance {:optional true} :any]
|
||||
;; when it is an undo of a cut-paste, we need to undo the movement
|
||||
;; of the shapes so we need to move them delta
|
||||
[:delta {:optional true} ::gpt/point]
|
||||
[:skip-undelete? {:optional true} :boolean]]]
|
||||
|
||||
[:restore-component
|
||||
@@ -403,26 +406,31 @@
|
||||
[:type [:= :set-token-theme]]
|
||||
[:theme-name :string]
|
||||
[:group :string]
|
||||
[:theme [:maybe ::ctot/token-theme]]]]
|
||||
[:theme [:maybe ctob/schema:token-theme-attrs]]]]
|
||||
|
||||
[:set-tokens-lib
|
||||
[:map {:title "SetTokensLib"}
|
||||
[:type [:= :set-tokens-lib]]
|
||||
[:tokens-lib :any]]]
|
||||
[:tokens-lib ::sm/any]]]
|
||||
|
||||
[:set-token-set
|
||||
[:map {:title "SetTokenSetChange"}
|
||||
[:type [:= :set-token-set]]
|
||||
[:set-name :string]
|
||||
[:group? :boolean]
|
||||
[:token-set [:maybe ::ctot/token-set]]]]
|
||||
[:token-set [:maybe ctob/schema:token-set-attrs]]]]
|
||||
|
||||
[:set-token
|
||||
[:map {:title "SetTokenChange"}
|
||||
[:type [:= :set-token]]
|
||||
[:set-name :string]
|
||||
[:token-name :string]
|
||||
[:token [:maybe ::cto/token]]]]]])
|
||||
[:token [:maybe ctob/schema:token-attrs]]]]
|
||||
|
||||
[:set-base-font-size
|
||||
[:map {:title "ModBaseFontSize"}
|
||||
[:type [:= :set-base-font-size]]
|
||||
[:base-font-size :string]]]]])
|
||||
|
||||
(def schema:changes
|
||||
[:sequential {:gen/max 5 :gen/min 1} schema:change])
|
||||
@@ -729,20 +737,22 @@
|
||||
|
||||
(update-group [group objects]
|
||||
(let [lookup (d/getf objects)
|
||||
children (->> group :shapes (map lookup))]
|
||||
children (get group :shapes)]
|
||||
(cond
|
||||
;; If the group is empty we don't make any changes. Will be removed by a later process
|
||||
(empty? children)
|
||||
group
|
||||
|
||||
(= :bool (:type group))
|
||||
(gsh/update-bool-selrect group children objects)
|
||||
(gsh/update-bool group objects)
|
||||
|
||||
(:masked-group group)
|
||||
(set-mask-selrect group children)
|
||||
(->> (map lookup children)
|
||||
(set-mask-selrect group))
|
||||
|
||||
:else
|
||||
(gsh/update-group-selrect group children))))]
|
||||
(->> (map lookup children)
|
||||
(gsh/update-group-selrect group)))))]
|
||||
|
||||
(if page-id
|
||||
(d/update-in-when data [:pages-index page-id :objects] reg-objects)
|
||||
@@ -956,8 +966,8 @@
|
||||
(ctkl/mod-component data params))
|
||||
|
||||
(defmethod process-change :del-component
|
||||
[data {:keys [id skip-undelete? main-instance]}]
|
||||
(ctf/delete-component data id skip-undelete? main-instance))
|
||||
[data {:keys [id skip-undelete? delta]}]
|
||||
(ctf/delete-component data id skip-undelete? delta))
|
||||
|
||||
(defmethod process-change :restore-component
|
||||
[data {:keys [id page-id]}]
|
||||
@@ -1063,6 +1073,13 @@
|
||||
(ctob/ensure-tokens-lib)
|
||||
(ctob/move-set-group from-path to-path before-path before-group))))
|
||||
|
||||
;; === Base font size
|
||||
|
||||
(defmethod process-change :set-base-font-size
|
||||
[data {:keys [base-font-size]}]
|
||||
(ctf/set-base-font-size data base-font-size))
|
||||
|
||||
|
||||
;; === Operations
|
||||
|
||||
(def ^:private decode-shape
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.features :as cfeat]
|
||||
[app.common.files.changes :as cfc]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
@@ -25,18 +24,19 @@
|
||||
|
||||
;; Auxiliary functions to help create a set of changes (undo + redo)
|
||||
|
||||
(sm/register!
|
||||
^{::sm/type ::changes}
|
||||
[:map {:title "changes"}
|
||||
[:redo-changes vector?]
|
||||
[:undo-changes seq?]
|
||||
[:origin {:optional true} any?]
|
||||
[:save-undo? {:optional true} boolean?]
|
||||
[:stack-undo? {:optional true} boolean?]
|
||||
[:undo-group {:optional true} any?]])
|
||||
(def schema:changes
|
||||
(sm/register!
|
||||
^{::sm/type ::changes}
|
||||
[:map {:title "changes"}
|
||||
[:redo-changes vector?]
|
||||
[:undo-changes seq?]
|
||||
[:origin {:optional true} ::sm/any]
|
||||
[:save-undo? {:optional true} boolean?]
|
||||
[:stack-undo? {:optional true} boolean?]
|
||||
[:undo-group {:optional true} ::sm/any]]))
|
||||
|
||||
(def check-changes!
|
||||
(sm/check-fn ::changes))
|
||||
(sm/check-fn schema:changes))
|
||||
|
||||
(defn empty-changes
|
||||
([origin page-id]
|
||||
@@ -84,8 +84,7 @@
|
||||
|
||||
(defn with-objects
|
||||
[changes objects]
|
||||
(let [fdata (binding [cfeat/*current* #{"components/v2"}]
|
||||
(ctf/make-file-data (uuid/next) uuid/zero))
|
||||
(let [fdata (ctf/make-file-data (uuid/next) uuid/zero)
|
||||
fdata (assoc-in fdata [:pages-index uuid/zero :objects] objects)]
|
||||
(vary-meta changes assoc
|
||||
::file-data fdata
|
||||
@@ -126,28 +125,41 @@
|
||||
; TODO: remove this when not needed
|
||||
(defn- assert-page-id!
|
||||
[changes]
|
||||
(dm/assert!
|
||||
"Give a page-id or call (with-page) before using this function"
|
||||
(contains? (meta changes) ::page-id)))
|
||||
(assert
|
||||
(contains? (meta changes) ::page-id)
|
||||
"Give a page-id or call (with-page) before using this function"))
|
||||
|
||||
(defn- assert-page!
|
||||
[changes]
|
||||
(assert
|
||||
(contains? (meta changes) ::page)
|
||||
"Give a page or call (with-page) before using this function"))
|
||||
|
||||
(defn- assert-container-id!
|
||||
[changes]
|
||||
(dm/assert!
|
||||
"Give a page-id or call (with-container) before using this function"
|
||||
(assert
|
||||
(or (contains? (meta changes) ::page-id)
|
||||
(contains? (meta changes) ::component-id))))
|
||||
(contains? (meta changes) ::component-id))
|
||||
"Give a page-id or call (with-container) before using this function"))
|
||||
|
||||
(defn- assert-objects!
|
||||
[changes]
|
||||
(dm/assert!
|
||||
"Call (with-objects) before using this function"
|
||||
(contains? (meta changes) ::file-data)))
|
||||
(assert
|
||||
(contains? (meta changes) ::file-data)
|
||||
"Call (with-objects) before using this function"))
|
||||
|
||||
(defn- assert-library!
|
||||
[changes]
|
||||
(dm/assert!
|
||||
"Call (with-library-data) before using this function"
|
||||
(contains? (meta changes) ::library-data)))
|
||||
(assert
|
||||
(contains? (meta changes) ::library-data)
|
||||
"Call (with-library-data) before using this function"))
|
||||
|
||||
(defn- assert-file-data!
|
||||
[changes]
|
||||
(assert
|
||||
(contains? (meta changes) ::file-data)
|
||||
"Call (with-file-data) before using this function"))
|
||||
|
||||
|
||||
(defn- lookup-objects
|
||||
[changes]
|
||||
@@ -156,9 +168,9 @@
|
||||
|
||||
(defn apply-changes-local
|
||||
[changes & {:keys [apply-to-library?]}]
|
||||
(dm/assert!
|
||||
"expected valid changes"
|
||||
(check-changes! changes))
|
||||
(assert
|
||||
(check-changes! changes)
|
||||
"expected valid changes")
|
||||
|
||||
(if-let [file-data (::file-data (meta changes))]
|
||||
(let [library-data (::library-data (meta changes))
|
||||
@@ -197,6 +209,7 @@
|
||||
|
||||
(defn mod-page
|
||||
([changes options]
|
||||
(assert-page! changes)
|
||||
(let [page (::page (meta changes))]
|
||||
(mod-page changes page options)))
|
||||
|
||||
@@ -227,6 +240,7 @@
|
||||
([changes type id namespace key value]
|
||||
(set-plugin-data changes type id nil namespace key value))
|
||||
([changes type id page-id namespace key value]
|
||||
(assert-file-data! changes)
|
||||
(let [data (::file-data (meta changes))
|
||||
old-val
|
||||
(case type
|
||||
@@ -293,6 +307,8 @@
|
||||
|
||||
(defn set-guide
|
||||
[changes id guide]
|
||||
(assert-page-id! changes)
|
||||
(assert-page! changes)
|
||||
(let [page-id (::page-id (meta changes))
|
||||
page (::page (meta changes))
|
||||
old-val (dm/get-in page [:guides id])]
|
||||
@@ -306,8 +322,11 @@
|
||||
:page-id page-id
|
||||
:id id
|
||||
:params old-val}))))
|
||||
|
||||
(defn set-flow
|
||||
[changes id flow]
|
||||
(assert-page-id! changes)
|
||||
(assert-page! changes)
|
||||
(let [page-id (::page-id (meta changes))
|
||||
page (::page (meta changes))
|
||||
old-val (dm/get-in page [:flows id])
|
||||
@@ -326,6 +345,8 @@
|
||||
|
||||
(defn set-comment-thread-position
|
||||
[changes {:keys [id frame-id position] :as thread}]
|
||||
(assert-page-id! changes)
|
||||
(assert-page! changes)
|
||||
(let [page-id (::page-id (meta changes))
|
||||
page (::page (meta changes))
|
||||
|
||||
@@ -347,6 +368,8 @@
|
||||
|
||||
(defn set-default-grid
|
||||
[changes type params]
|
||||
(assert-page-id! changes)
|
||||
(assert-page! changes)
|
||||
(let [page-id (::page-id (meta changes))
|
||||
page (::page (meta changes))
|
||||
old-val (dm/get-in page [:grids type])
|
||||
@@ -480,9 +503,12 @@
|
||||
(let [old-val (get old attr)
|
||||
new-val (get new attr)]
|
||||
(not= old-val new-val)))
|
||||
new-obj (if with-objects?
|
||||
(update-fn object objects)
|
||||
(update-fn object))]
|
||||
|
||||
new-obj
|
||||
(if with-objects?
|
||||
(update-fn object objects)
|
||||
(update-fn object))]
|
||||
|
||||
(when-not (= object new-obj)
|
||||
(let [attrs (or attrs (d/concat-set (keys object) (keys new-obj)))]
|
||||
(filter (partial changed? object new-obj) attrs)))))
|
||||
@@ -497,6 +523,7 @@
|
||||
:or {ignore-geometry? false ignore-touched false with-objects? false}}]
|
||||
(assert-container-id! changes)
|
||||
(assert-objects! changes)
|
||||
(assert-page-id! changes)
|
||||
(let [page-id (::page-id (meta changes))
|
||||
component-id (::component-id (meta changes))
|
||||
objects (lookup-objects changes)
|
||||
@@ -659,9 +686,13 @@
|
||||
nil ;; so it does not need resize
|
||||
|
||||
(= (:type parent) :bool)
|
||||
(gsh/update-bool-selrect parent children objects)
|
||||
(gsh/update-bool parent objects)
|
||||
|
||||
(= (:type parent) :group)
|
||||
;; FIXME: this functions should be
|
||||
;; normalized in the same way as
|
||||
;; update-bool in order to make all
|
||||
;; this code consistent
|
||||
(if (:masked-group parent)
|
||||
(gsh/update-mask-selrect parent children)
|
||||
(gsh/update-group-selrect parent children)))]
|
||||
@@ -841,6 +872,7 @@
|
||||
|
||||
(defn set-tokens-lib
|
||||
[changes tokens-lib]
|
||||
(assert-library! changes)
|
||||
(let [library-data (::library-data (meta changes))
|
||||
prev-tokens-lib (get library-data :tokens-lib)]
|
||||
(-> changes
|
||||
@@ -921,11 +953,11 @@
|
||||
(apply-changes-local))))
|
||||
|
||||
(defn add-component
|
||||
([changes id path name new-shapes updated-shapes main-instance-id main-instance-page]
|
||||
(add-component changes id path name new-shapes updated-shapes main-instance-id main-instance-page nil nil nil))
|
||||
([changes id path name new-shapes updated-shapes main-instance-id main-instance-page annotation]
|
||||
(add-component changes id path name new-shapes updated-shapes main-instance-id main-instance-page annotation nil nil))
|
||||
([changes id path name new-shapes updated-shapes main-instance-id main-instance-page annotation variant-id variant-properties & {:keys [apply-changes-local-library?]}]
|
||||
([changes id path name updated-shapes main-instance-id main-instance-page]
|
||||
(add-component changes id path name updated-shapes main-instance-id main-instance-page nil nil nil))
|
||||
([changes id path name updated-shapes main-instance-id main-instance-page annotation]
|
||||
(add-component changes id path name updated-shapes main-instance-id main-instance-page annotation nil nil))
|
||||
([changes id path name updated-shapes main-instance-id main-instance-page annotation variant-id variant-properties & {:keys [apply-changes-local-library?]}]
|
||||
(assert-page-id! changes)
|
||||
(assert-objects! changes)
|
||||
(let [page-id (::page-id (meta changes))
|
||||
@@ -964,11 +996,11 @@
|
||||
:name name
|
||||
:main-instance-id main-instance-id
|
||||
:main-instance-page main-instance-page
|
||||
:annotation annotation
|
||||
:variant-id variant-id
|
||||
:variant-properties variant-properties}
|
||||
(some? new-shapes) ;; this will be null in components-v2
|
||||
(assoc :shapes (vec new-shapes))))
|
||||
:annotation annotation}
|
||||
(some? variant-id)
|
||||
(assoc :variant-id variant-id)
|
||||
(seq variant-properties)
|
||||
(assoc :variant-properties variant-properties)))
|
||||
(into (map mk-change) updated-shapes))))
|
||||
(update :undo-changes
|
||||
(fn [undo-changes]
|
||||
@@ -991,27 +1023,39 @@
|
||||
new-component (update-fn prev-component)]
|
||||
(if prev-component
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :mod-component
|
||||
:id id
|
||||
:name (:name new-component)
|
||||
:path (:path new-component)
|
||||
:main-instance-id (:main-instance-id new-component)
|
||||
:main-instance-page (:main-instance-page new-component)
|
||||
:annotation (:annotation new-component)
|
||||
:variant-id (:variant-id new-component)
|
||||
:variant-properties (:variant-properties new-component)
|
||||
:objects (:objects new-component) ;; this won't exist in components-v2 (except for deleted components)
|
||||
:modified-at (:modified-at new-component)})
|
||||
(update :undo-changes conj {:type :mod-component
|
||||
:id id
|
||||
:name (:name prev-component)
|
||||
:path (:path prev-component)
|
||||
:main-instance-id (:main-instance-id prev-component)
|
||||
:main-instance-page (:main-instance-page prev-component)
|
||||
:annotation (:annotation prev-component)
|
||||
:variant-id (:variant-id prev-component)
|
||||
:variant-properties (:variant-properties prev-component)
|
||||
:objects (:objects prev-component)})
|
||||
(update :redo-changes conj (cond-> {:type :mod-component
|
||||
:id id
|
||||
:name (:name new-component)
|
||||
:path (:path new-component)
|
||||
:main-instance-id (:main-instance-id new-component)
|
||||
:main-instance-page (:main-instance-page new-component)
|
||||
:annotation (:annotation new-component)
|
||||
:objects (:objects new-component) ;; for deleted components
|
||||
:modified-at (:modified-at new-component)}
|
||||
(some? (:variant-id new-component))
|
||||
(assoc :variant-id (:variant-id new-component))
|
||||
(nil? (:variant-id new-component))
|
||||
(dissoc :variant-id)
|
||||
(seq (:variant-properties new-component))
|
||||
(assoc :variant-properties (:variant-properties new-component))
|
||||
(not (seq (:variant-properties new-component)))
|
||||
(dissoc :variant-properties)))
|
||||
(update :undo-changes conj (cond-> {:type :mod-component
|
||||
:id id
|
||||
:name (:name prev-component)
|
||||
:path (:path prev-component)
|
||||
:main-instance-id (:main-instance-id prev-component)
|
||||
:main-instance-page (:main-instance-page prev-component)
|
||||
:annotation (:annotation prev-component)
|
||||
:objects (:objects prev-component)}
|
||||
(some? (:variant-id prev-component))
|
||||
(assoc :variant-id (:variant-id prev-component))
|
||||
(nil? (:variant-id prev-component))
|
||||
(dissoc :variant-id)
|
||||
(seq (:variant-properties prev-component))
|
||||
(assoc :variant-properties (:variant-properties prev-component))
|
||||
(not (seq (:variant-properties prev-component)))
|
||||
(dissoc :variant-properties)))
|
||||
(cond-> apply-changes-local-library?
|
||||
(apply-changes-local {:apply-to-library? true})))
|
||||
changes)))
|
||||
@@ -1027,7 +1071,7 @@
|
||||
:page-id page-id})))
|
||||
|
||||
(defn restore-component
|
||||
[changes id page-id main-instance]
|
||||
[changes id page-id delta]
|
||||
(assert-library! changes)
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :restore-component
|
||||
@@ -1035,7 +1079,34 @@
|
||||
:page-id page-id})
|
||||
(update :undo-changes conj {:type :del-component
|
||||
:id id
|
||||
:main-instance main-instance})))
|
||||
:delta delta})))
|
||||
|
||||
(defn reorder-children
|
||||
[changes id children]
|
||||
(assert-page-id! changes)
|
||||
(assert-objects! changes)
|
||||
|
||||
(let [page-id (::page-id (meta changes))
|
||||
objects (lookup-objects changes)
|
||||
shape (get objects id)
|
||||
old-children (:shapes shape)
|
||||
|
||||
redo-change
|
||||
{:type :reorder-children
|
||||
:parent-id (:id shape)
|
||||
:page-id page-id
|
||||
:shapes children}
|
||||
|
||||
undo-change
|
||||
{:type :reorder-children
|
||||
:parent-id (:id shape)
|
||||
:page-id page-id
|
||||
:shapes old-children}]
|
||||
|
||||
(-> changes
|
||||
(update :redo-changes conj redo-change)
|
||||
(update :undo-changes conj undo-change)
|
||||
(apply-changes-local))))
|
||||
|
||||
(defn reorder-grid-children
|
||||
[changes ids]
|
||||
@@ -1083,3 +1154,24 @@
|
||||
(defn get-objects
|
||||
[changes]
|
||||
(dm/get-in (::file-data (meta changes)) [:pages-index uuid/zero :objects]))
|
||||
|
||||
(defn get-page
|
||||
[changes]
|
||||
(::page (meta changes)))
|
||||
|
||||
(defn get-page-id
|
||||
[changes]
|
||||
(::page-id (meta changes)))
|
||||
|
||||
(defn set-base-font-size
|
||||
[changes new-base-font-size]
|
||||
(assert-file-data! changes)
|
||||
(let [file-data (::file-data (meta changes))
|
||||
previous-font-size (ctf/get-base-font-size file-data)]
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :set-base-font-size
|
||||
:base-font-size new-base-font-size})
|
||||
|
||||
(update :undo-changes conj {:type :set-base-font-size
|
||||
:base-font-size previous-font-size})
|
||||
(apply-changes-local))))
|
||||
@@ -427,11 +427,6 @@
|
||||
(map #(str/concat base-name (suffix-fn %))
|
||||
(iterate inc 1))))
|
||||
|
||||
(defn ^:private get-suffix
|
||||
"Default suffix impelemtation"
|
||||
[copy-count]
|
||||
(str/concat " " copy-count))
|
||||
|
||||
(defn generate-unique-name
|
||||
"Generates a unique name by selecting the first available name from a generated sequence.
|
||||
The sequence consists of `base-name` and its variants, avoiding conflicts with `existing-names`.
|
||||
@@ -445,8 +440,7 @@
|
||||
|
||||
Returns:
|
||||
- A unique name not present in `existing-names`."
|
||||
[base-name existing-names & {:keys [suffix-fn immediate-suffix?]
|
||||
:or {suffix-fn get-suffix}}]
|
||||
[base-name existing-names & {:keys [suffix-fn immediate-suffix? suffix]}]
|
||||
(dm/assert!
|
||||
"expected a set of strings"
|
||||
(coll? existing-names))
|
||||
@@ -454,9 +448,21 @@
|
||||
(dm/assert!
|
||||
"expected a string for `basename`."
|
||||
(string? base-name))
|
||||
(let [existing-name-set (cond-> (set existing-names)
|
||||
(let [suffix-fn (if suffix-fn
|
||||
suffix-fn
|
||||
(if suffix
|
||||
(fn [copy-count]
|
||||
(str/concat "-"
|
||||
suffix
|
||||
(when (> copy-count 1)
|
||||
(str "-" copy-count))))
|
||||
(fn [copy-count]
|
||||
(str/concat " " copy-count))))
|
||||
|
||||
existing-name-set (cond-> (set existing-names)
|
||||
immediate-suffix? (conj base-name))
|
||||
names (name-seq base-name suffix-fn)]
|
||||
|
||||
(->> names
|
||||
(remove #(contains? existing-name-set %))
|
||||
first)))
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.rect :as grc]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.geom.shapes.path :as gsp]
|
||||
[app.common.geom.shapes.text :as gsht]
|
||||
[app.common.logging :as l]
|
||||
[app.common.math :as mth]
|
||||
@@ -27,7 +26,10 @@
|
||||
[app.common.types.component :as ctk]
|
||||
[app.common.types.container :as ctn]
|
||||
[app.common.types.file :as ctf]
|
||||
[app.common.types.path :as path]
|
||||
[app.common.types.path.segment :as path.segment]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.types.shape.interactions :as ctsi]
|
||||
[app.common.types.shape.shadow :as ctss]
|
||||
[app.common.uuid :as uuid]
|
||||
[clojure.set :as set]
|
||||
@@ -35,9 +37,7 @@
|
||||
|
||||
#?(:cljs (l/set-level! :info))
|
||||
|
||||
(declare ^:private available-migrations)
|
||||
(declare ^:private migration-up-index)
|
||||
(declare ^:private migration-down-index)
|
||||
(declare available-migrations)
|
||||
|
||||
(def version cfd/version)
|
||||
|
||||
@@ -49,7 +49,10 @@
|
||||
[file]
|
||||
(or (nil? (:version file))
|
||||
(not= cfd/version (:version file))
|
||||
(not= available-migrations (:migrations file))))
|
||||
(boolean
|
||||
(->> (:migrations file #{})
|
||||
(set/difference available-migrations)
|
||||
(not-empty)))))
|
||||
|
||||
(def xf:map-name
|
||||
(map :name))
|
||||
@@ -96,13 +99,13 @@
|
||||
(if (nil? migrations)
|
||||
(generate-migrations-from-version version)
|
||||
migrations)))
|
||||
(update :features (fnil into #{}) (deref cfeat/*new*))
|
||||
;; NOTE: in some future we can consider to apply
|
||||
;; a migration to the whole database and remove
|
||||
;; this code from this function that executes on
|
||||
;; each file migration operation
|
||||
(update :features cfeat/migrate-legacy-features)
|
||||
(migrate)))))
|
||||
(migrate)
|
||||
(update :features (fnil into #{}) (deref cfeat/*new*))))))
|
||||
|
||||
(defn migrated?
|
||||
[file]
|
||||
@@ -119,16 +122,16 @@
|
||||
(into [] shapes)
|
||||
shapes))))
|
||||
(update-page [page]
|
||||
(update page :objects update-vals update-object))]
|
||||
(update page :objects d/update-vals update-object))]
|
||||
|
||||
(update data :pages-index update-vals update-page)))
|
||||
(update data :pages-index d/update-vals update-page)))
|
||||
|
||||
(defmethod migrate-data "legacy-3"
|
||||
[data _]
|
||||
(letfn [(migrate-path [shape]
|
||||
(if-not (contains? shape :content)
|
||||
(let [content (gsp/segments->content (:segments shape) (:close? shape))
|
||||
selrect (gsh/content->selrect content)
|
||||
(let [content (path.segment/points->content (:segments shape) :close (:close? shape))
|
||||
selrect (path.segment/content->selrect content)
|
||||
points (grc/rect->points selrect)]
|
||||
(-> shape
|
||||
(dissoc :segments)
|
||||
@@ -172,9 +175,9 @@
|
||||
(fix-empty-points)))
|
||||
|
||||
(update-page [page]
|
||||
(update page :objects update-vals update-object))]
|
||||
(update page :objects d/update-vals update-object))]
|
||||
|
||||
(update data :pages-index update-vals update-page)))
|
||||
(update data :pages-index d/update-vals update-page)))
|
||||
|
||||
;; Put the id of the local file in :component-file in instances of
|
||||
;; local components
|
||||
@@ -187,9 +190,9 @@
|
||||
object))
|
||||
|
||||
(update-page [page]
|
||||
(update page :objects update-vals update-object))]
|
||||
(update page :objects d/update-vals update-object))]
|
||||
|
||||
(update data :pages-index update-vals update-page)))
|
||||
(update data :pages-index d/update-vals update-page)))
|
||||
|
||||
;; Fixes issues with selrect/points for shapes with width/height =
|
||||
;; 0 (line-like paths)
|
||||
@@ -199,7 +202,7 @@
|
||||
(if (= (:type shape) :path)
|
||||
(let [{:keys [width height]} (grc/points->rect (:points shape))]
|
||||
(if (or (mth/almost-zero? width) (mth/almost-zero? height))
|
||||
(let [selrect (gsh/content->selrect (:content shape))
|
||||
(let [selrect (path.segment/content->selrect (:content shape))
|
||||
points (grc/rect->points selrect)
|
||||
transform (gmt/matrix)
|
||||
transform-inv (gmt/matrix)]
|
||||
@@ -212,11 +215,11 @@
|
||||
shape))
|
||||
|
||||
(update-container [container]
|
||||
(update container :objects update-vals fix-line-paths))]
|
||||
(update container :objects d/update-vals fix-line-paths))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
;; Remove interactions pointing to deleted frames
|
||||
(defmethod migrate-data "legacy-7"
|
||||
@@ -227,9 +230,9 @@
|
||||
(filterv #(get-in page [:objects (:destination %)]) interactions))))
|
||||
|
||||
(update-page [page]
|
||||
(update page :objects update-vals (partial update-object page)))]
|
||||
(update page :objects d/update-vals (partial update-object page)))]
|
||||
|
||||
(update data :pages-index update-vals update-page)))
|
||||
(update data :pages-index d/update-vals update-page)))
|
||||
|
||||
;; Remove groups without any shape, both in pages and components
|
||||
(defmethod migrate-data "legacy-8"
|
||||
@@ -269,8 +272,8 @@
|
||||
(assoc container :objects objects)))))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals clean-container)
|
||||
(update :components update-vals clean-container))))
|
||||
(update :pages-index d/update-vals clean-container)
|
||||
(d/update-when :components d/update-vals clean-container))))
|
||||
|
||||
(defmethod migrate-data "legacy-9"
|
||||
[data _]
|
||||
@@ -304,7 +307,7 @@
|
||||
[data _]
|
||||
(letfn [(update-page [page]
|
||||
(d/update-in-when page [:objects uuid/zero] dissoc :points :selrect))]
|
||||
(update data :pages-index update-vals update-page)))
|
||||
(update data :pages-index d/update-vals update-page)))
|
||||
|
||||
(defmethod migrate-data "legacy-11"
|
||||
[data _]
|
||||
@@ -318,7 +321,7 @@
|
||||
(update page :objects (fn [objects]
|
||||
(update-vals objects (partial update-object objects)))))]
|
||||
|
||||
(update data :pages-index update-vals update-page)))
|
||||
(update data :pages-index d/update-vals update-page)))
|
||||
|
||||
(defmethod migrate-data "legacy-12"
|
||||
[data _]
|
||||
@@ -328,9 +331,9 @@
|
||||
(assoc :size nil)))
|
||||
|
||||
(update-page [page]
|
||||
(d/update-in-when page [:options :saved-grids] update-vals update-grid))]
|
||||
(d/update-in-when page [:options :saved-grids] d/update-vals update-grid))]
|
||||
|
||||
(update data :pages-index update-vals update-page)))
|
||||
(update data :pages-index d/update-vals update-page)))
|
||||
|
||||
;; Add rx and ry to images
|
||||
(defmethod migrate-data "legacy-13"
|
||||
@@ -348,9 +351,9 @@
|
||||
(fix-radius)))
|
||||
|
||||
(update-page [page]
|
||||
(update page :objects update-vals update-object))]
|
||||
(update page :objects d/update-vals update-object))]
|
||||
|
||||
(update data :pages-index update-vals update-page)))
|
||||
(update data :pages-index d/update-vals update-page)))
|
||||
|
||||
(defmethod migrate-data "legacy-14"
|
||||
[data _]
|
||||
@@ -380,8 +383,8 @@
|
||||
container))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "legacy-16"
|
||||
[data _]
|
||||
@@ -423,11 +426,11 @@
|
||||
(assign-fills)))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-object))]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "legacy-17"
|
||||
[data _]
|
||||
@@ -452,11 +455,11 @@
|
||||
(assoc :fills [])))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-object))]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
;; Remove position-data to solve a bug with the text positioning
|
||||
(defmethod migrate-data "legacy-18"
|
||||
@@ -467,11 +470,11 @@
|
||||
(dissoc :position-data)))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-object))]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "legacy-19"
|
||||
[data _]
|
||||
@@ -483,11 +486,11 @@
|
||||
(dissoc :position-data)))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-object))]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "legacy-25"
|
||||
[data _]
|
||||
@@ -499,10 +502,10 @@
|
||||
(update :selrect grc/make-rect)
|
||||
(cts/create-shape))))
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-object))]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "legacy-26"
|
||||
[data _]
|
||||
@@ -515,11 +518,11 @@
|
||||
(assoc :transform-inverse (gmt/matrix))))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-object))]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "legacy-27"
|
||||
[data _]
|
||||
@@ -546,11 +549,11 @@
|
||||
(dissoc :saved-component-root?))))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-object))]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "legacy-28"
|
||||
[data _]
|
||||
@@ -575,8 +578,8 @@
|
||||
(d/update-when container :objects #(update-vals % (partial update-object %))))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "legacy-29"
|
||||
[data _]
|
||||
@@ -607,11 +610,11 @@
|
||||
(update :content #(txt/transform-nodes invalid-node? fix-node %)))))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-object))]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "legacy-31"
|
||||
[data _]
|
||||
@@ -622,10 +625,10 @@
|
||||
(dissoc :use-for-thumbnail?))))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-object))]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "legacy-32"
|
||||
[data _]
|
||||
@@ -640,11 +643,11 @@
|
||||
object)))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-object))]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "legacy-33"
|
||||
[data _]
|
||||
@@ -662,9 +665,9 @@
|
||||
object))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-object))]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "legacy-34"
|
||||
[data _]
|
||||
@@ -674,10 +677,10 @@
|
||||
(dissoc object :x :y :width :height)
|
||||
object))
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-object))]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "legacy-36"
|
||||
[data _]
|
||||
@@ -687,8 +690,8 @@
|
||||
(dissoc objects nil)
|
||||
objects))))]
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "legacy-37"
|
||||
[data _]
|
||||
@@ -716,11 +719,11 @@
|
||||
shape)))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-shape))]
|
||||
(d/update-when container :objects d/update-vals update-shape))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "legacy-39"
|
||||
[data _]
|
||||
@@ -738,11 +741,11 @@
|
||||
shape))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-shape))]
|
||||
(d/update-when container :objects d/update-vals update-shape))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "legacy-40"
|
||||
[data _]
|
||||
@@ -762,11 +765,11 @@
|
||||
shape))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-shape))]
|
||||
(d/update-when container :objects d/update-vals update-shape))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "legacy-41"
|
||||
[data _]
|
||||
@@ -795,11 +798,11 @@
|
||||
shape))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-shape))]
|
||||
(d/update-when container :objects d/update-vals update-shape))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "legacy-42"
|
||||
[data _]
|
||||
@@ -812,11 +815,11 @@
|
||||
object))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-object))]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(def ^:private valid-fill?
|
||||
(sm/lazy-validator ::cts/fill))
|
||||
@@ -841,14 +844,11 @@
|
||||
object))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-object))]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
|
||||
(def ^:private valid-shadow?
|
||||
(sm/lazy-validator ::ctss/shadow))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "legacy-44"
|
||||
[data _]
|
||||
@@ -861,14 +861,14 @@
|
||||
|
||||
(update-object [object]
|
||||
(let [xform (comp (map fix-shadow)
|
||||
(filter valid-shadow?))]
|
||||
(filter ctss/valid-shadow?))]
|
||||
(d/update-when object :shadow #(into [] xform %))))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-object))]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "legacy-45"
|
||||
[data _]
|
||||
@@ -881,9 +881,9 @@
|
||||
:parent-id parent-id)))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals fix-shape))]
|
||||
(d/update-when container :objects d/update-vals fix-shape))]
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "legacy-46"
|
||||
[data _]
|
||||
@@ -891,10 +891,10 @@
|
||||
(dissoc object :thumbnail))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-object))]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "legacy-47"
|
||||
[data _]
|
||||
@@ -915,9 +915,9 @@
|
||||
shape)))
|
||||
|
||||
(update-page [page]
|
||||
(d/update-when page :objects update-vals (partial fix-shape page)))]
|
||||
(d/update-when page :objects d/update-vals (partial fix-shape page)))]
|
||||
(-> data
|
||||
(update :pages-index update-vals update-page))))
|
||||
(update :pages-index d/update-vals update-page))))
|
||||
|
||||
(defmethod migrate-data "legacy-48"
|
||||
[data _]
|
||||
@@ -929,9 +929,9 @@
|
||||
shape)))
|
||||
|
||||
(update-page [page]
|
||||
(d/update-when page :objects update-vals fix-shape))]
|
||||
(d/update-when page :objects d/update-vals fix-shape))]
|
||||
(-> data
|
||||
(update :pages-index update-vals update-page))))
|
||||
(update :pages-index d/update-vals update-page))))
|
||||
|
||||
;; Remove hide-in-viewer for shapes that are origin or destination of an interaction
|
||||
(defmethod migrate-data "legacy-49"
|
||||
@@ -949,9 +949,9 @@
|
||||
(mapcat :interactions)
|
||||
(map :destination)
|
||||
(set))]
|
||||
(update page :objects update-vals (partial update-object destinations))))]
|
||||
(update page :objects d/update-vals (partial update-object destinations))))]
|
||||
|
||||
(update data :pages-index update-vals update-page)))
|
||||
(update data :pages-index d/update-vals update-page)))
|
||||
|
||||
;; This migration mainly fixes paths with curve-to segments
|
||||
;; without :c1x :c1y :c2x :c2y properties. Additionally, we found a
|
||||
@@ -994,11 +994,11 @@
|
||||
|
||||
update-container
|
||||
(fn [page]
|
||||
(d/update-when page :objects update-vals update-shape))]
|
||||
(d/update-when page :objects d/update-vals update-shape))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(def ^:private valid-color?
|
||||
(sm/lazy-validator ::ctc/color))
|
||||
@@ -1018,9 +1018,9 @@
|
||||
shape))
|
||||
|
||||
(update-page [page]
|
||||
(d/update-when page :objects update-vals update-shape))]
|
||||
(d/update-when page :objects d/update-vals update-shape))]
|
||||
|
||||
(update data :pages-index update-vals update-page)))
|
||||
(update data :pages-index d/update-vals update-page)))
|
||||
|
||||
|
||||
(defmethod migrate-data "legacy-53"
|
||||
@@ -1036,15 +1036,15 @@
|
||||
|
||||
(update-shape [shape]
|
||||
(let [xform (comp (map fix-shadow)
|
||||
(filter valid-shadow?))]
|
||||
(filter ctss/valid-shadow?))]
|
||||
(d/update-when shape :shadow #(into [] xform %))))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-shape))]
|
||||
(d/update-when container :objects d/update-vals update-shape))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
;; This migration moves page options to the page level
|
||||
(defmethod migrate-data "legacy-55"
|
||||
@@ -1096,11 +1096,11 @@
|
||||
(update :content (partial txt/transform-nodes identity fix-fills)))))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-object))]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
|
||||
(defmethod migrate-data "legacy-57"
|
||||
@@ -1127,7 +1127,7 @@
|
||||
(-> data
|
||||
(update :pages (fn [pages] (into [] (remove nil?) pages)))
|
||||
(update :pages-index dissoc nil)
|
||||
(update :pages-index update-vals update-page))))
|
||||
(update :pages-index d/update-vals update-page))))
|
||||
|
||||
(defmethod migrate-data "legacy-59"
|
||||
[data _]
|
||||
@@ -1138,11 +1138,11 @@
|
||||
(d/update-when shape :touched #(into #{} (map fix-touched) %)))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-shape))]
|
||||
(d/update-when container :objects d/update-vals update-shape))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "legacy-62"
|
||||
[data _]
|
||||
@@ -1175,7 +1175,7 @@
|
||||
;; so the relevant objects are inside the component
|
||||
(d/update-when component :objects remove-cycles))]
|
||||
|
||||
(update data :components update-vals update-component)))
|
||||
(d/update-when data :components d/update-vals update-component)))
|
||||
|
||||
(defmethod migrate-data "legacy-65"
|
||||
[data _]
|
||||
@@ -1186,14 +1186,14 @@
|
||||
update-page
|
||||
(fn [page]
|
||||
(-> (update-object page)
|
||||
(update :objects update-vals update-object)))]
|
||||
(update :objects d/update-vals update-object)))]
|
||||
|
||||
(-> data
|
||||
(update-object)
|
||||
(d/update-when :pages-index update-vals update-page)
|
||||
(d/update-when :colors update-vals update-object)
|
||||
(d/update-when :typographies update-vals update-object)
|
||||
(d/update-when :components update-vals update-object))))
|
||||
(update :pages-index d/update-vals update-page)
|
||||
(d/update-when :colors d/update-vals update-object)
|
||||
(d/update-when :typographies d/update-vals update-object)
|
||||
(d/update-when :components d/update-vals update-object))))
|
||||
|
||||
(defmethod migrate-data "legacy-66"
|
||||
[data _]
|
||||
@@ -1207,11 +1207,11 @@
|
||||
object))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-object))]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "legacy-67"
|
||||
[data _]
|
||||
@@ -1219,11 +1219,11 @@
|
||||
(d/update-when object :shadow #(into [] (reverse %))))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-object))]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "0001-remove-tokens-from-groups"
|
||||
[data _]
|
||||
@@ -1237,8 +1237,92 @@
|
||||
(dissoc :applied-tokens)))
|
||||
|
||||
(update-page [page]
|
||||
(d/update-when page :objects update-vals update-object))]
|
||||
(update data :pages-index update-vals update-page)))
|
||||
(d/update-when page :objects d/update-vals update-object))]
|
||||
|
||||
(update data :pages-index d/update-vals update-page)))
|
||||
|
||||
(defmethod migrate-data "0002-clean-shape-interactions"
|
||||
[data _]
|
||||
(let [decode-fn (sm/decoder ctsi/schema:interaction sm/json-transformer)
|
||||
validate-fn (sm/validator ctsi/schema:interaction)
|
||||
|
||||
xform
|
||||
(comp
|
||||
(map decode-fn)
|
||||
(filter validate-fn))
|
||||
|
||||
update-object
|
||||
(fn [object]
|
||||
(d/update-when object :interactions
|
||||
(fn [interactions]
|
||||
(into [] xform interactions))))
|
||||
|
||||
update-container
|
||||
(fn [container]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "0002-normalize-bool-content"
|
||||
[data _]
|
||||
(letfn [(update-object [object]
|
||||
;; NOTE: we still preserve the previous value for possible
|
||||
;; rollback, we still need to perform an other migration
|
||||
;; for properly delete the bool-content prop from shapes
|
||||
;; once the know the migration was OK
|
||||
(if (cfh/bool-shape? object)
|
||||
(if-let [content (:bool-content object)]
|
||||
(assoc object :content content)
|
||||
object)
|
||||
(dissoc object :bool-content :bool-type)))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-object))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "0003-fix-root-shape"
|
||||
[data _]
|
||||
(letfn [(update-object [shape]
|
||||
(if (= (:id shape) uuid/zero)
|
||||
(-> shape
|
||||
(assoc :parent-id uuid/zero)
|
||||
(assoc :frame-id uuid/zero)
|
||||
;; We explicitly dissoc them and let the shape-setup
|
||||
;; to regenerate it with valid values.
|
||||
(dissoc :selrect)
|
||||
(dissoc :points)
|
||||
(cts/setup-shape))
|
||||
shape))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container)
|
||||
(d/without-nils))))
|
||||
|
||||
(defmethod migrate-data "0003-convert-path-content"
|
||||
[data _]
|
||||
(some-> cfeat/*new* (swap! conj "fdata/path-data"))
|
||||
|
||||
(letfn [(update-object [object]
|
||||
(if (or (cfh/bool-shape? object)
|
||||
(cfh/path-shape? object))
|
||||
(update object :content path/content)
|
||||
object))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-object))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(def available-migrations
|
||||
(into (d/ordered-set)
|
||||
@@ -1294,4 +1378,8 @@
|
||||
"legacy-65"
|
||||
"legacy-66"
|
||||
"legacy-67"
|
||||
"0001-remove-tokens-from-groups"]))
|
||||
"0001-remove-tokens-from-groups"
|
||||
"0002-normalize-bool-content"
|
||||
"0002-clean-shape-interactions"
|
||||
"0003-fix-root-shape"
|
||||
"0003-convert-path-content"]))
|
||||
|
||||
@@ -572,6 +572,51 @@
|
||||
(pcb/with-file-data file-data)
|
||||
(pcb/update-shapes [(:id shape)] repair-shape))))
|
||||
|
||||
(defmethod repair-error :not-a-variant
|
||||
[_ error file _]
|
||||
(log/error :hint "Variant error code, we don't want to auto repair it for now" :code (:code error))
|
||||
file)
|
||||
|
||||
(defmethod repair-error :invalid-variant-id
|
||||
[_ error file _]
|
||||
(log/error :hint "Variant error code, we don't want to auto repair it for now" :code (:code error))
|
||||
file)
|
||||
|
||||
(defmethod repair-error :invalid-variant-properties
|
||||
[_ error file _]
|
||||
(log/error :hint "Variant error code, we don't want to auto repair it for now" :code (:code error))
|
||||
file)
|
||||
|
||||
(defmethod repair-error :variant-not-main
|
||||
[_ error file _]
|
||||
(log/error :hint "Variant error code, we don't want to auto repair it for now" :code (:code error))
|
||||
file)
|
||||
|
||||
(defmethod repair-error :parent-not-variant
|
||||
[_ error file _]
|
||||
(log/error :hint "Variant error code, we don't want to auto repair it for now" :code (:code error))
|
||||
file)
|
||||
|
||||
(defmethod repair-error :variant-bad-name
|
||||
[_ error file _]
|
||||
(log/error :hint "Variant error code, we don't want to auto repair it for now" :code (:code error))
|
||||
file)
|
||||
|
||||
(defmethod repair-error :variant-no-properties
|
||||
[_ error file _]
|
||||
(log/error :hint "Variant error code, we don't want to auto repair it for now" :code (:code error))
|
||||
file)
|
||||
|
||||
(defmethod repair-error :variant-bad-variant-name
|
||||
[_ error file _]
|
||||
(log/error :hint "Variant error code, we don't want to auto repair it for now" :code (:code error))
|
||||
file)
|
||||
|
||||
(defmethod repair-error :variant-component-bad-name
|
||||
[_ error file _]
|
||||
(log/error :hint "Variant error code, we don't want to auto repair it for now" :code (:code error))
|
||||
file)
|
||||
|
||||
(defmethod repair-error :default
|
||||
[_ error file _]
|
||||
(log/error :hint "Unknown error code, don't know how to repair" :code (:code error))
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.common.svg.shapes-builder
|
||||
(ns app.common.files.shapes-builder
|
||||
"A SVG to Shapes builder."
|
||||
(:require
|
||||
[app.common.colors :as clr]
|
||||
@@ -21,7 +21,8 @@
|
||||
[app.common.math :as mth]
|
||||
[app.common.schema :as sm :refer [max-safe-int min-safe-int]]
|
||||
[app.common.svg :as csvg]
|
||||
[app.common.svg.path :as path]
|
||||
[app.common.types.path :as path]
|
||||
[app.common.types.path.segment :as path.segm]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.uuid :as uuid]
|
||||
[cuerdas.core :as str]))
|
||||
@@ -218,11 +219,11 @@
|
||||
(defn create-path-shape [name frame-id svg-data {:keys [attrs] :as data}]
|
||||
(when (and (contains? attrs :d) (seq (:d attrs)))
|
||||
(let [transform (csvg/parse-transform (:transform attrs))
|
||||
content (cond-> (path/parse (:d attrs))
|
||||
content (cond-> (path/from-string (:d attrs))
|
||||
(some? transform)
|
||||
(gsh/transform-content transform))
|
||||
(path.segm/transform-content transform))
|
||||
|
||||
selrect (gsh/content->selrect content)
|
||||
selrect (path.segm/content->selrect content)
|
||||
points (grc/rect->points selrect)
|
||||
origin (gpt/negate (gpt/point svg-data))
|
||||
attrs (-> (dissoc attrs :d :transform)
|
||||
@@ -435,16 +436,12 @@
|
||||
|
||||
attrs
|
||||
(-> attrs
|
||||
(cond-> linecap
|
||||
(dissoc :strokeLinecap))
|
||||
(cond-> (some? color)
|
||||
(dissoc :stroke :strokeWidth :strokeOpacity))
|
||||
(update
|
||||
:style
|
||||
(fn [style]
|
||||
(-> style
|
||||
(cond-> linecap
|
||||
(dissoc :strokeLinecap))
|
||||
(cond-> (some? color)
|
||||
(dissoc :stroke :strokeWidth :strokeOpacity)))))
|
||||
(d/without-nils))]
|
||||
@@ -461,12 +458,14 @@
|
||||
|
||||
(and (some? linecap) (cfh/path-shape? shape)
|
||||
(or (= linecap :round) (= linecap :square)))
|
||||
|
||||
(assoc :stroke-cap-start linecap
|
||||
:stroke-cap-end linecap)
|
||||
:stroke-cap-end linecap
|
||||
:stroke-linecap linecap)
|
||||
|
||||
(d/any-key? (dm/get-in shape [:strokes 0])
|
||||
:strokeColor :strokeOpacity :strokeWidth
|
||||
:strokeCapStart :strokeCapEnd)
|
||||
:strokeLinecap :strokeCapStart :strokeCapEnd)
|
||||
(assoc-in [:strokes 0 :stroke-style] :svg))))
|
||||
|
||||
(defn setup-opacity [shape]
|
||||
@@ -15,6 +15,8 @@
|
||||
[app.common.types.shape.layout :as ctl]
|
||||
[app.common.uuid :as uuid]))
|
||||
|
||||
;; FIXME: move to logic?
|
||||
|
||||
(defn prepare-add-shape
|
||||
[changes shape objects]
|
||||
(let [index (:index (meta shape))
|
||||
@@ -35,6 +37,7 @@
|
||||
(pcb/update-shapes [(:parent-id shape)] #(ctl/push-into-cell % [id] row column)))
|
||||
(cond-> (ctl/grid-layout? objects (:parent-id shape))
|
||||
(pcb/update-shapes [(:parent-id shape)] ctl/assign-cells {:with-objects? true})))]
|
||||
|
||||
[shape changes]))
|
||||
|
||||
(defn prepare-move-shapes-into-frame
|
||||
@@ -44,6 +47,7 @@
|
||||
to-move (->> shapes
|
||||
(map (d/getf objects))
|
||||
(not-empty))]
|
||||
|
||||
(if to-move
|
||||
(-> changes
|
||||
(cond-> (and remove-layout-data?
|
||||
@@ -62,6 +66,10 @@
|
||||
changes id parent-id objects selected index frame-name without-fill? nil))
|
||||
|
||||
([changes id parent-id objects selected index frame-name without-fill? target-cell-id]
|
||||
(prepare-create-artboard-from-selection
|
||||
changes id parent-id objects selected index frame-name without-fill? target-cell-id nil))
|
||||
|
||||
([changes id parent-id objects selected index frame-name without-fill? target-cell-id delta]
|
||||
(when-let [selected-objs (->> selected
|
||||
(map (d/getf objects))
|
||||
(not-empty))]
|
||||
@@ -99,10 +107,11 @@
|
||||
:id))
|
||||
target-cell-id)
|
||||
|
||||
|
||||
attrs
|
||||
{:type :frame
|
||||
:x (:x srect)
|
||||
:y (:y srect)
|
||||
:x (cond-> (:x srect) delta (+ (:x delta)))
|
||||
:y (cond-> (:y srect) delta (+ (:y delta)))
|
||||
:width (:width srect)
|
||||
:height (:height srect)}
|
||||
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
(ns app.main.ui.workspace.tokens.token
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.common.files.tokens
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.main.ui.workspace.tokens.tinycolor :as tinycolor]
|
||||
[clojure.set :as set]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
@@ -128,18 +133,6 @@
|
||||
(defn color-token? [token]
|
||||
(= (:type token) :color))
|
||||
|
||||
|
||||
;; FIXME: this should be precalculated ?
|
||||
(defn is-reference? [token]
|
||||
(str/includes? (:value token) "{"))
|
||||
|
||||
(defn color-bullet-color [token-color-value]
|
||||
(when-let [tc (tinycolor/valid-color token-color-value)]
|
||||
(if (tinycolor/alpha tc)
|
||||
{:color (tinycolor/->hex-string tc)
|
||||
:opacity (tinycolor/alpha tc)}
|
||||
(tinycolor/->hex-string tc))))
|
||||
|
||||
(defn resolved-token-bullet-color [{:keys [resolved-value] :as token}]
|
||||
(when (and resolved-value (color-token? token))
|
||||
(color-bullet-color resolved-value)))
|
||||
@@ -10,12 +10,15 @@
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.files.variant :as cfv]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.types.component :as ctk]
|
||||
[app.common.types.components-list :as ctkl]
|
||||
[app.common.types.container :as ctn]
|
||||
[app.common.types.file :as ctf]
|
||||
[app.common.types.pages-list :as ctpl]
|
||||
[app.common.types.shape-tree :as ctst]
|
||||
[app.common.types.variant :as ctv]
|
||||
[app.common.uuid :as uuid]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
@@ -56,7 +59,17 @@
|
||||
:instance-head-not-frame
|
||||
:misplaced-slot
|
||||
:missing-slot
|
||||
:shape-ref-cycle})
|
||||
:shape-ref-cycle
|
||||
:not-a-variant
|
||||
:invalid-variant-id
|
||||
:invalid-variant-properties
|
||||
:variant-not-main
|
||||
:parent-not-variant
|
||||
:variant-bad-name
|
||||
:variant-bad-variant-name
|
||||
:variant-component-bad-name
|
||||
:variant-no-properties
|
||||
:variant-component-bad-id})
|
||||
|
||||
(def ^:private schema:error
|
||||
[:map {:title "ValidationError"}
|
||||
@@ -401,6 +414,68 @@
|
||||
(check-empty-swap-slot shape file page)
|
||||
(run! #(check-shape % file page libraries :context :not-component) (:shapes shape)))
|
||||
|
||||
(defn- check-variant-container
|
||||
"Shape is a variant container, so:
|
||||
-all its children should be variants with variant-id equals to the shape-id
|
||||
-all the components should have the same properties
|
||||
"
|
||||
[shape file page]
|
||||
(let [shape-id (:id shape)
|
||||
shapes (:shapes shape)
|
||||
children (map #(ctst/get-shape page %) shapes)
|
||||
prop-names (cfv/extract-properties-names (first children) (:data file))]
|
||||
(doseq [child children]
|
||||
(if (not (ctk/is-variant? child))
|
||||
(report-error :not-a-variant
|
||||
(str/ffmt "Shape % should be a variant" (:id child))
|
||||
child file page)
|
||||
(do
|
||||
(when (not= (:variant-id child) shape-id)
|
||||
(report-error :invalid-variant-id
|
||||
(str/ffmt "Variant % has invalid variant-id %" (:id child) (:variant-id child))
|
||||
child file page))
|
||||
(when (not= prop-names (cfv/extract-properties-names child (:data file)))
|
||||
(report-error :invalid-variant-properties
|
||||
(str/ffmt "Variant % has invalid properties %" (:id child) (vec prop-names))
|
||||
child file page)))))))
|
||||
|
||||
(defn- check-variant
|
||||
"Shape is a variant, so
|
||||
-it should be a main component
|
||||
-its parent should be a variant-container
|
||||
-its variant-name is derived from the properties
|
||||
-its name should be tha same as its parent's
|
||||
"
|
||||
[shape file page]
|
||||
(let [parent (ctst/get-shape page (:parent-id shape))
|
||||
component (ctkl/get-component (:data file) (:component-id shape) true)
|
||||
name (ctv/properties-to-name (:variant-properties component))]
|
||||
(when-not (ctk/main-instance? shape)
|
||||
(report-error :variant-not-main
|
||||
(str/ffmt "Variant % is not a main instance" (:id shape))
|
||||
shape file page))
|
||||
(when-not (ctk/is-variant-container? parent)
|
||||
(report-error :parent-not-variant
|
||||
(str/ffmt "Variant % has an invalid parent" (:id shape))
|
||||
shape file page))
|
||||
|
||||
(when-not (= name (:variant-name shape))
|
||||
(report-error :variant-bad-variant-name
|
||||
(str/ffmt "Variant % has an invalid variant-name" (:id shape))
|
||||
shape file page))
|
||||
(when-not (= (:name parent) (:name shape))
|
||||
(report-error :variant-bad-name
|
||||
(str/ffmt "Variant % has an invalid name" (:id shape))
|
||||
shape file page))
|
||||
(when-not (= (:name parent) (cfh/merge-path-item (:path component) (:name component)))
|
||||
(report-error :variant-component-bad-name
|
||||
(str/ffmt "Component % has an invalid name" (:id shape))
|
||||
shape file page))
|
||||
(when-not (= (:variant-id component) (:variant-id shape))
|
||||
(report-error :variant-component-bad-id
|
||||
(str/ffmt "Variant % has adifferent variant-id than its component" (:id shape))
|
||||
shape file page))))
|
||||
|
||||
(defn- check-shape
|
||||
"Validate referential integrity and semantic coherence of
|
||||
a shape and all its children. Report all errors found.
|
||||
@@ -421,6 +496,12 @@
|
||||
(check-parent-children shape file page)
|
||||
(check-frame shape file page)
|
||||
|
||||
(when (ctk/is-variant-container? shape)
|
||||
(check-variant-container shape file page))
|
||||
|
||||
(when (ctk/is-variant? shape)
|
||||
(check-variant shape file page))
|
||||
|
||||
(if (ctk/instance-head? shape)
|
||||
(if (not= :frame (:type shape))
|
||||
(report-error :instance-head-not-frame
|
||||
@@ -496,6 +577,24 @@
|
||||
"This deleted component has shapes with shape-ref pointing to self"
|
||||
component file nil :cycles-ids cycles-ids))))
|
||||
|
||||
(defn- check-variant-component
|
||||
"Component is a variant, so:
|
||||
-Its main should be a variant
|
||||
-It should have at least one variant property"
|
||||
[component file]
|
||||
(let [component-page (ctf/get-component-page (:data file) component)
|
||||
main-component (if (:deleted component)
|
||||
(dm/get-in component [:objects (:main-instance-id component)])
|
||||
(ctst/get-shape component-page (:main-instance-id component)))]
|
||||
(when-not (ctk/is-variant? main-component)
|
||||
(report-error :not-a-variant
|
||||
(str/ffmt "Shape % should be a variant" (:id main-component))
|
||||
main-component file component-page))
|
||||
(when (< (count (:variant-properties component)) 1)
|
||||
(report-error :variant-no-properties
|
||||
(str/ffmt "Component variant % should have properties" (:id main-component))
|
||||
main-component file nil))))
|
||||
|
||||
(defn- check-component
|
||||
"Validate semantic coherence of a component. Report all errors found."
|
||||
[component file]
|
||||
@@ -505,7 +604,10 @@
|
||||
component file nil))
|
||||
(when (:deleted component)
|
||||
(check-component-duplicate-swap-slot component file)
|
||||
(check-ref-cycles component file)))
|
||||
(check-ref-cycles component file))
|
||||
|
||||
(when (ctk/is-variant? component)
|
||||
(check-variant-component component file)))
|
||||
|
||||
(defn- get-orphan-shapes
|
||||
[{:keys [objects] :as page}]
|
||||
|
||||
84
common/src/app/common/files/variant.cljc
Normal file
84
common/src/app/common/files/variant.cljc
Normal file
@@ -0,0 +1,84 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
(ns app.common.files.variant
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.types.component :as ctc]
|
||||
[app.common.types.components-list :as ctcl]
|
||||
[app.common.types.variant :as ctv]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
|
||||
(defn find-variant-components
|
||||
"Find a list of the components thet belongs to this variant-id"
|
||||
[data objects variant-id]
|
||||
;; We can't simply filter components, because we need to maintain the order
|
||||
(->> (dm/get-in objects [variant-id :shapes])
|
||||
(map #(dm/get-in objects [% :component-id]))
|
||||
(map #(ctcl/get-component data % true))
|
||||
reverse))
|
||||
|
||||
(defn- dashes-to-end
|
||||
[property-values]
|
||||
(let [dashes (if (some #(= % "--") property-values) ["--"] [])]
|
||||
(concat (remove #(= % "--") property-values) dashes)))
|
||||
|
||||
|
||||
(defn extract-properties-names
|
||||
[shape data]
|
||||
(->> shape
|
||||
(#(ctcl/get-component data (:component-id %) true))
|
||||
:variant-properties
|
||||
(map :name)))
|
||||
|
||||
|
||||
(defn extract-properties-values
|
||||
[data objects variant-id]
|
||||
(->> (find-variant-components data objects variant-id)
|
||||
(mapcat :variant-properties)
|
||||
(group-by :name)
|
||||
(map (fn [[k v]]
|
||||
{:name k
|
||||
:value (->> v
|
||||
(map #(if (str/empty? (:value %)) "--" (:value %)))
|
||||
distinct
|
||||
dashes-to-end)}))))
|
||||
|
||||
(defn get-variant-mains
|
||||
[component data]
|
||||
(assert (ctv/valid-variant-component? component) "expected valid component variant")
|
||||
(when-let [variant-id (:variant-id component)]
|
||||
(let [page-id (:main-instance-page component)
|
||||
objects (-> (dm/get-in data [:pages-index page-id])
|
||||
(get :objects))]
|
||||
(dm/get-in objects [variant-id :shapes]))))
|
||||
|
||||
|
||||
(defn is-secondary-variant?
|
||||
[component data]
|
||||
(let [shapes (get-variant-mains component data)]
|
||||
(and (seq shapes)
|
||||
(not= (:main-instance-id component) (last shapes)))))
|
||||
|
||||
(defn get-primary-variant
|
||||
[data component]
|
||||
(let [page-id (:main-instance-page component)
|
||||
objects (-> (dm/get-in data [:pages-index page-id])
|
||||
(get :objects))
|
||||
variant-id (:variant-id component)]
|
||||
(->> (dm/get-in objects [variant-id :shapes])
|
||||
peek
|
||||
(get objects))))
|
||||
|
||||
(defn get-primary-component
|
||||
[data component-id]
|
||||
(when-let [component (ctcl/get-component data component-id)]
|
||||
(if (ctc/is-variant? component)
|
||||
(->> component
|
||||
(get-primary-variant data)
|
||||
:component-id
|
||||
(ctcl/get-component data))
|
||||
component)))
|
||||
@@ -79,7 +79,7 @@
|
||||
:file-schema-validation
|
||||
;; Reports the schema validation errors internally.
|
||||
:soft-file-schema-validation
|
||||
;; Activates the referential integrity validation during update file; related to components-v2.
|
||||
;; Activates the referential integrity validation during update file.
|
||||
:file-validation
|
||||
;; Reports the referential integrity validation errors internally.
|
||||
:soft-file-validation
|
||||
@@ -124,7 +124,9 @@
|
||||
;; TODO: deprecate this flag and consolidate the code
|
||||
:export-file-v3
|
||||
:render-wasm-dpr
|
||||
:hide-release-modal})
|
||||
:hide-release-modal
|
||||
:subscriptions
|
||||
:subscriptions-old})
|
||||
|
||||
(def all-flags
|
||||
(set/union email login varia))
|
||||
|
||||
@@ -126,21 +126,20 @@
|
||||
o)))
|
||||
|
||||
(def schema:matrix
|
||||
{:type :map
|
||||
:pred valid-matrix?
|
||||
:type-properties
|
||||
{:title "matrix"
|
||||
:description "Matrix instance"
|
||||
:error/message "expected a valid matrix instance"
|
||||
:gen/gen (matrix-generator)
|
||||
:decode/json decode-matrix
|
||||
:decode/string decode-matrix
|
||||
:encode/json matrix->json
|
||||
:encode/string matrix->str
|
||||
::oapi/type "string"
|
||||
::oapi/format "matrix"}})
|
||||
|
||||
(sm/register! ::matrix schema:matrix)
|
||||
(sm/register!
|
||||
{:type ::matrix
|
||||
:pred valid-matrix?
|
||||
:type-properties
|
||||
{:title "matrix"
|
||||
:description "Matrix instance"
|
||||
:error/message "expected a valid matrix instance"
|
||||
:gen/gen (matrix-generator)
|
||||
:decode/json decode-matrix
|
||||
:decode/string decode-matrix
|
||||
:encode/json matrix->json
|
||||
:encode/string matrix->str
|
||||
::oapi/type "string"
|
||||
::oapi/format "matrix"}}))
|
||||
|
||||
;; FIXME: deprecated
|
||||
(s/def ::a ::us/safe-float)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.common.geom.point
|
||||
(:refer-clojure :exclude [divide min max abs])
|
||||
(:refer-clojure :exclude [divide min max abs zero?])
|
||||
(:require
|
||||
#?(:clj [app.common.fressian :as fres])
|
||||
#?(:cljs [cljs.core :as c]
|
||||
@@ -85,24 +85,22 @@
|
||||
(into {} p)
|
||||
p))
|
||||
|
||||
;; FIXME: make like matrix
|
||||
(def schema:point
|
||||
{:type ::point
|
||||
:pred valid-point?
|
||||
:type-properties
|
||||
{:title "point"
|
||||
:description "Point"
|
||||
:error/message "expected a valid point"
|
||||
:gen/gen (->> (sg/tuple (sg/small-int) (sg/small-int))
|
||||
(sg/fmap #(apply pos->Point %)))
|
||||
::oapi/type "string"
|
||||
::oapi/format "point"
|
||||
:decode/json decode-point
|
||||
:decode/string decode-point
|
||||
:encode/json point->json
|
||||
:encode/string point->str}})
|
||||
|
||||
(sm/register! schema:point)
|
||||
(sm/register!
|
||||
{:type ::point
|
||||
:pred valid-point?
|
||||
:type-properties
|
||||
{:title "point"
|
||||
:description "Point"
|
||||
:error/message "expected a valid point"
|
||||
:gen/gen (->> (sg/tuple (sg/small-int) (sg/small-int))
|
||||
(sg/fmap #(apply pos->Point %)))
|
||||
::oapi/type "string"
|
||||
::oapi/format "point"
|
||||
:decode/json decode-point
|
||||
:decode/string decode-point
|
||||
:encode/json point->json
|
||||
:encode/string point->str}}))
|
||||
|
||||
(defn point-like?
|
||||
[{:keys [x y] :as v}]
|
||||
@@ -470,6 +468,13 @@
|
||||
(and ^boolean (mth/almost-zero? (dm/get-prop p :x))
|
||||
^boolean (mth/almost-zero? (dm/get-prop p :y))))
|
||||
|
||||
(defn zero?
|
||||
[p]
|
||||
(let [x (dm/get-prop p :x)
|
||||
y (dm/get-prop p :y)]
|
||||
(and ^boolean (== 0 x)
|
||||
^boolean (== 0 y))))
|
||||
|
||||
(defn lerp
|
||||
"Calculates a linear interpolation between two points given a tvalue"
|
||||
[p1 p2 t]
|
||||
|
||||
@@ -10,13 +10,11 @@
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.rect :as grc]
|
||||
[app.common.geom.shapes.bool :as gsb]
|
||||
[app.common.geom.shapes.common :as gco]
|
||||
[app.common.geom.shapes.constraints :as gct]
|
||||
[app.common.geom.shapes.corners :as gsc]
|
||||
[app.common.geom.shapes.fit-frame :as gsff]
|
||||
[app.common.geom.shapes.intersect :as gsi]
|
||||
[app.common.geom.shapes.path :as gsp]
|
||||
[app.common.geom.shapes.transforms :as gtr]
|
||||
[app.common.math :as mth]))
|
||||
|
||||
@@ -166,7 +164,7 @@
|
||||
(dm/export gtr/calculate-geometry)
|
||||
(dm/export gtr/update-group-selrect)
|
||||
(dm/export gtr/update-mask-selrect)
|
||||
(dm/export gtr/update-bool-selrect)
|
||||
(dm/export gtr/update-bool)
|
||||
(dm/export gtr/apply-transform)
|
||||
(dm/export gtr/transform-shape)
|
||||
(dm/export gtr/transform-selrect)
|
||||
@@ -180,12 +178,6 @@
|
||||
;; Constratins
|
||||
(dm/export gct/calc-child-modifiers)
|
||||
|
||||
;; PATHS
|
||||
;; FIXME: rename
|
||||
(dm/export gsp/content->selrect)
|
||||
(dm/export gsp/transform-content)
|
||||
(dm/export gsp/open-path?)
|
||||
|
||||
;; Intersection
|
||||
(dm/export gsi/overlaps?)
|
||||
(dm/export gsi/overlaps-path?)
|
||||
@@ -193,9 +185,6 @@
|
||||
(dm/export gsi/has-point-rect?)
|
||||
(dm/export gsi/rect-contains-shape?)
|
||||
|
||||
;; Bool
|
||||
(dm/export gsb/calc-bool-content)
|
||||
|
||||
;; Constraints
|
||||
(dm/export gct/default-constraints-h)
|
||||
(dm/export gct/default-constraints-v)
|
||||
@@ -206,6 +195,7 @@
|
||||
|
||||
;; Rect
|
||||
(dm/export grc/rect->points)
|
||||
(dm/export grc/center->rect)
|
||||
|
||||
;;
|
||||
(dm/export gsff/fit-frame-modifiers)
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.common.geom.shapes.bool
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.files.helpers :as cpf]
|
||||
[app.common.svg.path.bool :as pb]
|
||||
[app.common.svg.path.shapes-to-path :as stp]))
|
||||
|
||||
(defn calc-bool-content
|
||||
[shape objects]
|
||||
|
||||
(let [extract-content-xf
|
||||
(comp (map (d/getf objects))
|
||||
(filter (comp not :hidden))
|
||||
(remove cpf/svg-raw-shape?)
|
||||
(map #(stp/convert-to-path % objects))
|
||||
(map :content))
|
||||
|
||||
shapes-content
|
||||
(into [] extract-content-xf (:shapes shape))]
|
||||
(pb/content-bool (:bool-type shape) shapes-content)))
|
||||
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.geom.rect :as grc]
|
||||
[app.common.geom.shapes.path :as gsp]
|
||||
[app.common.math :as mth]))
|
||||
[app.common.math :as mth]
|
||||
[app.common.types.path :as path]))
|
||||
|
||||
(defn shape-stroke-margin
|
||||
[shape stroke-width]
|
||||
@@ -104,7 +104,7 @@
|
||||
(let [strokes (:strokes shape)
|
||||
|
||||
open-path? (and ^boolean (cfh/path-shape? shape)
|
||||
^boolean (gsp/open-path? shape))
|
||||
^boolean (path/shape-with-open-path? shape))
|
||||
|
||||
stroke-width
|
||||
(->> strokes
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
;;
|
||||
;; 5. If any track still has an infinite growth limit set its growth limit to its base size.
|
||||
|
||||
;; - Distribute extra space accross spaned tracks
|
||||
;; - Distribute extra space accross spaned tracks
|
||||
;; - Maximize tracks
|
||||
;;
|
||||
;; - Expand flexible tracks
|
||||
@@ -198,7 +198,7 @@
|
||||
|
||||
track-list))
|
||||
|
||||
(defn add-auto-size
|
||||
(defn stretch-tracks
|
||||
[track-list add-size]
|
||||
(->> track-list
|
||||
(mapv (fn [{:keys [type size max-size] :as track}]
|
||||
@@ -357,7 +357,8 @@
|
||||
to-idx (+ (dec (get cell prop)) (get cell prop-span))
|
||||
indexed-tracks (subvec (d/enumerate track-list) from-idx to-idx)
|
||||
|
||||
to-allocate (size-to-allocate type parent (get children-map shape-id) cell bounds objects)
|
||||
to-allocate
|
||||
(size-to-allocate type parent (get children-map shape-id) cell bounds objects)
|
||||
|
||||
;; Remove the size and the tracks that are not allocated
|
||||
[to-allocate total-frs indexed-tracks]
|
||||
@@ -493,11 +494,11 @@
|
||||
|
||||
column-tracks (cond-> column-tracks
|
||||
(= :stretch (:layout-justify-content parent))
|
||||
(add-auto-size column-add-auto))
|
||||
(stretch-tracks column-add-auto))
|
||||
|
||||
row-tracks (cond-> row-tracks
|
||||
(= :stretch (:layout-align-content parent))
|
||||
(add-auto-size row-add-auto))
|
||||
(stretch-tracks row-add-auto))
|
||||
|
||||
column-total-size (tracks-total-size column-tracks)
|
||||
row-total-size (tracks-total-size row-tracks)
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.rect :as grc]
|
||||
[app.common.geom.shapes.common :as gco]
|
||||
[app.common.geom.shapes.path :as gpp]
|
||||
[app.common.geom.shapes.text :as gte]
|
||||
[app.common.math :as mth]))
|
||||
[app.common.math :as mth]
|
||||
[app.common.types.path.segment :as path.segm]))
|
||||
|
||||
(defn orientation
|
||||
"Given three ordered points gives the orientation
|
||||
@@ -186,7 +186,7 @@
|
||||
rect-lines (points->lines rect-points)
|
||||
path-lines (if simple?
|
||||
(points->lines (:points shape))
|
||||
(gpp/path->lines shape))
|
||||
(path.segm/path->lines shape))
|
||||
start-point (-> shape :content (first) :params (gpt/point))]
|
||||
|
||||
(or (intersects-lines? rect-lines path-lines)
|
||||
|
||||
@@ -12,11 +12,10 @@
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.rect :as grc]
|
||||
[app.common.geom.shapes.bool :as gshb]
|
||||
[app.common.geom.shapes.common :as gco]
|
||||
[app.common.geom.shapes.path :as gpa]
|
||||
[app.common.math :as mth]
|
||||
[app.common.types.modifiers :as ctm]))
|
||||
[app.common.types.modifiers :as ctm]
|
||||
[app.common.types.path :as path]))
|
||||
|
||||
#?(:clj (set! *warn-on-reflection* true))
|
||||
|
||||
@@ -77,7 +76,11 @@
|
||||
position-data)
|
||||
position-data))))
|
||||
|
||||
;; FIXME: revist usage of mutability
|
||||
;; FIXME: review performance of this; this function is executing too
|
||||
;; many times, including when the point vector is 0,0. This function
|
||||
;; can be implemented in function of transform which is already mor
|
||||
;; performant
|
||||
|
||||
(defn move
|
||||
"Move the shape relatively to its current
|
||||
position applying the provided delta."
|
||||
@@ -95,8 +98,8 @@
|
||||
(d/update-when :x d/safe+ dx)
|
||||
(d/update-when :y d/safe+ dy)
|
||||
(d/update-when :position-data move-position-data mvec)
|
||||
(cond-> (= :bool type) (update :bool-content gpa/move-content mvec))
|
||||
(cond-> (= :path type) (update :content gpa/move-content mvec)))))
|
||||
(cond-> (or (= :bool type) (= :path type))
|
||||
(update :content path/move-content mvec)))))
|
||||
|
||||
;; --- Absolute Movement
|
||||
|
||||
@@ -317,14 +320,11 @@
|
||||
points (gco/transform-points (dm/get-prop shape :points) transform-mtx)
|
||||
selrect (gco/transform-selrect (dm/get-prop shape :selrect) transform-mtx)
|
||||
|
||||
shape (if (= type :bool)
|
||||
(update shape :bool-content gpa/transform-content transform-mtx)
|
||||
shape)
|
||||
shape (if (= type :text)
|
||||
(update shape :position-data transform-position-data transform-mtx)
|
||||
shape)
|
||||
shape (if (= type :path)
|
||||
(update shape :content gpa/transform-content transform-mtx)
|
||||
shape (if (or (= type :path) (= type :bool))
|
||||
(update shape :content path/transform-content transform-mtx)
|
||||
(assoc shape
|
||||
:x (dm/get-prop selrect :x)
|
||||
:y (dm/get-prop selrect :y)
|
||||
@@ -355,12 +355,9 @@
|
||||
rotation (mod (+ (d/nilv (:rotation shape) 0)
|
||||
(d/nilv (dm/get-in shape [:modifiers :rotation]) 0))
|
||||
360)
|
||||
shape (if (= type :bool)
|
||||
(update shape :bool-content gpa/transform-content transform-mtx)
|
||||
shape)
|
||||
|
||||
shape (if (= type :path)
|
||||
(update shape :content gpa/transform-content transform-mtx)
|
||||
shape (if (or (= type :path) (= type :bool))
|
||||
(update shape :content path/transform-content transform-mtx)
|
||||
(assoc shape
|
||||
:x (dm/get-prop selrect :x)
|
||||
:y (dm/get-prop selrect :y)
|
||||
@@ -377,8 +374,14 @@
|
||||
"Given a new set of points transformed, set up the rectangle so it keeps
|
||||
its properties. We adjust de x,y,width,height and create a custom transform"
|
||||
[shape transform-mtx]
|
||||
(if ^boolean (gmt/move? transform-mtx)
|
||||
(cond
|
||||
(nil? transform-mtx)
|
||||
shape
|
||||
|
||||
^boolean (gmt/move? transform-mtx)
|
||||
(apply-transform-move shape transform-mtx)
|
||||
|
||||
:else
|
||||
(apply-transform-generic shape transform-mtx)))
|
||||
|
||||
(defn- update-group-viewbox
|
||||
@@ -450,20 +453,14 @@
|
||||
(assoc :flip-x (-> mask :flip-x))
|
||||
(assoc :flip-y (-> mask :flip-y)))))
|
||||
|
||||
(defn update-bool-selrect
|
||||
(defn update-bool
|
||||
"Calculates the selrect+points for the boolean shape"
|
||||
[shape children objects]
|
||||
|
||||
(let [bool-content (gshb/calc-bool-content shape objects)
|
||||
shape (assoc shape :bool-content bool-content)
|
||||
[points selrect] (gpa/content->points+selrect shape bool-content)]
|
||||
|
||||
(if (and (some? selrect) (d/not-empty? points))
|
||||
(-> shape
|
||||
(assoc :selrect selrect)
|
||||
(assoc :points points))
|
||||
(update-group-selrect shape children))))
|
||||
[shape objects]
|
||||
(let [content (path/calc-bool-content shape objects)
|
||||
shape (assoc shape :content content)]
|
||||
(path/update-geometry shape)))
|
||||
|
||||
;; FIXME: revisit
|
||||
(defn update-shapes-geometry
|
||||
[objects ids]
|
||||
(->> ids
|
||||
@@ -477,7 +474,7 @@
|
||||
(update-mask-selrect shape children)
|
||||
|
||||
(cfh/bool-shape? shape)
|
||||
(update-bool-selrect shape children objects)
|
||||
(update-bool shape objects)
|
||||
|
||||
(cfh/group-shape? shape)
|
||||
(update-group-selrect shape children)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,14 +10,14 @@
|
||||
[app.common.files.changes-builder :as pcb]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.logic.variants :as clv]
|
||||
[app.common.logic.variant-properties :as clvp]
|
||||
[app.common.types.component :as ctk]
|
||||
[app.common.types.container :as ctn]
|
||||
[app.common.types.pages-list :as ctpl]
|
||||
[app.common.types.shape.interactions :as ctsi]
|
||||
[app.common.types.shape.layout :as ctl]
|
||||
[app.common.types.token :as cto]
|
||||
[app.common.uuid :as uuid]
|
||||
[cuerdas.core :as str]))
|
||||
[app.common.uuid :as uuid]))
|
||||
|
||||
(defn- generate-unapply-tokens
|
||||
"When updating attributes that have a token applied, we must unapply it, because the value
|
||||
@@ -81,163 +81,171 @@
|
||||
(pcb/update-shapes ids update-fn {:attrs #{:blocked :hidden}}))))
|
||||
|
||||
(defn generate-delete-shapes
|
||||
[changes file page objects ids {:keys [components-v2 ignore-touched component-swap]}]
|
||||
(let [ids (cfh/clean-loops objects ids)
|
||||
([changes file page objects ids options]
|
||||
(generate-delete-shapes (-> changes
|
||||
(pcb/with-page page)
|
||||
(pcb/with-objects objects)
|
||||
(pcb/with-library-data file))
|
||||
ids
|
||||
options))
|
||||
([changes ids {:keys [ignore-touched component-swap]}]
|
||||
(let [objects (pcb/get-objects changes)
|
||||
data (pcb/get-library-data changes)
|
||||
page-id (pcb/get-page-id changes)
|
||||
page (or (pcb/get-page changes)
|
||||
(ctpl/get-page data page-id))
|
||||
|
||||
in-component-copy?
|
||||
(fn [shape-id]
|
||||
ids (cfh/clean-loops objects ids)
|
||||
in-component-copy?
|
||||
(fn [shape-id]
|
||||
;; Look for shapes that are inside a component copy, but are
|
||||
;; not the root. In this case, they must not be deleted,
|
||||
;; but hidden (to be able to recover them more easily).
|
||||
;; Unless we are doing a component swap, in which case we want
|
||||
;; to delete the old shape
|
||||
(let [shape (get objects shape-id)]
|
||||
(and (ctn/has-any-copy-parent? objects shape)
|
||||
(not component-swap))))
|
||||
(let [shape (get objects shape-id)]
|
||||
(and (ctn/has-any-copy-parent? objects shape)
|
||||
(not component-swap))))
|
||||
|
||||
[ids-to-delete ids-to-hide]
|
||||
(if components-v2
|
||||
(loop [ids-seq (seq ids)
|
||||
ids-to-delete []
|
||||
ids-to-hide []]
|
||||
(let [id (first ids-seq)]
|
||||
(if (nil? id)
|
||||
[ids-to-delete ids-to-hide]
|
||||
(if (in-component-copy? id)
|
||||
(recur (rest ids-seq)
|
||||
ids-to-delete
|
||||
(conj ids-to-hide id))
|
||||
(recur (rest ids-seq)
|
||||
(conj ids-to-delete id)
|
||||
ids-to-hide)))))
|
||||
[ids []])
|
||||
[ids-to-delete ids-to-hide]
|
||||
(loop [ids-seq (seq ids)
|
||||
ids-to-delete []
|
||||
ids-to-hide []]
|
||||
(let [id (first ids-seq)]
|
||||
(if (nil? id)
|
||||
[ids-to-delete ids-to-hide]
|
||||
(if (in-component-copy? id)
|
||||
(recur (rest ids-seq)
|
||||
ids-to-delete
|
||||
(conj ids-to-hide id))
|
||||
(recur (rest ids-seq)
|
||||
(conj ids-to-delete id)
|
||||
ids-to-hide)))))
|
||||
|
||||
changes (-> changes
|
||||
(pcb/with-page page)
|
||||
(pcb/with-objects objects)
|
||||
(pcb/with-library-data file))
|
||||
lookup (d/getf objects)
|
||||
|
||||
lookup (d/getf objects)
|
||||
|
||||
groups-to-unmask
|
||||
(reduce (fn [group-ids id]
|
||||
groups-to-unmask
|
||||
(reduce (fn [group-ids id]
|
||||
;; When the shape to delete is the mask of a masked group,
|
||||
;; the mask condition must be removed, and it must be
|
||||
;; converted to a normal group.
|
||||
(let [obj (lookup id)
|
||||
parent (lookup (:parent-id obj))]
|
||||
(if (and (:masked-group parent)
|
||||
(= id (first (:shapes parent))))
|
||||
(conj group-ids (:id parent))
|
||||
group-ids)))
|
||||
#{}
|
||||
ids-to-delete)
|
||||
(let [obj (lookup id)
|
||||
parent (lookup (:parent-id obj))]
|
||||
(if (and (:masked-group parent)
|
||||
(= id (first (:shapes parent))))
|
||||
(conj group-ids (:id parent))
|
||||
group-ids)))
|
||||
#{}
|
||||
ids-to-delete)
|
||||
|
||||
interacting-shapes
|
||||
(filter (fn [shape]
|
||||
interacting-shapes
|
||||
(filter (fn [shape]
|
||||
;; If any of the deleted shapes is the destination of
|
||||
;; some interaction, this must be deleted, too.
|
||||
(let [interactions (:interactions shape)]
|
||||
(some #(and (ctsi/has-destination %)
|
||||
(contains? ids-to-delete (:destination %)))
|
||||
interactions)))
|
||||
(vals objects))
|
||||
(let [interactions (:interactions shape)]
|
||||
(some #(and (ctsi/has-destination %)
|
||||
(contains? ids-to-delete (:destination %)))
|
||||
interactions)))
|
||||
(vals objects))
|
||||
|
||||
changes
|
||||
(reduce (fn [changes {:keys [id] :as flow}]
|
||||
(if (contains? ids-to-delete (:starting-frame flow))
|
||||
(pcb/set-flow changes id nil)
|
||||
changes))
|
||||
changes
|
||||
(:flows page))
|
||||
changes
|
||||
(reduce (fn [changes {:keys [id] :as flow}]
|
||||
(if (contains? ids-to-delete (:starting-frame flow))
|
||||
(-> changes
|
||||
(pcb/with-page page)
|
||||
(pcb/set-flow id nil))
|
||||
changes))
|
||||
changes
|
||||
(:flows page))
|
||||
|
||||
|
||||
all-parents
|
||||
(reduce (fn [res id]
|
||||
all-parents
|
||||
(reduce (fn [res id]
|
||||
;; All parents of any deleted shape must be resized.
|
||||
(into res (cfh/get-parent-ids objects id)))
|
||||
(d/ordered-set)
|
||||
(concat ids-to-delete ids-to-hide))
|
||||
(into res (cfh/get-parent-ids objects id)))
|
||||
(d/ordered-set)
|
||||
(concat ids-to-delete ids-to-hide))
|
||||
|
||||
all-children
|
||||
(->> ids-to-delete ;; Children of deleted shapes must be also deleted.
|
||||
(reduce (fn [res id]
|
||||
(into res (cfh/get-children-ids objects id)))
|
||||
[])
|
||||
(reverse)
|
||||
(into (d/ordered-set)))
|
||||
all-children
|
||||
(->> ids-to-delete ;; Children of deleted shapes must be also deleted.
|
||||
(reduce (fn [res id]
|
||||
(into res (cfh/get-children-ids objects id)))
|
||||
[])
|
||||
(reverse)
|
||||
(into (d/ordered-set)))
|
||||
|
||||
find-all-empty-parents
|
||||
(fn recursive-find-empty-parents [empty-parents]
|
||||
(let [all-ids (into empty-parents ids-to-delete)
|
||||
contains? (partial contains? all-ids)
|
||||
xform (comp (map lookup)
|
||||
(filter #(or (cfh/group-shape? %) (cfh/bool-shape? %)))
|
||||
(remove #(->> (:shapes %) (remove contains?) seq))
|
||||
(map :id))
|
||||
parents (into #{} xform all-parents)]
|
||||
(if (= empty-parents parents)
|
||||
empty-parents
|
||||
(recursive-find-empty-parents parents))))
|
||||
find-all-empty-parents
|
||||
(fn recursive-find-empty-parents [empty-parents]
|
||||
(let [all-ids (into empty-parents ids-to-delete)
|
||||
contains? (partial contains? all-ids)
|
||||
xform (comp (map lookup)
|
||||
(filter #(or (cfh/group-shape? %) (cfh/bool-shape? %) (ctk/is-variant-container? %)))
|
||||
(remove #(->> (:shapes %) (remove contains?) seq))
|
||||
(map :id))
|
||||
parents (into #{} xform all-parents)]
|
||||
(if (= empty-parents parents)
|
||||
empty-parents
|
||||
(recursive-find-empty-parents parents))))
|
||||
|
||||
empty-parents
|
||||
empty-parents
|
||||
;; Any parent whose children are all deleted, must be deleted too.
|
||||
;; Unless we are during a component swap: in this case we are replacing a shape by
|
||||
;; other one, so must not delete empty parents.
|
||||
(if-not component-swap
|
||||
(into (d/ordered-set) (find-all-empty-parents #{}))
|
||||
#{})
|
||||
(if-not component-swap
|
||||
(into (d/ordered-set) (find-all-empty-parents #{}))
|
||||
#{})
|
||||
|
||||
components-to-delete
|
||||
(if components-v2
|
||||
(reduce (fn [components id]
|
||||
(let [shape (get objects id)]
|
||||
(if (and (= (:component-file shape) (:id file)) ;; Main instances should exist only in local file
|
||||
(:main-instance shape)) ;; but check anyway
|
||||
(conj components (:component-id shape))
|
||||
components)))
|
||||
[]
|
||||
(into ids-to-delete all-children))
|
||||
[])
|
||||
components-to-delete
|
||||
(reduce (fn [components id]
|
||||
(let [shape (get objects id)]
|
||||
(if (and (= (:component-file shape) (:id data)) ;; Main instances should exist only in local file
|
||||
(:main-instance shape)) ;; but check anyway
|
||||
(conj components (:component-id shape))
|
||||
components)))
|
||||
[]
|
||||
(into ids-to-delete all-children))
|
||||
|
||||
ids-set (set ids-to-delete)
|
||||
|
||||
guides-to-delete
|
||||
(->> (:guides page)
|
||||
(vals)
|
||||
(filter #(contains? ids-set (:frame-id %)))
|
||||
(map :id))
|
||||
ids-set (set ids-to-delete)
|
||||
|
||||
changes (reduce (fn [changes guide-id]
|
||||
(pcb/set-flow changes guide-id nil))
|
||||
changes
|
||||
guides-to-delete)
|
||||
guides-to-delete
|
||||
(->> (:guides page)
|
||||
(vals)
|
||||
(filter #(contains? ids-set (:frame-id %)))
|
||||
(map :id))
|
||||
|
||||
changes (reduce (fn [changes component-id]
|
||||
changes (reduce (fn [changes guide-id]
|
||||
(-> changes
|
||||
(pcb/with-page page)
|
||||
(pcb/set-flow guide-id nil)))
|
||||
changes
|
||||
guides-to-delete)
|
||||
|
||||
changes (reduce (fn [changes component-id]
|
||||
;; It's important to delete the component before the main instance, because we
|
||||
;; need to store the instance position if we want to restore it later.
|
||||
(pcb/delete-component changes component-id (:id page)))
|
||||
changes
|
||||
components-to-delete)
|
||||
(pcb/delete-component changes component-id (:id page)))
|
||||
changes
|
||||
components-to-delete)
|
||||
|
||||
changes (-> changes
|
||||
(generate-update-shape-flags ids-to-hide objects {:hidden true})
|
||||
(pcb/remove-objects all-children {:ignore-touched true})
|
||||
(pcb/remove-objects ids-to-delete {:ignore-touched ignore-touched})
|
||||
(pcb/remove-objects empty-parents)
|
||||
(pcb/resize-parents all-parents)
|
||||
(pcb/update-shapes groups-to-unmask
|
||||
(fn [shape]
|
||||
(assoc shape :masked-group false)))
|
||||
(pcb/update-shapes (map :id interacting-shapes)
|
||||
(fn [shape]
|
||||
(d/update-when shape :interactions
|
||||
(fn [interactions]
|
||||
(into []
|
||||
(remove #(and (ctsi/has-destination %)
|
||||
(contains? ids-to-delete (:destination %))))
|
||||
interactions))))))]
|
||||
[all-parents changes]))
|
||||
changes (-> changes
|
||||
(generate-update-shape-flags ids-to-hide objects {:hidden true})
|
||||
(pcb/remove-objects all-children {:ignore-touched true})
|
||||
(pcb/remove-objects ids-to-delete {:ignore-touched ignore-touched})
|
||||
(pcb/remove-objects empty-parents)
|
||||
(pcb/resize-parents all-parents)
|
||||
(pcb/update-shapes groups-to-unmask
|
||||
(fn [shape]
|
||||
(assoc shape :masked-group false)))
|
||||
(pcb/update-shapes (map :id interacting-shapes)
|
||||
(fn [shape]
|
||||
(d/update-when shape :interactions
|
||||
(fn [interactions]
|
||||
(into []
|
||||
(remove #(and (ctsi/has-destination %)
|
||||
(contains? ids-to-delete (:destination %))))
|
||||
interactions))))))]
|
||||
[all-parents changes])))
|
||||
|
||||
|
||||
(defn generate-relocate
|
||||
@@ -255,7 +263,7 @@
|
||||
|
||||
child-heads-ids (map :id child-heads)
|
||||
|
||||
variant-heads (filter ctk/is-variant? child-heads)
|
||||
variant-shapes (filter ctk/is-variant? shapes)
|
||||
|
||||
component-main-parent
|
||||
(ctn/find-component-main objects parent false)
|
||||
@@ -339,7 +347,19 @@
|
||||
(map :id)))
|
||||
|
||||
index-cell-data (when to-index (ctl/get-cell-by-index parent to-index))
|
||||
cell (or cell (and index-cell-data [(:row index-cell-data) (:column index-cell-data)]))]
|
||||
cell (or cell (and index-cell-data [(:row index-cell-data) (:column index-cell-data)]))
|
||||
|
||||
|
||||
;; Parents that are a variant-container that becomes empty
|
||||
empty-variant-cont (reduce
|
||||
(fn [to-delete parent-id]
|
||||
(let [parent (get objects parent-id)]
|
||||
(if (and (ctk/is-variant-container? parent)
|
||||
(empty? (remove (set ids) (:shapes parent))))
|
||||
(conj to-delete (:id parent))
|
||||
to-delete)))
|
||||
#{}
|
||||
(remove #(= % parent-id) all-parents))]
|
||||
|
||||
(-> changes
|
||||
;; Remove layout-item properties when moving a shape outside a layout
|
||||
@@ -368,82 +388,11 @@
|
||||
|
||||
;; Remove variant info and rename when moving outside a variant-container
|
||||
(cond-> (not (ctk/is-variant-container? parent))
|
||||
((fn [changes]
|
||||
(reduce
|
||||
(fn [changes shape]
|
||||
(let [new-name (str/replace (:variant-name shape) #", " " / ")
|
||||
[cpath cname] (cfh/parse-path-name new-name)]
|
||||
(-> changes
|
||||
(pcb/update-component (:component-id shape)
|
||||
#(-> (dissoc % :variant-id :variant-properties)
|
||||
(assoc :name cname
|
||||
:path cpath))
|
||||
{:apply-changes-local-library? true})
|
||||
(pcb/update-shapes [(:id shape)]
|
||||
#(-> (dissoc % :variant-id :variant-name)
|
||||
(assoc :name new-name))))))
|
||||
changes
|
||||
variant-heads))))
|
||||
(clvp/generate-make-shapes-no-variant variant-shapes))
|
||||
|
||||
;; Add variant info and rename when moving into a different variant-container
|
||||
(cond-> (ctk/is-variant-container? parent)
|
||||
((fn [changes]
|
||||
(let [get-base-name #(if (some? (:variant-name %))
|
||||
(str/replace (:variant-name %) #", " " / ")
|
||||
(:name %))
|
||||
|
||||
calc-num-props #(-> %
|
||||
get-base-name
|
||||
cfh/split-path
|
||||
count)
|
||||
|
||||
max-path-items (apply max (map calc-num-props child-heads))
|
||||
|
||||
first-comp-id (->> parent
|
||||
:shapes
|
||||
first
|
||||
(get objects)
|
||||
:component-id)
|
||||
|
||||
data (pcb/get-library-data changes)
|
||||
variant-properties (get-in data [:components first-comp-id :variant-properties])
|
||||
num-props (count variant-properties)
|
||||
num-new-props (if (< max-path-items num-props)
|
||||
0
|
||||
(- max-path-items num-props))
|
||||
|
||||
changes (nth
|
||||
(iterate #(clv/generate-add-new-property % (:id parent)) changes)
|
||||
num-new-props)]
|
||||
(reduce
|
||||
(fn [changes shape]
|
||||
(if (= (:id parent) (:variant-id shape))
|
||||
changes ;; do nothing if we aren't changing the parent
|
||||
(let [base-name (get-base-name shape)
|
||||
|
||||
;; we need to get the updated library data to have access to the current properties
|
||||
data (pcb/get-library-data changes)
|
||||
|
||||
props (clv/path-to-properties
|
||||
base-name
|
||||
(get-in data [:components first-comp-id :variant-properties]))
|
||||
|
||||
variant-name (clv/properties-to-name props)
|
||||
[cpath cname] (cfh/parse-path-name (:name parent))]
|
||||
|
||||
(-> (pcb/update-component changes
|
||||
(:component-id shape)
|
||||
#(assoc % :variant-id (:id parent)
|
||||
:variant-properties props
|
||||
:name cname
|
||||
:path cpath)
|
||||
{:apply-changes-local-library? true})
|
||||
(pcb/update-shapes [(:id shape)]
|
||||
#(assoc % :variant-id (:id parent)
|
||||
:variant-name variant-name
|
||||
:name (:name parent)))))))
|
||||
changes
|
||||
child-heads)))))
|
||||
(clvp/generate-make-shapes-variant child-heads parent))
|
||||
|
||||
;; Move the shapes
|
||||
(pcb/change-parent parent-id
|
||||
@@ -518,7 +467,11 @@
|
||||
(pcb/update-shapes ids #(assoc % :blocked true)))
|
||||
|
||||
;; Resize parent containers that need to
|
||||
(pcb/resize-parents parents))))
|
||||
(pcb/resize-parents parents)
|
||||
|
||||
;; Remove parents when are a variant-container that becomes empty
|
||||
(cond-> (seq empty-variant-cont)
|
||||
(#(second (generate-delete-shapes % empty-variant-cont {})))))))
|
||||
|
||||
(defn change-show-in-viewer
|
||||
[shape hide?]
|
||||
|
||||
212
common/src/app/common/logic/variant_properties.cljc
Normal file
212
common/src/app/common/logic/variant_properties.cljc
Normal file
@@ -0,0 +1,212 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
(ns app.common.logic.variant-properties
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.files.changes-builder :as pcb]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.files.variant :as cfv]
|
||||
[app.common.types.component :as ctk]
|
||||
[app.common.types.components-list :as ctcl]
|
||||
[app.common.types.variant :as ctv]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(defn generate-update-property-name
|
||||
[changes variant-id pos new-name]
|
||||
(let [data (pcb/get-library-data changes)
|
||||
objects (pcb/get-objects changes)
|
||||
related-components (cfv/find-variant-components data objects variant-id)]
|
||||
(reduce (fn [changes component]
|
||||
(pcb/update-component
|
||||
changes (:id component)
|
||||
#(assoc-in % [:variant-properties pos :name] new-name)
|
||||
{:apply-changes-local-library? true}))
|
||||
changes
|
||||
related-components)))
|
||||
|
||||
|
||||
(defn generate-remove-property
|
||||
[changes variant-id pos]
|
||||
(let [data (pcb/get-library-data changes)
|
||||
objects (pcb/get-objects changes)
|
||||
related-components (cfv/find-variant-components data objects variant-id)]
|
||||
(reduce (fn [changes component]
|
||||
(let [props (:variant-properties component)
|
||||
props (d/remove-at-index props pos)
|
||||
main-id (:main-instance-id component)
|
||||
name (ctv/properties-to-name props)]
|
||||
(-> changes
|
||||
(pcb/update-component (:id component) #(assoc % :variant-properties props)
|
||||
{:apply-changes-local-library? true})
|
||||
(pcb/update-shapes [main-id] #(assoc % :variant-name name)))))
|
||||
changes
|
||||
related-components)))
|
||||
|
||||
|
||||
(defn generate-update-property-value
|
||||
[changes component-id pos value]
|
||||
(let [data (pcb/get-library-data changes)
|
||||
component (ctcl/get-component data component-id true)
|
||||
main-id (:main-instance-id component)
|
||||
name (-> (:variant-properties component)
|
||||
(update pos assoc :value value)
|
||||
ctv/properties-to-name)]
|
||||
(-> changes
|
||||
(pcb/update-component component-id #(assoc-in % [:variant-properties pos :value] value)
|
||||
{:apply-changes-local-library? true})
|
||||
(pcb/update-shapes [main-id] #(assoc % :variant-name name)))))
|
||||
|
||||
|
||||
(defn generate-set-variant-error
|
||||
[changes component-id value]
|
||||
(let [data (pcb/get-library-data changes)
|
||||
component (ctcl/get-component data component-id true)
|
||||
main-id (:main-instance-id component)]
|
||||
(-> changes
|
||||
(pcb/update-shapes [main-id] (if (str/blank? value)
|
||||
#(dissoc % :variant-error)
|
||||
#(assoc % :variant-error value))))))
|
||||
|
||||
|
||||
(defn generate-add-new-property
|
||||
[changes variant-id & {:keys [fill-values? property-name]}]
|
||||
(let [data (pcb/get-library-data changes)
|
||||
objects (pcb/get-objects changes)
|
||||
related-components (cfv/find-variant-components data objects variant-id)
|
||||
|
||||
props (-> related-components last :variant-properties)
|
||||
next-prop-num (ctv/next-property-number props)
|
||||
property-name (or property-name (str ctv/property-prefix next-prop-num))
|
||||
|
||||
[_ changes]
|
||||
(reduce (fn [[num changes] component]
|
||||
(let [main-id (:main-instance-id component)
|
||||
|
||||
update-props #(-> (d/nilv % [])
|
||||
(conj {:name property-name
|
||||
:value (if fill-values? (str ctv/value-prefix num) "")}))
|
||||
|
||||
update-name #(if fill-values?
|
||||
(if (str/empty? %)
|
||||
(str ctv/value-prefix num)
|
||||
(str % ", " ctv/value-prefix num))
|
||||
%)]
|
||||
[(inc num)
|
||||
(-> changes
|
||||
(pcb/update-component (:id component)
|
||||
#(update % :variant-properties update-props)
|
||||
{:apply-changes-local-library? true})
|
||||
(pcb/update-shapes [main-id] #(update % :variant-name update-name)))]))
|
||||
[1 changes]
|
||||
related-components)]
|
||||
changes))
|
||||
|
||||
(defn- generate-make-shape-no-variant
|
||||
[changes shape]
|
||||
(let [new-name (ctv/variant-name-to-name shape)
|
||||
[cpath cname] (cfh/parse-path-name new-name)]
|
||||
(-> changes
|
||||
(pcb/update-component (:component-id shape)
|
||||
#(-> (dissoc % :variant-id :variant-properties)
|
||||
(assoc :name cname
|
||||
:path cpath))
|
||||
{:apply-changes-local-library? true})
|
||||
(pcb/update-shapes [(:id shape)]
|
||||
#(-> (dissoc % :variant-id :variant-name)
|
||||
(assoc :name new-name))))))
|
||||
|
||||
(defn generate-make-shapes-no-variant
|
||||
[changes shapes]
|
||||
(reduce generate-make-shape-no-variant changes shapes))
|
||||
|
||||
|
||||
(defn- create-new-properties-from-variant
|
||||
[shape min-props data container-name base-properties]
|
||||
(let [component (ctcl/get-component data (:component-id shape) true)
|
||||
|
||||
add-name? (not= (:name component) container-name)
|
||||
props (ctv/merge-properties base-properties
|
||||
(:variant-properties component))
|
||||
new-props (- min-props
|
||||
(+ (count props)
|
||||
(if add-name? 1 0)))
|
||||
props (ctv/add-new-props props (repeat new-props ""))]
|
||||
|
||||
(if add-name?
|
||||
(ctv/add-new-prop props (:name component))
|
||||
props)))
|
||||
|
||||
(defn- create-new-properties-from-non-variant
|
||||
[shape min-props container-name base-properties]
|
||||
(let [;; Remove container name from shape name if present
|
||||
shape-name (ctv/remove-prefix (:name shape) container-name)]
|
||||
(ctv/path-to-properties shape-name base-properties min-props)))
|
||||
|
||||
|
||||
(defn generate-make-shapes-variant
|
||||
[changes shapes variant-container]
|
||||
(let [data (pcb/get-library-data changes)
|
||||
objects (pcb/get-objects changes)
|
||||
variant-id (:id variant-container)
|
||||
|
||||
;; If we are cut-pasting a variant-container, this will be null
|
||||
;; because it hasn't any shapes yet
|
||||
first-comp-id (->> variant-container
|
||||
:shapes
|
||||
first
|
||||
(get objects)
|
||||
:component-id)
|
||||
|
||||
base-props (->> (get-in data [:components first-comp-id :variant-properties])
|
||||
(map #(assoc % :value "")))
|
||||
num-base-props (count base-props)
|
||||
|
||||
[cpath cname] (cfh/parse-path-name (:name variant-container))
|
||||
container-name (:name variant-container)
|
||||
|
||||
create-new-properties
|
||||
(fn [shape min-props]
|
||||
(if (ctk/is-variant? shape)
|
||||
(create-new-properties-from-variant shape min-props data container-name base-props)
|
||||
(create-new-properties-from-non-variant shape min-props container-name base-props)))
|
||||
|
||||
total-props (reduce (fn [m shape]
|
||||
(max m (count (create-new-properties shape num-base-props))))
|
||||
0
|
||||
shapes)
|
||||
|
||||
num-new-props (if (or (zero? num-base-props)
|
||||
(< total-props num-base-props))
|
||||
0
|
||||
(- total-props num-base-props))
|
||||
|
||||
changes (nth
|
||||
(iterate #(generate-add-new-property % variant-id) changes)
|
||||
num-new-props)
|
||||
|
||||
changes (pcb/update-shapes changes (map :id shapes)
|
||||
#(assoc % :variant-id variant-id
|
||||
:name (:name variant-container)))]
|
||||
(reduce
|
||||
(fn [changes shape]
|
||||
(let [component (ctcl/get-component data (:component-id shape) true)]
|
||||
(if (or (zero? num-base-props) ;; do nothing if there are no base props
|
||||
(and (= variant-id (:variant-id shape)) ;; or we are only moving the shape inside its parent (it is
|
||||
(not (:deleted component)))) ;; the same parent and the component isn't deleted)
|
||||
changes
|
||||
(let [props (create-new-properties shape total-props)
|
||||
variant-name (ctv/properties-to-name props)]
|
||||
(-> (pcb/update-component changes
|
||||
(:component-id shape)
|
||||
#(assoc % :variant-id variant-id
|
||||
:variant-properties props
|
||||
:name cname
|
||||
:path cpath)
|
||||
{:apply-changes-local-library? true})
|
||||
(pcb/update-shapes [(:id shape)]
|
||||
#(assoc % :variant-name variant-name)))))))
|
||||
changes
|
||||
shapes)))
|
||||
@@ -1,160 +1,91 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
(ns app.common.logic.variants
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.files.changes-builder :as pcb]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.types.components-list :as ctcl]
|
||||
[cuerdas.core :as str]))
|
||||
[app.common.files.variant :as cfv]
|
||||
[app.common.logic.libraries :as cll]
|
||||
[app.common.logic.variant-properties :as clvp]
|
||||
[app.common.types.container :as ctn]
|
||||
[app.common.types.file :as ctf]
|
||||
[app.common.types.variant :as ctv]))
|
||||
|
||||
(defn generate-add-new-variant
|
||||
[changes shape variant-id new-component-id new-shape-id prop-num]
|
||||
(let [data (pcb/get-library-data changes)
|
||||
objects (pcb/get-objects changes)
|
||||
component-id (:component-id shape)
|
||||
value (str ctv/value-prefix
|
||||
(-> (cfv/extract-properties-values data objects variant-id)
|
||||
last
|
||||
:value
|
||||
count
|
||||
inc))
|
||||
|
||||
|
||||
(def property-prefix "Property")
|
||||
(def property-regex (re-pattern (str property-prefix "(\\d+)")))
|
||||
(def value-prefix "Value")
|
||||
|
||||
(defn find-related-components
|
||||
"Find a list of the components thet belongs to this variant-id"
|
||||
[data objects variant-id]
|
||||
(->> (dm/get-in objects [variant-id :shapes])
|
||||
(map #(dm/get-in objects [% :component-id]))
|
||||
(map #(ctcl/get-component data % true))
|
||||
reverse))
|
||||
|
||||
|
||||
(defn properties-to-name
|
||||
"Transform the properties into a name, with the values separated by comma"
|
||||
[properties]
|
||||
(->> properties
|
||||
(map :value)
|
||||
(remove str/empty?)
|
||||
(str/join ", ")))
|
||||
|
||||
|
||||
(defn next-property-number
|
||||
"Returns the next property number, to avoid duplicates on the property names"
|
||||
[properties]
|
||||
(let [numbers (keep
|
||||
#(some->> (:name %) (re-find property-regex) second d/parse-integer)
|
||||
properties)
|
||||
max-num (if (seq numbers)
|
||||
(apply max numbers)
|
||||
0)]
|
||||
(inc (max max-num (count properties)))))
|
||||
|
||||
|
||||
(defn path-to-properties
|
||||
"From a list of properties and a name with path, assign each token of the
|
||||
path as value of a different property"
|
||||
[path properties]
|
||||
(let [next-prop-num (next-property-number properties)
|
||||
cpath (cfh/split-path path)
|
||||
assigned (mapv #(assoc % :value (nth cpath %2 "")) properties (range))
|
||||
remaining (drop (count properties) cpath)
|
||||
new-properties (map-indexed (fn [i v] {:name (str property-prefix (+ next-prop-num i))
|
||||
:value v}) remaining)]
|
||||
(into assigned new-properties)))
|
||||
|
||||
(defn- dashes-to-end
|
||||
[property-values]
|
||||
(let [dashes (if (some #(= % "--") property-values) ["--"] [])]
|
||||
(concat (remove #(= % "--") property-values) dashes)))
|
||||
|
||||
|
||||
(defn extract-properties-values
|
||||
[data objects variant-id]
|
||||
(->> (find-related-components data objects variant-id)
|
||||
(mapcat :variant-properties)
|
||||
(group-by :name)
|
||||
(map (fn [[k v]]
|
||||
{:name k
|
||||
:value (->> v
|
||||
(map #(if (str/empty? (:value %)) "--" (:value %)))
|
||||
distinct
|
||||
dashes-to-end)}))))
|
||||
|
||||
|
||||
(defn generate-update-property-name
|
||||
[changes variant-id pos new-name]
|
||||
(let [data (pcb/get-library-data changes)
|
||||
objects (pcb/get-objects changes)
|
||||
related-components (find-related-components data objects variant-id)]
|
||||
(reduce (fn [changes component]
|
||||
(pcb/update-component
|
||||
changes (:id component)
|
||||
#(assoc-in % [:variant-properties pos :name] new-name)
|
||||
{:apply-changes-local-library? true}))
|
||||
changes
|
||||
related-components)))
|
||||
|
||||
|
||||
(defn generate-remove-property
|
||||
[changes variant-id pos]
|
||||
(let [data (pcb/get-library-data changes)
|
||||
objects (pcb/get-objects changes)
|
||||
related-components (find-related-components data objects variant-id)]
|
||||
(reduce (fn [changes component]
|
||||
(let [props (:variant-properties component)
|
||||
props (d/remove-at-index props pos)
|
||||
main-id (:main-instance-id component)
|
||||
name (properties-to-name props)]
|
||||
(-> changes
|
||||
(pcb/update-component (:id component) #(assoc % :variant-properties props)
|
||||
{:apply-changes-local-library? true})
|
||||
(pcb/update-shapes [main-id] #(assoc % :variant-name name)))))
|
||||
changes
|
||||
related-components)))
|
||||
|
||||
|
||||
(defn generate-update-property-value
|
||||
[changes component-id pos value]
|
||||
(let [data (pcb/get-library-data changes)
|
||||
component (ctcl/get-component data component-id true)
|
||||
main-id (:main-instance-id component)
|
||||
name (-> (:variant-properties component)
|
||||
(update pos assoc :value value)
|
||||
properties-to-name)]
|
||||
[new-shape changes] (-> changes
|
||||
(cll/generate-duplicate-component
|
||||
{:data data}
|
||||
component-id
|
||||
new-component-id
|
||||
{:new-shape-id new-shape-id :apply-changes-local-library? true}))]
|
||||
(-> changes
|
||||
(pcb/update-component component-id #(assoc-in % [:variant-properties pos :value] value)
|
||||
{:apply-changes-local-library? true})
|
||||
(pcb/update-shapes [main-id] #(assoc % :variant-name name)))))
|
||||
(clvp/generate-update-property-value new-component-id prop-num value)
|
||||
(pcb/change-parent (:parent-id shape) [new-shape] 0))))
|
||||
|
||||
(defn- generate-path
|
||||
[path objects base-id shape]
|
||||
(let [get-type #(case %
|
||||
:frame :container
|
||||
:group :container
|
||||
:rect :shape
|
||||
:circle :shape
|
||||
:bool :shape
|
||||
:path :shape
|
||||
%)]
|
||||
(if (= base-id (:id shape))
|
||||
path
|
||||
(generate-path (str path " " (:name shape) (get-type (:type shape))) objects base-id (get objects (:parent-id shape))))))
|
||||
|
||||
(defn- add-unique-path
|
||||
"Adds a new property :shape-path to the shape, with the path of the shape.
|
||||
Suffixes like -1, -2, etc. are added to ensure uniqueness."
|
||||
[shapes objects base-id]
|
||||
(letfn [(unique-path [shape counts]
|
||||
(let [path (generate-path "" objects base-id shape)
|
||||
num (get counts path 1)]
|
||||
[(str path "-" num) (update counts path (fnil inc 1))]))]
|
||||
(first
|
||||
(reduce
|
||||
(fn [[result counts] shape]
|
||||
(let [[shape-path counts'] (unique-path shape counts)]
|
||||
[(conj result (assoc shape :shape-path shape-path)) counts']))
|
||||
[[] {}]
|
||||
shapes))))
|
||||
|
||||
|
||||
(defn generate-add-new-property
|
||||
[changes variant-id & {:keys [fill-values?]}]
|
||||
(let [data (pcb/get-library-data changes)
|
||||
objects (pcb/get-objects changes)
|
||||
related-components (find-related-components data objects variant-id)
|
||||
(defn generate-keep-touched
|
||||
[changes new-shape original-shape original-shapes page libraries]
|
||||
(let [objects (pcb/get-objects changes)
|
||||
orig-objects (into {} (map (juxt :id identity) original-shapes))
|
||||
orig-shapes-w-path (add-unique-path
|
||||
(reverse original-shapes)
|
||||
orig-objects
|
||||
(:id original-shape))
|
||||
new-shapes-w-path (add-unique-path
|
||||
(reverse (cfh/get-children-with-self objects (:id new-shape)))
|
||||
objects
|
||||
(:id new-shape))
|
||||
new-shapes-map (into {} (map (juxt :shape-path identity) new-shapes-w-path))
|
||||
orig-touched (filter (comp seq :touched) orig-shapes-w-path)
|
||||
|
||||
props (-> related-components first :variant-properties)
|
||||
next-prop-num (next-property-number props)
|
||||
property-name (str property-prefix next-prop-num)
|
||||
|
||||
[_ changes]
|
||||
(reduce (fn [[num changes] component]
|
||||
(let [main-id (:main-instance-id component)
|
||||
|
||||
update-props #(-> (d/nilv % [])
|
||||
(conj {:name property-name
|
||||
:value (if fill-values? (str value-prefix num) "")}))
|
||||
|
||||
update-name #(if fill-values?
|
||||
(if (str/empty? %)
|
||||
(str value-prefix num)
|
||||
(str % ", " value-prefix num))
|
||||
%)]
|
||||
[(inc num)
|
||||
(-> changes
|
||||
(pcb/update-component (:id component)
|
||||
#(update % :variant-properties update-props)
|
||||
{:apply-changes-local-library? true})
|
||||
(pcb/update-shapes [main-id] #(update % :variant-name update-name)))]))
|
||||
[1 changes]
|
||||
related-components)]
|
||||
changes))
|
||||
container (ctn/make-container page :page)]
|
||||
(reduce
|
||||
(fn [changes touched-shape]
|
||||
(let [related-shape (get new-shapes-map (:shape-path touched-shape))
|
||||
orig-ref-shape (ctf/find-ref-shape nil container libraries touched-shape)]
|
||||
(if related-shape
|
||||
(cll/update-attrs-on-switch
|
||||
changes related-shape touched-shape new-shape original-shape orig-ref-shape container)
|
||||
changes)))
|
||||
changes
|
||||
orig-touched)))
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.common.media
|
||||
"Media assets helpers (images, fonts, etc)"
|
||||
(:require
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
;; We have added ".ttf" as string to solve a problem with chrome input selector
|
||||
@@ -48,38 +48,28 @@
|
||||
(defn mtype->extension [mtype]
|
||||
;; https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types
|
||||
(case mtype
|
||||
"image/apng" ".apng"
|
||||
"image/avif" ".avif"
|
||||
"image/gif" ".gif"
|
||||
"image/jpeg" ".jpg"
|
||||
"image/png" ".png"
|
||||
"image/svg+xml" ".svg"
|
||||
"image/webp" ".webp"
|
||||
"application/zip" ".zip"
|
||||
"application/penpot" ".penpot"
|
||||
"application/pdf" ".pdf"
|
||||
"text/plain" ".txt"
|
||||
"image/apng" ".apng"
|
||||
"image/avif" ".avif"
|
||||
"image/gif" ".gif"
|
||||
"image/jpeg" ".jpg"
|
||||
"image/png" ".png"
|
||||
"image/svg+xml" ".svg"
|
||||
"image/webp" ".webp"
|
||||
"application/zip" ".zip"
|
||||
"application/penpot" ".penpot"
|
||||
"application/pdf" ".pdf"
|
||||
"text/plain" ".txt"
|
||||
"font/woff" ".woff"
|
||||
"font/woff2" ".woff2"
|
||||
"font/ttf" ".ttf"
|
||||
"font/otf" ".otf"
|
||||
"application/octet-stream" ".bin"
|
||||
nil))
|
||||
|
||||
(s/def ::id uuid?)
|
||||
(s/def ::name string?)
|
||||
(s/def ::width number?)
|
||||
(s/def ::height number?)
|
||||
(s/def ::created-at inst?)
|
||||
(s/def ::modified-at inst?)
|
||||
(s/def ::mtype string?)
|
||||
(s/def ::uri string?)
|
||||
|
||||
(s/def ::media-object
|
||||
(s/keys :req-un [::id
|
||||
::name
|
||||
::width
|
||||
::height
|
||||
::mtype
|
||||
::created-at
|
||||
::modified-at
|
||||
::uri]))
|
||||
|
||||
(defn strip-image-extension
|
||||
[filename]
|
||||
(let [image-extensions-re #"(\.png)|(\.jpg)|(\.jpeg)|(\.webp)|(\.gif)|(\.svg)$"]
|
||||
(str/replace filename image-extensions-re "")))
|
||||
|
||||
(defn parse-font-weight
|
||||
[variant]
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#?(:cljs (:require-macros [app.common.schema :refer [ignoring]]))
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.math :as mth]
|
||||
[app.common.pprint :as pp]
|
||||
[app.common.schema.generators :as sg]
|
||||
[app.common.schema.openapi :as-alias oapi]
|
||||
@@ -27,10 +28,6 @@
|
||||
[malli.transform :as mt]
|
||||
[malli.util :as mu]))
|
||||
|
||||
(defprotocol ILazySchema
|
||||
(-validate [_ o])
|
||||
(-explain [_ o]))
|
||||
|
||||
(def default-options
|
||||
{:registry sr/default-registry})
|
||||
|
||||
@@ -50,10 +47,6 @@
|
||||
[s]
|
||||
(m/type-properties s))
|
||||
|
||||
(defn- lazy-schema?
|
||||
[s]
|
||||
(satisfies? ILazySchema s))
|
||||
|
||||
(defn schema
|
||||
[s]
|
||||
(if (schema? s)
|
||||
@@ -110,8 +103,16 @@
|
||||
(malli.error/error-value exp {:malli.error/mask-valid-values '...}))
|
||||
|
||||
(defn optional-keys
|
||||
[schema]
|
||||
(mu/optional-keys schema default-options))
|
||||
([schema]
|
||||
(mu/optional-keys schema nil default-options))
|
||||
([schema keys]
|
||||
(mu/optional-keys schema keys default-options)))
|
||||
|
||||
(defn required-keys
|
||||
([schema]
|
||||
(mu/required-keys schema nil default-options))
|
||||
([schema keys]
|
||||
(mu/required-keys schema keys default-options)))
|
||||
|
||||
(defn transformer
|
||||
[& transformers]
|
||||
@@ -145,11 +146,30 @@
|
||||
;; :else
|
||||
;; o))
|
||||
|
||||
(defn -transform-map-keys
|
||||
([f]
|
||||
(let [xform (map (fn [[k v]] [(f k) v]))]
|
||||
#(cond->> % (map? %) (into (empty %) xform))))
|
||||
([ks f]
|
||||
(let [xform (map (fn [[k v]] [(cond-> k (contains? ks k) f) v]))]
|
||||
#(cond->> % (map? %) (into (empty %) xform)))))
|
||||
|
||||
(defn json-transformer
|
||||
[]
|
||||
(mt/transformer
|
||||
(mt/json-transformer)
|
||||
(mt/collection-transformer)))
|
||||
(let [map-of-key-decoders (mt/-string-decoders)]
|
||||
(mt/transformer
|
||||
{:name :json
|
||||
:decoders (-> (mt/-json-decoders)
|
||||
(assoc :map-of {:compile (fn [schema _]
|
||||
(let [key-schema (some-> schema (m/children) (first))]
|
||||
(or (some-> key-schema (m/type) map-of-key-decoders
|
||||
(mt/-interceptor schema {}) (m/-intercepting)
|
||||
(m/-comp m/-keyword->string)
|
||||
(mt/-transform-if-valid key-schema)
|
||||
(-transform-map-keys))
|
||||
(-transform-map-keys m/-keyword->string))))}))
|
||||
:encoders (mt/-json-encoders)}
|
||||
(mt/collection-transformer))))
|
||||
|
||||
(defn string-transformer
|
||||
[]
|
||||
@@ -191,8 +211,7 @@
|
||||
|
||||
(defn lazy-validator
|
||||
[s]
|
||||
(let [s (schema s)
|
||||
vfn (delay (validator s))]
|
||||
(let [vfn (delay (validator s))]
|
||||
(fn [v] (@vfn v))))
|
||||
|
||||
(defn lazy-explainer
|
||||
@@ -205,6 +224,11 @@
|
||||
(let [vfn (delay (decoder (if (delay? s) (deref s) s) transformer))]
|
||||
(fn [v] (@vfn v))))
|
||||
|
||||
(defn decode-fn
|
||||
[s transformer]
|
||||
(let [vfn (delay (decoder (if (delay? s) (deref s) s) transformer))]
|
||||
(fn [v] (@vfn v))))
|
||||
|
||||
(defn humanize-explain
|
||||
"Returns a string representation of the explain data structure"
|
||||
[{:keys [errors value]} & {:keys [length level]}]
|
||||
@@ -250,38 +274,36 @@
|
||||
([s] (lookup sr/default-registry s))
|
||||
([registry s] (schema (mr/schema registry s))))
|
||||
|
||||
(defn- fast-check
|
||||
"A fast path for checking process, assumes the ILazySchema protocol
|
||||
implemented on the provided `s` schema. Sould not be used directly."
|
||||
[s type code hint value]
|
||||
(when-not ^boolean (-validate s value)
|
||||
(let [explain (-explain s value)]
|
||||
(throw (ex-info hint {:type type
|
||||
:code code
|
||||
:hint hint
|
||||
::explain explain}))))
|
||||
value)
|
||||
|
||||
(declare ^:private lazy-schema)
|
||||
|
||||
(defn check-fn
|
||||
"Create a predefined check function"
|
||||
[s & {:keys [hint type code]}]
|
||||
(let [schema (if (lazy-schema? s) s (lazy-schema s))
|
||||
hint (or ^boolean hint "check error")
|
||||
type (or ^boolean type :assertion)
|
||||
code (or ^boolean code :data-validation)]
|
||||
(partial fast-check schema type code hint)))
|
||||
(let [s (schema s)
|
||||
validator* (delay (m/validator s))
|
||||
explainer* (delay (m/explainer s))
|
||||
hint (or ^boolean hint "check error")
|
||||
type (or ^boolean type :assertion)
|
||||
code (or ^boolean code :data-validation)]
|
||||
|
||||
(fn [value]
|
||||
(let [validate-fn @validator*]
|
||||
(when-not ^boolean (validate-fn value)
|
||||
(let [explain-fn @explainer*
|
||||
explain (explain-fn value)]
|
||||
(throw (ex-info hint {:type type
|
||||
:code code
|
||||
:hint hint
|
||||
::explain explain}))))
|
||||
value))))
|
||||
|
||||
(defn check
|
||||
"A helper intended to be used on assertions for validate/check the
|
||||
schema over provided data. Raises an assertion exception."
|
||||
[s value & {:keys [hint type code]}]
|
||||
(let [s (if (lazy-schema? s) s (lazy-schema s))
|
||||
hint (or ^boolean hint "check error")
|
||||
type (or ^boolean type :assertion)
|
||||
code (or ^boolean code :data-validation)]
|
||||
(fast-check s type code hint value)))
|
||||
schema over provided data. Raises an assertion exception.
|
||||
|
||||
Use only on non-performance sensitive code, because it creates the
|
||||
check-fn instance all the time it is invoked."
|
||||
[s value & {:as opts}]
|
||||
(let [check-fn (check-fn s opts)]
|
||||
(check-fn value)))
|
||||
|
||||
(defn type-schema
|
||||
[& {:as params}]
|
||||
@@ -295,11 +317,14 @@
|
||||
([params]
|
||||
(cond
|
||||
(map? params)
|
||||
(let [type (get params :type)]
|
||||
(let [mdata (meta params)
|
||||
type (or (get mdata ::id)
|
||||
(get mdata ::type)
|
||||
(get params :type))]
|
||||
(assert (qualified-keyword? type) "expected qualified keyword for `type`")
|
||||
(let [s (m/-simple-schema params)]
|
||||
(swap! sr/registry assoc type s)
|
||||
nil))
|
||||
s))
|
||||
|
||||
(vector? params)
|
||||
(let [mdata (meta params)
|
||||
@@ -307,97 +332,41 @@
|
||||
(get mdata ::type))]
|
||||
(assert (qualified-keyword? type) "expected qualified keyword to be on metadata")
|
||||
(swap! sr/registry assoc type params)
|
||||
nil)
|
||||
params)
|
||||
|
||||
(m/into-schema? params)
|
||||
(let [type (m/-type params)]
|
||||
(swap! sr/registry assoc type params))
|
||||
(swap! sr/registry assoc type params)
|
||||
params)
|
||||
|
||||
:else
|
||||
(throw (ex-info "Invalid Arguments" {}))))
|
||||
|
||||
([type params]
|
||||
(let [s (if (map? params)
|
||||
(cond
|
||||
(= :set (:type params))
|
||||
(m/-collection-schema params)
|
||||
|
||||
(= :vector (:type params))
|
||||
(m/-collection-schema params)
|
||||
|
||||
:else
|
||||
(m/-simple-schema params))
|
||||
params)]
|
||||
|
||||
(swap! sr/registry assoc type s)
|
||||
nil)))
|
||||
|
||||
(defn- lazy-schema
|
||||
"Create ans instance of ILazySchema"
|
||||
[s]
|
||||
(let [schema (schema s)
|
||||
validator (delay (m/validator schema))
|
||||
explainer (delay (m/explainer schema))]
|
||||
|
||||
(reify
|
||||
m/AST
|
||||
(-to-ast [_ options] (m/-to-ast schema options))
|
||||
|
||||
m/EntrySchema
|
||||
(-entries [_] (m/-entries schema))
|
||||
(-entry-parser [_] (m/-entry-parser schema))
|
||||
|
||||
m/Cached
|
||||
(-cache [_] (m/-cache schema))
|
||||
|
||||
m/LensSchema
|
||||
(-keep [_] (m/-keep schema))
|
||||
(-get [_ key default] (m/-get schema key default))
|
||||
(-set [_ key value] (m/-set schema key value))
|
||||
|
||||
m/Schema
|
||||
(-validator [_]
|
||||
(m/-validator schema))
|
||||
(-explainer [_ path]
|
||||
(m/-explainer schema path))
|
||||
(-parser [_]
|
||||
(m/-parser schema))
|
||||
(-unparser [_]
|
||||
(m/-unparser schema))
|
||||
(-transformer [_ transformer method options]
|
||||
(m/-transformer schema transformer method options))
|
||||
(-walk [_ walker path options]
|
||||
(m/-walk schema walker path options))
|
||||
(-properties [_]
|
||||
(m/-properties schema))
|
||||
(-options [_]
|
||||
(m/-options schema))
|
||||
(-children [_]
|
||||
(m/-children schema))
|
||||
(-parent [_]
|
||||
(m/-parent schema))
|
||||
(-form [_]
|
||||
(m/-form schema))
|
||||
|
||||
ILazySchema
|
||||
(-validate [_ o]
|
||||
(@validator o))
|
||||
(-explain [_ o]
|
||||
(@explainer o)))))
|
||||
(swap! sr/registry assoc type params)
|
||||
params))
|
||||
|
||||
;; --- BUILTIN SCHEMAS
|
||||
|
||||
(register! :merge (mu/-merge))
|
||||
(register! :union (mu/-union))
|
||||
|
||||
(def uuid-rx
|
||||
#"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$")
|
||||
|
||||
(defn parse-uuid
|
||||
(defn- parse-uuid
|
||||
[s]
|
||||
(if (string? s)
|
||||
(some->> (re-matches uuid-rx s) uuid/uuid)
|
||||
s))
|
||||
(if (uuid? s)
|
||||
s
|
||||
(if (str/empty? s)
|
||||
nil
|
||||
(try
|
||||
(uuid/parse s)
|
||||
(catch #?(:clj Exception :cljs :default) _cause
|
||||
s)))))
|
||||
|
||||
(defn- encode-uuid
|
||||
[v]
|
||||
(if (uuid? v)
|
||||
(str v)
|
||||
v))
|
||||
|
||||
(register!
|
||||
{:type ::uuid
|
||||
@@ -409,8 +378,8 @@
|
||||
:gen/gen (sg/uuid)
|
||||
:decode/string parse-uuid
|
||||
:decode/json parse-uuid
|
||||
:encode/string str
|
||||
:encode/json str
|
||||
:encode/string encode-uuid
|
||||
:encode/json encode-uuid
|
||||
::oapi/type "string"
|
||||
::oapi/format "uuid"}})
|
||||
|
||||
@@ -801,7 +770,8 @@
|
||||
|
||||
gen (sg/one-of
|
||||
(sg/small-int :max max :min min)
|
||||
(sg/small-double :max max :min min))]
|
||||
(->> (sg/small-double :max max :min min)
|
||||
(sg/fmap #(mth/precision % 2))))]
|
||||
|
||||
{:pred pred
|
||||
:type-properties
|
||||
@@ -856,7 +826,7 @@
|
||||
choices))]
|
||||
{:pred pred
|
||||
:type-properties
|
||||
{:title "contains"
|
||||
{:title "contains any"
|
||||
:description "contains predicate"}}))})
|
||||
|
||||
(register!
|
||||
@@ -866,7 +836,7 @@
|
||||
{:title "inst"
|
||||
:description "Satisfies Inst protocol"
|
||||
:error/message "should be an instant"
|
||||
:gen/gen (->> (sg/small-int)
|
||||
:gen/gen (->> (sg/small-int :min 0 :max 100000)
|
||||
(sg/fmap (fn [v] (tm/parse-instant v))))
|
||||
|
||||
:decode/string tm/parse-instant
|
||||
@@ -876,6 +846,22 @@
|
||||
::oapi/type "string"
|
||||
::oapi/format "iso"}})
|
||||
|
||||
(register!
|
||||
{:type ::timestamp
|
||||
:pred inst?
|
||||
:type-properties
|
||||
{:title "inst"
|
||||
:description "Satisfies Inst protocol"
|
||||
:error/message "should be an instant"
|
||||
:gen/gen (->> (sg/small-int)
|
||||
(sg/fmap (fn [v] (tm/parse-instant v))))
|
||||
:decode/string tm/parse-instant
|
||||
:encode/string inst-ms
|
||||
:decode/json tm/parse-instant
|
||||
:encode/json inst-ms
|
||||
::oapi/type "string"
|
||||
::oapi/format "number"}})
|
||||
|
||||
(register!
|
||||
{:type ::fn
|
||||
:pred fn?})
|
||||
@@ -1011,6 +997,8 @@
|
||||
{:title "agent"
|
||||
:description "instance of clojure agent"}}))
|
||||
|
||||
(register! ::any (mu/update-properties :any assoc :gen/gen sg/any))
|
||||
|
||||
;; ---- PREDICATES
|
||||
|
||||
(def valid-safe-number?
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
(ns app.common.schema.desc-js-like
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.schema :as-alias sm]
|
||||
[cuerdas.core :as str]
|
||||
[malli.core :as m]
|
||||
[malli.util :as mu]))
|
||||
@@ -90,7 +91,7 @@
|
||||
(defmethod visit :int [_ schema _ _] (str "integer" (-titled schema) (-min-max-suffix-number schema)))
|
||||
(defmethod visit :double [_ schema _ _] (str "double" (-titled schema) (-min-max-suffix-number schema)))
|
||||
(defmethod visit :select-keys [_ schema _ options] (describe* (m/deref schema) options))
|
||||
(defmethod visit :and [_ s children _] (str (str/join ", and " children) (-titled s)))
|
||||
(defmethod visit :and [_ s children _] (str (str/join " && " children) (-titled s)))
|
||||
(defmethod visit :enum [_ s children _options] (str "enum" (-titled s) " of " (str/join ", " children)))
|
||||
(defmethod visit :maybe [_ _ children _] (str (first children) " nullable"))
|
||||
(defmethod visit :tuple [_ _ children _] (str "(" (str/join ", " children) ")"))
|
||||
@@ -106,7 +107,8 @@
|
||||
(defmethod visit :qualified-symbol [_ _ _ _] "qualified symbol")
|
||||
(defmethod visit :uuid [_ _ _ _] "uuid")
|
||||
(defmethod visit :boolean [_ _ _ _] "boolean")
|
||||
(defmethod visit :keyword [_ _ _ _] "keyword")
|
||||
(defmethod visit :keyword [_ _ _ _] "string")
|
||||
(defmethod visit :fn [_ _ _ _] "FN")
|
||||
|
||||
(defmethod visit :vector [_ _ children _]
|
||||
(str "[" (last children) "]"))
|
||||
@@ -123,10 +125,12 @@
|
||||
(defmethod visit :repeat [_ schema children _]
|
||||
(str "repeat " (-diamond (first children)) (-repeat-suffix schema)))
|
||||
|
||||
|
||||
(defmethod visit :set [_ schema children _]
|
||||
(str "set[" (first children) "]" (minmax-suffix schema)))
|
||||
|
||||
(defmethod visit ::sm/set [_ schema children _]
|
||||
(str "set[" (first children) "]" (minmax-suffix schema)))
|
||||
|
||||
(defmethod visit ::m/val [_ schema children _]
|
||||
(let [suffix (minmax-suffix schema)]
|
||||
(cond-> (first children)
|
||||
@@ -152,7 +156,6 @@
|
||||
(or (:title props)
|
||||
"*")))
|
||||
|
||||
|
||||
(defmethod visit :map
|
||||
[_ schema children {:keys [::level ::max-level] :as options}]
|
||||
(let [props (m/properties schema)
|
||||
@@ -172,13 +175,11 @@
|
||||
": " s)))
|
||||
(str/join ",\n"))
|
||||
|
||||
header (cond-> (if (zero? level)
|
||||
(str "type " title)
|
||||
(str title))
|
||||
header (cond-> (str "type " title)
|
||||
closed? (str "!")
|
||||
(some? title) (str " "))]
|
||||
|
||||
(str header "{\n" entries "\n" (pad "}" level))))))
|
||||
(str (pad header level) "{\n" entries "\n" (pad "}\n" level))))))
|
||||
|
||||
(defmethod visit :multi
|
||||
[_ s children {:keys [::level ::max-level] :as options}]
|
||||
@@ -205,18 +206,18 @@
|
||||
|
||||
(defmethod visit :merge
|
||||
[_ schema children _]
|
||||
(let [entries (str/join " , " children)
|
||||
(let [entries (str/join ",\n" children)
|
||||
props (m/properties schema)
|
||||
title (or (some-> (:title props) str/camel str/capital)
|
||||
"<untitled>")]
|
||||
(str "merge object " title " { " entries " }")))
|
||||
(str "merge type " title " { \n" entries "\n}\n")))
|
||||
|
||||
(defmethod visit :app.common.schema/one-of
|
||||
[_ _ children _]
|
||||
(defmethod visit ::sm/one-of
|
||||
[_ _ children _]
|
||||
(let [elems (last children)]
|
||||
(str "OneOf[" (->> elems
|
||||
(map d/name)
|
||||
(str/join ",")) "]")))
|
||||
(str "string oneOf (" (->> elems
|
||||
(map d/name)
|
||||
(str/join "|")) ")")))
|
||||
|
||||
(defmethod visit :schema [_ schema children options]
|
||||
(visit ::m/schema schema children options))
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.common.schema.generators
|
||||
(:refer-clojure :exclude [set subseq uuid filter map let boolean])
|
||||
(:refer-clojure :exclude [set subseq uuid filter map let boolean vector keyword int double])
|
||||
#?(:cljs (:require-macros [app.common.schema.generators]))
|
||||
(:require
|
||||
[app.common.schema.registry :as sr]
|
||||
@@ -38,10 +38,6 @@
|
||||
([s opts]
|
||||
(mg/generator s (assoc opts :registry sr/default-registry))))
|
||||
|
||||
(defn filter
|
||||
[pred gen]
|
||||
(tg/such-that pred gen 100))
|
||||
|
||||
(defn small-double
|
||||
[& {:keys [min max] :or {min -100 max 100}}]
|
||||
(tg/double* {:min min, :max max, :infinite? false, :NaN? false}))
|
||||
@@ -61,7 +57,7 @@
|
||||
(defn word-keyword
|
||||
[]
|
||||
(->> (word-string)
|
||||
(tg/fmap keyword)))
|
||||
(tg/fmap c/keyword)))
|
||||
|
||||
(defn email
|
||||
[]
|
||||
@@ -100,12 +96,11 @@
|
||||
(c/map second))
|
||||
(c/map list bools elements)))))))
|
||||
|
||||
(def any tg/any)
|
||||
(def boolean tg/boolean)
|
||||
|
||||
(defn set
|
||||
[g]
|
||||
(tg/set g))
|
||||
(defn map-of
|
||||
([kg vg]
|
||||
(tg/map kg vg {:min-elements 1 :max-elements 3}))
|
||||
([kg vg opts]
|
||||
(tg/map kg vg opts)))
|
||||
|
||||
(defn elements
|
||||
[s]
|
||||
@@ -119,6 +114,10 @@
|
||||
[f g]
|
||||
(tg/fmap f g))
|
||||
|
||||
(defn filter
|
||||
[pred gen]
|
||||
(tg/such-that pred gen 100))
|
||||
|
||||
(defn mcat
|
||||
[f g]
|
||||
(tg/bind g f))
|
||||
@@ -126,3 +125,22 @@
|
||||
(defn tuple
|
||||
[& opts]
|
||||
(apply tg/tuple opts))
|
||||
|
||||
(defn vector
|
||||
[& opts]
|
||||
(apply tg/vector opts))
|
||||
|
||||
(defn set
|
||||
[g]
|
||||
(tg/set g))
|
||||
|
||||
;; Static Generators
|
||||
|
||||
(def boolean tg/boolean)
|
||||
(def text (word-string))
|
||||
(def double (small-double))
|
||||
(def int (small-int))
|
||||
(def keyword (word-keyword))
|
||||
|
||||
(def any
|
||||
(tg/one-of [text boolean double int keyword]))
|
||||
|
||||
@@ -97,7 +97,8 @@
|
||||
(defmethod visit :enum [_ _ children options] (merge (some-> (m/-infer children) (transform* options)) {:enum children}))
|
||||
(defmethod visit :maybe [_ _ children _] {:oneOf (conj children {:type "null"})})
|
||||
(defmethod visit :tuple [_ _ children _] {:type "array", :items children, :additionalItems false})
|
||||
(defmethod visit :re [_ schema _ options] {:type "string", :pattern (first (m/children schema options))})
|
||||
(defmethod visit :re [_ schema _ options]
|
||||
{:type "string", :pattern (str (first (m/children schema options)))})
|
||||
(defmethod visit :nil [_ _ _ _] {:type "null"})
|
||||
|
||||
(defmethod visit :string [_ schema _ _]
|
||||
|
||||
@@ -56,12 +56,8 @@
|
||||
(str "(pass=TRUE, tests=" (:num-tests params) ", seed=" (:seed params) ", elapsed=" time "ms)"))))
|
||||
|
||||
(defmethod ct/report #?(:clj ::thrunk :cljs [:cljs.test/default ::thrunk])
|
||||
[{:keys [::params] :as m}]
|
||||
(let [smallest (-> params :shrunk :smallest vec)]
|
||||
(println)
|
||||
(println "Condition failed with the following params:")
|
||||
(println)
|
||||
(pp/pprint smallest)))
|
||||
[_]
|
||||
nil)
|
||||
|
||||
(defmethod ct/report #?(:clj ::trial :cljs [:cljs.test/default ::trial])
|
||||
[_]
|
||||
@@ -75,9 +71,12 @@
|
||||
(let [tvar (get-testing-var)
|
||||
tsym (get-testing-sym tvar)
|
||||
res (:result params)]
|
||||
(println)
|
||||
|
||||
(println "---------------------------------------------------------")
|
||||
(println "Generative test:" (str "'" tsym "'")
|
||||
(str "(pass=FALSE, tests=" (:num-tests params) ", seed=" (:seed params) ")"))
|
||||
(pp/pprint (:fail params))
|
||||
(println "---------------------------------------------------------")
|
||||
|
||||
(when (ex/exception? res)
|
||||
#?(:clj (ex/print-throwable res)
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
|
||||
(ns app.common.svg
|
||||
(:require
|
||||
#?(:clj [clojure.xml :as xml]
|
||||
:cljs [tubax.core :as tubax])
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
@@ -15,15 +13,7 @@
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.math :as mth]
|
||||
[app.common.uuid :as uuid]
|
||||
[cuerdas.core :as str])
|
||||
#?(:clj
|
||||
(:import
|
||||
clojure.lang.XMLHandler
|
||||
java.io.InputStream
|
||||
javax.xml.XMLConstants
|
||||
javax.xml.parsers.SAXParserFactory
|
||||
org.apache.commons.io.IOUtils)))
|
||||
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
;; Regex for XML ids per Spec
|
||||
;; https://www.w3.org/TR/2008/REC-xml-20081126/#sec-common-syn
|
||||
@@ -1030,24 +1020,3 @@
|
||||
:height (d/parse-integer (:height attrs) 0)})))]
|
||||
(reduce-nodes redfn [] svg-data)))
|
||||
|
||||
#?(:clj
|
||||
(defn- secure-parser-factory
|
||||
[^InputStream input ^XMLHandler handler]
|
||||
(.. (doto (SAXParserFactory/newInstance)
|
||||
(.setFeature XMLConstants/FEATURE_SECURE_PROCESSING true)
|
||||
(.setFeature "http://apache.org/xml/features/disallow-doctype-decl" true))
|
||||
(newSAXParser)
|
||||
(parse input handler))))
|
||||
|
||||
(defn strip-doctype
|
||||
[data]
|
||||
(cond-> data
|
||||
(str/includes? data "<!DOCTYPE")
|
||||
(str/replace #"<\!DOCTYPE[^>]*>" "")))
|
||||
|
||||
(defn parse
|
||||
[text]
|
||||
#?(:cljs (tubax/xml->clj text)
|
||||
:clj (let [text (strip-doctype text)]
|
||||
(dm/with-open [istream (IOUtils/toInputStream text "UTF-8")]
|
||||
(xml/parse istream secure-parser-factory)))))
|
||||
|
||||
@@ -40,76 +40,3 @@
|
||||
(map (fn [segment]
|
||||
(.toPersistentMap ^js segment)))
|
||||
(parser/parse path-str)))))
|
||||
|
||||
#?(:cljs
|
||||
(defn content->buffer
|
||||
"Converts the path content into binary format."
|
||||
[content]
|
||||
(let [total (count content)
|
||||
ssize 28
|
||||
buffer (new js/ArrayBuffer (* total ssize))
|
||||
dview (new js/DataView buffer)]
|
||||
(loop [index 0]
|
||||
(when (< index total)
|
||||
(let [segment (nth content index)
|
||||
offset (* index ssize)]
|
||||
(case (:command segment)
|
||||
:move-to
|
||||
(let [{:keys [x y]} (:params segment)]
|
||||
(.setInt16 dview (+ offset 0) 1)
|
||||
(.setFloat32 dview (+ offset 20) x)
|
||||
(.setFloat32 dview (+ offset 24) y))
|
||||
:line-to
|
||||
(let [{:keys [x y]} (:params segment)]
|
||||
(.setInt16 dview (+ offset 0) 2)
|
||||
(.setFloat32 dview (+ offset 20) x)
|
||||
(.setFloat32 dview (+ offset 24) y))
|
||||
:curve-to
|
||||
(let [{:keys [c1x c1y c2x c2y x y]} (:params segment)]
|
||||
(.setInt16 dview (+ offset 0) 3)
|
||||
(.setFloat32 dview (+ offset 4) c1x)
|
||||
(.setFloat32 dview (+ offset 8) c1y)
|
||||
(.setFloat32 dview (+ offset 12) c2x)
|
||||
(.setFloat32 dview (+ offset 16) c2y)
|
||||
(.setFloat32 dview (+ offset 20) x)
|
||||
(.setFloat32 dview (+ offset 24) y))
|
||||
|
||||
:close-path
|
||||
(.setInt16 dview (+ offset 0) 4))
|
||||
(recur (inc index)))))
|
||||
buffer)))
|
||||
|
||||
#?(:cljs
|
||||
(defn buffer->content
|
||||
"Converts the a buffer to a path content vector"
|
||||
[buffer]
|
||||
(assert (instance? js/ArrayBuffer buffer) "expected ArrayBuffer instance")
|
||||
(let [ssize 28
|
||||
total (/ (.-byteLength buffer) ssize)
|
||||
dview (new js/DataView buffer)]
|
||||
(loop [index 0
|
||||
result []]
|
||||
(if (< index total)
|
||||
(let [offset (* index ssize)
|
||||
type (.getInt16 dview (+ offset 0))
|
||||
command (case type
|
||||
1 :move-to
|
||||
2 :line-to
|
||||
3 :curve-to
|
||||
4 :close-path)
|
||||
params (case type
|
||||
1 {:x (.getFloat32 dview (+ offset 20))
|
||||
:y (.getFloat32 dview (+ offset 24))}
|
||||
2 {:x (.getFloat32 dview (+ offset 20))
|
||||
:y (.getFloat32 dview (+ offset 24))}
|
||||
3 {:c1x (.getFloat32 dview (+ offset 4))
|
||||
:c1y (.getFloat32 dview (+ offset 8))
|
||||
:c2x (.getFloat32 dview (+ offset 12))
|
||||
:c2y (.getFloat32 dview (+ offset 16))
|
||||
:x (.getFloat32 dview (+ offset 20))
|
||||
:y (.getFloat32 dview (+ offset 24))}
|
||||
4 {})]
|
||||
(recur (inc index)
|
||||
(conj result {:command command
|
||||
:params params})))
|
||||
result)))))
|
||||
|
||||
@@ -1,334 +0,0 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.common.svg.path.bool
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.rect :as grc]
|
||||
[app.common.geom.shapes.path :as gsp]
|
||||
[app.common.svg.path.command :as upc]
|
||||
[app.common.svg.path.subpath :as ups]))
|
||||
|
||||
(defn add-previous
|
||||
([content]
|
||||
(add-previous content nil))
|
||||
([content first]
|
||||
(->> (d/with-prev content)
|
||||
(mapv (fn [[cmd prev]]
|
||||
(cond-> cmd
|
||||
(and (nil? prev) (some? first))
|
||||
(assoc :prev first)
|
||||
|
||||
(some? prev)
|
||||
(assoc :prev (gsp/command->point prev))))))))
|
||||
|
||||
(defn close-paths
|
||||
"Removes the :close-path commands and replace them for line-to so we can calculate
|
||||
the intersections"
|
||||
[content]
|
||||
|
||||
(loop [head (first content)
|
||||
content (rest content)
|
||||
result []
|
||||
last-move nil
|
||||
last-p nil]
|
||||
|
||||
(if (nil? head)
|
||||
result
|
||||
(let [head-p (gsp/command->point head)
|
||||
head (cond
|
||||
(and (= :close-path (:command head))
|
||||
(or (nil? last-p) ;; Ignore consecutive close-paths
|
||||
(< (gpt/distance last-p last-move) 0.01)))
|
||||
nil
|
||||
|
||||
(= :close-path (:command head))
|
||||
(upc/make-line-to last-move)
|
||||
|
||||
:else
|
||||
head)]
|
||||
|
||||
(recur (first content)
|
||||
(rest content)
|
||||
(cond-> result (some? head) (conj head))
|
||||
(if (= :move-to (:command head))
|
||||
head-p
|
||||
last-move)
|
||||
head-p)))))
|
||||
|
||||
(defn- split-command
|
||||
[cmd values]
|
||||
(case (:command cmd)
|
||||
:line-to (gsp/split-line-to-ranges (:prev cmd) cmd values)
|
||||
:curve-to (gsp/split-curve-to-ranges (:prev cmd) cmd values)
|
||||
[cmd]))
|
||||
|
||||
(defn split-ts [seg-1 seg-2]
|
||||
(cond
|
||||
(and (= :line-to (:command seg-1))
|
||||
(= :line-to (:command seg-2)))
|
||||
(gsp/line-line-intersect (gsp/command->line seg-1) (gsp/command->line seg-2))
|
||||
|
||||
(and (= :line-to (:command seg-1))
|
||||
(= :curve-to (:command seg-2)))
|
||||
(gsp/line-curve-intersect (gsp/command->line seg-1) (gsp/command->bezier seg-2))
|
||||
|
||||
(and (= :curve-to (:command seg-1))
|
||||
(= :line-to (:command seg-2)))
|
||||
(let [[seg-2' seg-1']
|
||||
(gsp/line-curve-intersect (gsp/command->line seg-2) (gsp/command->bezier seg-1))]
|
||||
;; Need to reverse because we send the arguments reversed
|
||||
[seg-1' seg-2'])
|
||||
|
||||
(and (= :curve-to (:command seg-1))
|
||||
(= :curve-to (:command seg-2)))
|
||||
(gsp/curve-curve-intersect (gsp/command->bezier seg-1) (gsp/command->bezier seg-2))
|
||||
|
||||
:else
|
||||
[[] []]))
|
||||
|
||||
(defn content-intersect-split
|
||||
[content-a content-b sr-a sr-b]
|
||||
|
||||
(let [command->selrect (memoize gsp/command->selrect)]
|
||||
|
||||
(letfn [(overlap-segment-selrect?
|
||||
[segment selrect]
|
||||
(if (= :move-to (:command segment))
|
||||
false
|
||||
(let [r1 (command->selrect segment)]
|
||||
(grc/overlaps-rects? r1 selrect))))
|
||||
|
||||
(overlap-segments?
|
||||
[seg-1 seg-2]
|
||||
(if (or (= :move-to (:command seg-1))
|
||||
(= :move-to (:command seg-2)))
|
||||
false
|
||||
(let [r1 (command->selrect seg-1)
|
||||
r2 (command->selrect seg-2)]
|
||||
(grc/overlaps-rects? r1 r2))))
|
||||
|
||||
(split
|
||||
[seg-1 seg-2]
|
||||
(if (not (overlap-segments? seg-1 seg-2))
|
||||
[seg-1]
|
||||
(let [[ts-seg-1 _] (split-ts seg-1 seg-2)]
|
||||
(-> (split-command seg-1 ts-seg-1)
|
||||
(add-previous (:prev seg-1))))))
|
||||
|
||||
(split-segment-on-content
|
||||
[segment content content-sr]
|
||||
|
||||
(if (overlap-segment-selrect? segment content-sr)
|
||||
(->> content
|
||||
(filter #(overlap-segments? segment %))
|
||||
(reduce
|
||||
(fn [result current]
|
||||
(into [] (mapcat #(split % current)) result))
|
||||
[segment]))
|
||||
[segment]))
|
||||
|
||||
(split-content
|
||||
[content-a content-b sr-b]
|
||||
(into []
|
||||
(mapcat #(split-segment-on-content % content-b sr-b))
|
||||
content-a))]
|
||||
|
||||
[(split-content content-a content-b sr-b)
|
||||
(split-content content-b content-a sr-a)])))
|
||||
|
||||
(defn is-segment?
|
||||
[cmd]
|
||||
(and (contains? cmd :prev)
|
||||
(contains? #{:line-to :curve-to} (:command cmd))))
|
||||
|
||||
(defn contains-segment?
|
||||
[segment content content-sr content-geom]
|
||||
|
||||
(let [point (case (:command segment)
|
||||
:line-to (-> (gsp/command->line segment)
|
||||
(gsp/line-values 0.5))
|
||||
|
||||
:curve-to (-> (gsp/command->bezier segment)
|
||||
(gsp/curve-values 0.5)))]
|
||||
|
||||
(and (grc/contains-point? content-sr point)
|
||||
(or
|
||||
(gsp/is-point-in-geom-data? point content-geom)
|
||||
(gsp/is-point-in-border? point content)))))
|
||||
|
||||
(defn inside-segment?
|
||||
[segment content-sr content-geom]
|
||||
(let [point (case (:command segment)
|
||||
:line-to (-> (gsp/command->line segment)
|
||||
(gsp/line-values 0.5))
|
||||
|
||||
:curve-to (-> (gsp/command->bezier segment)
|
||||
(gsp/curve-values 0.5)))]
|
||||
|
||||
(and (grc/contains-point? content-sr point)
|
||||
(gsp/is-point-in-geom-data? point content-geom))))
|
||||
|
||||
(defn overlap-segment?
|
||||
"Finds if the current segment is overlapping against other
|
||||
segment meaning they have the same coordinates"
|
||||
[segment content]
|
||||
|
||||
(let [overlap-single?
|
||||
(fn [other]
|
||||
(when (and (= (:command segment) (:command other))
|
||||
(contains? #{:line-to :curve-to} (:command segment)))
|
||||
|
||||
(case (:command segment)
|
||||
:line-to (let [[p1 q1] (gsp/command->line segment)
|
||||
[p2 q2] (gsp/command->line other)]
|
||||
|
||||
(when (or (and (< (gpt/distance p1 p2) 0.1)
|
||||
(< (gpt/distance q1 q2) 0.1))
|
||||
(and (< (gpt/distance p1 q2) 0.1)
|
||||
(< (gpt/distance q1 p2) 0.1)))
|
||||
[segment other]))
|
||||
|
||||
:curve-to (let [[p1 q1 h11 h21] (gsp/command->bezier segment)
|
||||
[p2 q2 h12 h22] (gsp/command->bezier other)]
|
||||
|
||||
(when (or (and (< (gpt/distance p1 p2) 0.1)
|
||||
(< (gpt/distance q1 q2) 0.1)
|
||||
(< (gpt/distance h11 h12) 0.1)
|
||||
(< (gpt/distance h21 h22) 0.1))
|
||||
|
||||
(and (< (gpt/distance p1 q2) 0.1)
|
||||
(< (gpt/distance q1 p2) 0.1)
|
||||
(< (gpt/distance h11 h22) 0.1)
|
||||
(< (gpt/distance h21 h12) 0.1)))
|
||||
|
||||
[segment other])))))]
|
||||
|
||||
(->> content
|
||||
(d/seek overlap-single?)
|
||||
(some?))))
|
||||
|
||||
(defn fix-move-to
|
||||
[content]
|
||||
;; Remove the field `:prev` and makes the necessaries `move-to`
|
||||
;; then clean the subpaths
|
||||
|
||||
(loop [current (first content)
|
||||
content (rest content)
|
||||
prev nil
|
||||
result []]
|
||||
|
||||
(if (nil? current)
|
||||
result
|
||||
|
||||
(let [result (if (not= (:prev current) prev)
|
||||
(conj result (upc/make-move-to (:prev current)))
|
||||
result)]
|
||||
(recur (first content)
|
||||
(rest content)
|
||||
(gsp/command->point current)
|
||||
(conj result (dissoc current :prev)))))))
|
||||
|
||||
(defn create-union [content-a content-a-split content-b content-b-split sr-a sr-b]
|
||||
;; Pick all segments in content-a that are not inside content-b
|
||||
;; Pick all segments in content-b that are not inside content-a
|
||||
(let [content-a-geom (gsp/content->geom-data content-a)
|
||||
content-b-geom (gsp/content->geom-data content-b)
|
||||
|
||||
content
|
||||
(concat
|
||||
(->> content-a-split (filter #(not (contains-segment? % content-b sr-b content-b-geom))))
|
||||
(->> content-b-split (filter #(not (contains-segment? % content-a sr-a content-a-geom)))))
|
||||
|
||||
content-geom (gsp/content->geom-data content)
|
||||
|
||||
content-sr (gsp/content->selrect (fix-move-to content))
|
||||
|
||||
;; Overlapping segments should be added when they are part of the border
|
||||
border-content
|
||||
(->> content-b-split
|
||||
(filter #(and (contains-segment? % content-a sr-a content-a-geom)
|
||||
(overlap-segment? % content-a-split)
|
||||
(not (inside-segment? % content-sr content-geom)))))]
|
||||
|
||||
;; Ensure that the output is always a vector
|
||||
(d/concat-vec content border-content)))
|
||||
|
||||
(defn create-difference [content-a content-a-split content-b content-b-split sr-a sr-b]
|
||||
;; Pick all segments in content-a that are not inside content-b
|
||||
;; Pick all segments in content b that are inside content-a
|
||||
;; removing overlapping
|
||||
(let [content-a-geom (gsp/content->geom-data content-a)
|
||||
content-b-geom (gsp/content->geom-data content-b)]
|
||||
(d/concat-vec
|
||||
(->> content-a-split (filter #(not (contains-segment? % content-b sr-b content-b-geom))))
|
||||
|
||||
;; Reverse second content so we can have holes inside other shapes
|
||||
(->> content-b-split
|
||||
(filter #(and (contains-segment? % content-a sr-a content-a-geom)
|
||||
(not (overlap-segment? % content-a-split))))))))
|
||||
|
||||
(defn create-intersection [content-a content-a-split content-b content-b-split sr-a sr-b]
|
||||
;; Pick all segments in content-a that are inside content-b
|
||||
;; Pick all segments in content-b that are inside content-a
|
||||
(let [content-a-geom (gsp/content->geom-data content-a)
|
||||
content-b-geom (gsp/content->geom-data content-b)]
|
||||
(d/concat-vec
|
||||
(->> content-a-split (filter #(contains-segment? % content-b sr-b content-b-geom)))
|
||||
(->> content-b-split (filter #(contains-segment? % content-a sr-a content-a-geom))))))
|
||||
|
||||
|
||||
(defn create-exclusion [content-a content-b]
|
||||
;; Pick all segments
|
||||
(d/concat-vec content-a content-b))
|
||||
|
||||
(defn content-bool-pair
|
||||
[bool-type content-a content-b]
|
||||
|
||||
(let [;; We need to reverse the second path when making a difference/intersection/exclude
|
||||
;; and both shapes are in the same direction
|
||||
should-reverse? (and (not= :union bool-type)
|
||||
(= (ups/clockwise? content-b)
|
||||
(ups/clockwise? content-a)))
|
||||
|
||||
content-a (-> content-a
|
||||
(close-paths)
|
||||
(add-previous))
|
||||
|
||||
content-b (-> content-b
|
||||
(close-paths)
|
||||
(cond-> should-reverse? (ups/reverse-content))
|
||||
(add-previous))
|
||||
|
||||
sr-a (gsp/content->selrect content-a)
|
||||
sr-b (gsp/content->selrect content-b)
|
||||
|
||||
;; Split content in new segments in the intersection with the other path
|
||||
[content-a-split content-b-split] (content-intersect-split content-a content-b sr-a sr-b)
|
||||
content-a-split (->> content-a-split add-previous (filter is-segment?))
|
||||
content-b-split (->> content-b-split add-previous (filter is-segment?))
|
||||
|
||||
bool-content
|
||||
(case bool-type
|
||||
:union (create-union content-a content-a-split content-b content-b-split sr-a sr-b)
|
||||
:difference (create-difference content-a content-a-split content-b content-b-split sr-a sr-b)
|
||||
:intersection (create-intersection content-a content-a-split content-b content-b-split sr-a sr-b)
|
||||
:exclude (create-exclusion content-a-split content-b-split))]
|
||||
|
||||
(->> (fix-move-to bool-content)
|
||||
(ups/close-subpaths))))
|
||||
|
||||
(defn content-bool
|
||||
[bool-type contents]
|
||||
;; We apply the boolean operation in to each pair and the result to the next
|
||||
;; element
|
||||
(if (seq contents)
|
||||
(->> contents
|
||||
(reduce (partial content-bool-pair bool-type))
|
||||
(into []))
|
||||
[]))
|
||||
|
||||
@@ -1,204 +0,0 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.common.svg.path.command
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.point :as gpt]))
|
||||
|
||||
(defn command->point
|
||||
([prev-pos {:keys [relative params] :as command}]
|
||||
(let [{:keys [x y] :or {x (:x prev-pos) y (:y prev-pos)}} params]
|
||||
(if relative
|
||||
(-> prev-pos (update :x + x) (update :y + y))
|
||||
(command->point command))))
|
||||
|
||||
([command]
|
||||
(when command
|
||||
(let [{:keys [x y]} (:params command)]
|
||||
(gpt/point x y)))))
|
||||
|
||||
|
||||
(defn make-move-to [to]
|
||||
{:command :move-to
|
||||
:relative false
|
||||
:params {:x (:x to)
|
||||
:y (:y to)}})
|
||||
|
||||
(defn make-line-to [to]
|
||||
{:command :line-to
|
||||
:relative false
|
||||
:params {:x (:x to)
|
||||
:y (:y to)}})
|
||||
|
||||
(defn make-curve-params
|
||||
([point]
|
||||
(make-curve-params point point point))
|
||||
|
||||
([point handler] (make-curve-params point handler point))
|
||||
|
||||
([point h1 h2]
|
||||
{:x (:x point)
|
||||
:y (:y point)
|
||||
:c1x (:x h1)
|
||||
:c1y (:y h1)
|
||||
:c2x (:x h2)
|
||||
:c2y (:y h2)}))
|
||||
|
||||
(defn update-curve-to
|
||||
[command h1 h2]
|
||||
(let [params {:x (-> command :params :x)
|
||||
:y (-> command :params :y)
|
||||
:c1x (:x h1)
|
||||
:c1y (:y h1)
|
||||
:c2x (:x h2)
|
||||
:c2y (:y h2)}]
|
||||
(-> command
|
||||
(assoc :command :curve-to)
|
||||
(assoc :params params))))
|
||||
|
||||
(defn make-curve-to
|
||||
[to h1 h2]
|
||||
{:command :curve-to
|
||||
:relative false
|
||||
:params (make-curve-params to h1 h2)})
|
||||
|
||||
(defn update-handler
|
||||
[command prefix point]
|
||||
(let [[cox coy] (if (= prefix :c1) [:c1x :c1y] [:c2x :c2y])]
|
||||
(-> command
|
||||
(assoc-in [:params cox] (:x point))
|
||||
(assoc-in [:params coy] (:y point)))))
|
||||
|
||||
(defn apply-content-modifiers
|
||||
"Apply to content a map with point translations"
|
||||
[content modifiers]
|
||||
(letfn [(apply-to-index [content [index params]]
|
||||
(if (contains? content index)
|
||||
(cond-> content
|
||||
(and
|
||||
(or (:c1x params) (:c1y params) (:c2x params) (:c2y params))
|
||||
(= :line-to (get-in content [index :command])))
|
||||
|
||||
(-> (assoc-in [index :command] :curve-to)
|
||||
(assoc-in [index :params]
|
||||
(make-curve-params
|
||||
(get-in content [index :params])
|
||||
(get-in content [(dec index) :params]))))
|
||||
|
||||
(:x params) (update-in [index :params :x] + (:x params))
|
||||
(:y params) (update-in [index :params :y] + (:y params))
|
||||
|
||||
(:c1x params) (update-in [index :params :c1x] + (:c1x params))
|
||||
(:c1y params) (update-in [index :params :c1y] + (:c1y params))
|
||||
|
||||
(:c2x params) (update-in [index :params :c2x] + (:c2x params))
|
||||
(:c2y params) (update-in [index :params :c2y] + (:c2y params)))
|
||||
content))]
|
||||
(let [content (if (vector? content) content (into [] content))]
|
||||
(reduce apply-to-index content modifiers))))
|
||||
|
||||
(defn get-handler [{:keys [params] :as command} prefix]
|
||||
(let [cx (d/prefix-keyword prefix :x)
|
||||
cy (d/prefix-keyword prefix :y)]
|
||||
(when (and command
|
||||
(contains? params cx)
|
||||
(contains? params cy))
|
||||
(gpt/point (get params cx)
|
||||
(get params cy)))))
|
||||
|
||||
(defn content->handlers
|
||||
"Retrieve a map where for every point will retrieve a list of
|
||||
the handlers that are associated with that point.
|
||||
point -> [[index, prefix]]"
|
||||
[content]
|
||||
(->> (d/with-prev content)
|
||||
(d/enumerate)
|
||||
(mapcat (fn [[index [cur-cmd pre-cmd]]]
|
||||
(if (and pre-cmd (= :curve-to (:command cur-cmd)))
|
||||
(let [cur-pos (command->point cur-cmd)
|
||||
pre-pos (command->point pre-cmd)]
|
||||
(-> [[pre-pos [index :c1]]
|
||||
[cur-pos [index :c2]]]))
|
||||
[])))
|
||||
|
||||
(group-by first)
|
||||
(d/mapm #(mapv second %2))))
|
||||
|
||||
(defn point-indices
|
||||
[content point]
|
||||
(->> (d/enumerate content)
|
||||
(filter (fn [[_ cmd]] (= point (command->point cmd))))
|
||||
(mapv (fn [[index _]] index))))
|
||||
|
||||
(defn handler-indices
|
||||
"Return an index where the key is the positions and the values the handlers"
|
||||
[content point]
|
||||
(->> (d/with-prev content)
|
||||
(d/enumerate)
|
||||
(mapcat (fn [[index [cur-cmd pre-cmd]]]
|
||||
(if (and (some? pre-cmd) (= :curve-to (:command cur-cmd)))
|
||||
(let [cur-pos (command->point cur-cmd)
|
||||
pre-pos (command->point pre-cmd)]
|
||||
(cond-> []
|
||||
(= pre-pos point) (conj [index :c1])
|
||||
(= cur-pos point) (conj [index :c2])))
|
||||
[])))))
|
||||
|
||||
(defn opposite-index
|
||||
"Calculates the opposite index given a prefix and an index"
|
||||
[content index prefix]
|
||||
|
||||
(let [point (if (= prefix :c2)
|
||||
(command->point (nth content index))
|
||||
(command->point (nth content (dec index))))
|
||||
|
||||
point->handlers (content->handlers content)
|
||||
|
||||
handlers (->> point
|
||||
(point->handlers)
|
||||
(filter (fn [[ci cp]] (and (not= index ci) (not= prefix cp)))))]
|
||||
|
||||
(cond
|
||||
(= (count handlers) 1)
|
||||
(->> handlers first)
|
||||
|
||||
(and (= :c1 prefix) (= (count content) index))
|
||||
[(dec index) :c2]
|
||||
|
||||
:else nil)))
|
||||
|
||||
|
||||
(defn get-commands
|
||||
"Returns the commands involving a point with its indices"
|
||||
[content point]
|
||||
(->> (d/enumerate content)
|
||||
(filterv (fn [[_ cmd]] (= (command->point cmd) point)))))
|
||||
|
||||
|
||||
(defn prefix->coords [prefix]
|
||||
(case prefix
|
||||
:c1 [:c1x :c1y]
|
||||
:c2 [:c2x :c2y]
|
||||
nil))
|
||||
|
||||
(defn handler->point [content index prefix]
|
||||
(when (and (some? index)
|
||||
(some? prefix)
|
||||
(contains? content index))
|
||||
(let [[cx cy] (prefix->coords prefix)]
|
||||
(if (= :curve-to (get-in content [index :command]))
|
||||
(gpt/point (get-in content [index :params cx])
|
||||
(get-in content [index :params cy]))
|
||||
|
||||
(gpt/point (get-in content [index :params :x])
|
||||
(get-in content [index :params :y]))))))
|
||||
|
||||
(defn handler->node [content index prefix]
|
||||
(if (= prefix :c1)
|
||||
(command->point (get content (dec index)))
|
||||
(command->point (get content index))))
|
||||
|
||||
@@ -1,324 +0,0 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.common.svg.path.legacy-parser1
|
||||
"The first SVG Path parser implementation.
|
||||
|
||||
Written in a mix of CLJS and JS code and used in production until
|
||||
1.19, used mainly for tests."
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes.path :as upg]
|
||||
[app.common.svg :as csvg]
|
||||
[app.common.svg.path.arc-to-bezier :as a2b]
|
||||
[app.common.svg.path.command :as upc]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(def commands-regex #"(?i)[mzlhvcsqta][^mzlhvcsqta]*")
|
||||
|
||||
;; Matches numbers for path values allows values like... -.01, 10, +12.22
|
||||
;; 0 and 1 are special because can refer to flags
|
||||
(def num-regex #"[+-]?(\d+(\.\d+)?|\.\d+)(e[+-]?\d+)?")
|
||||
|
||||
(def flag-regex #"[01]")
|
||||
|
||||
(defn extract-params [cmd-str extract-commands]
|
||||
(loop [result []
|
||||
extract-idx 0
|
||||
current {}
|
||||
remain (-> cmd-str (subs 1) (str/trim))]
|
||||
|
||||
(let [[param type] (nth extract-commands extract-idx)
|
||||
regex (case type
|
||||
:flag flag-regex
|
||||
#_:number num-regex)
|
||||
match (re-find regex remain)]
|
||||
|
||||
(if match
|
||||
(let [value (-> match first csvg/fix-dot-number d/read-string)
|
||||
remain (str/replace-first remain regex "")
|
||||
current (assoc current param value)
|
||||
extract-idx (inc extract-idx)
|
||||
[result current extract-idx]
|
||||
(if (>= extract-idx (count extract-commands))
|
||||
[(conj result current) {} 0]
|
||||
[result current extract-idx])]
|
||||
(recur result
|
||||
extract-idx
|
||||
current
|
||||
remain))
|
||||
(cond-> result
|
||||
(seq current) (conj current))))))
|
||||
|
||||
;; Path specification
|
||||
;; https://www.w3.org/TR/SVG11/paths.html
|
||||
(defmulti parse-command (comp str/upper first))
|
||||
|
||||
(defmethod parse-command "M" [cmd]
|
||||
(let [relative (str/starts-with? cmd "m")
|
||||
param-list (extract-params cmd [[:x :number]
|
||||
[:y :number]])]
|
||||
|
||||
(into [{:command :move-to
|
||||
:relative relative
|
||||
:params (first param-list)}]
|
||||
|
||||
(for [params (rest param-list)]
|
||||
{:command :line-to
|
||||
:relative relative
|
||||
:params params}))))
|
||||
|
||||
(defmethod parse-command "Z" [_]
|
||||
[{:command :close-path}])
|
||||
|
||||
(defmethod parse-command "L" [cmd]
|
||||
(let [relative (str/starts-with? cmd "l")
|
||||
param-list (extract-params cmd [[:x :number]
|
||||
[:y :number]])]
|
||||
(for [params param-list]
|
||||
{:command :line-to
|
||||
:relative relative
|
||||
:params params})))
|
||||
|
||||
(defmethod parse-command "H" [cmd]
|
||||
(let [relative (str/starts-with? cmd "h")
|
||||
param-list (extract-params cmd [[:value :number]])]
|
||||
(for [params param-list]
|
||||
{:command :line-to-horizontal
|
||||
:relative relative
|
||||
:params params})))
|
||||
|
||||
(defmethod parse-command "V" [cmd]
|
||||
(let [relative (str/starts-with? cmd "v")
|
||||
param-list (extract-params cmd [[:value :number]])]
|
||||
(for [params param-list]
|
||||
{:command :line-to-vertical
|
||||
:relative relative
|
||||
:params params})))
|
||||
|
||||
(defmethod parse-command "C" [cmd]
|
||||
(let [relative (str/starts-with? cmd "c")
|
||||
param-list (extract-params cmd [[:c1x :number]
|
||||
[:c1y :number]
|
||||
[:c2x :number]
|
||||
[:c2y :number]
|
||||
[:x :number]
|
||||
[:y :number]])]
|
||||
(for [params param-list]
|
||||
{:command :curve-to
|
||||
:relative relative
|
||||
:params params})))
|
||||
|
||||
(defmethod parse-command "S" [cmd]
|
||||
(let [relative (str/starts-with? cmd "s")
|
||||
param-list (extract-params cmd [[:cx :number]
|
||||
[:cy :number]
|
||||
[:x :number]
|
||||
[:y :number]])]
|
||||
(for [params param-list]
|
||||
{:command :smooth-curve-to
|
||||
:relative relative
|
||||
:params params})))
|
||||
|
||||
(defmethod parse-command "Q" [cmd]
|
||||
(let [relative (str/starts-with? cmd "q")
|
||||
param-list (extract-params cmd [[:cx :number]
|
||||
[:cy :number]
|
||||
[:x :number]
|
||||
[:y :number]])]
|
||||
(for [params param-list]
|
||||
{:command :quadratic-bezier-curve-to
|
||||
:relative relative
|
||||
:params params})))
|
||||
|
||||
(defmethod parse-command "T" [cmd]
|
||||
(let [relative (str/starts-with? cmd "t")
|
||||
param-list (extract-params cmd [[:x :number]
|
||||
[:y :number]])]
|
||||
(for [params param-list]
|
||||
{:command :smooth-quadratic-bezier-curve-to
|
||||
:relative relative
|
||||
:params params})))
|
||||
|
||||
(defmethod parse-command "A" [cmd]
|
||||
(let [relative (str/starts-with? cmd "a")
|
||||
param-list (extract-params cmd [[:rx :number]
|
||||
[:ry :number]
|
||||
[:x-axis-rotation :number]
|
||||
[:large-arc-flag :flag]
|
||||
[:sweep-flag :flag]
|
||||
[:x :number]
|
||||
[:y :number]])]
|
||||
(for [params param-list]
|
||||
{:command :elliptical-arc
|
||||
:relative relative
|
||||
:params params})))
|
||||
|
||||
(defn smooth->curve
|
||||
[{:keys [params]} pos handler]
|
||||
(let [{c1x :x c1y :y} (upg/calculate-opposite-handler pos handler)]
|
||||
{:c1x c1x
|
||||
:c1y c1y
|
||||
:c2x (:cx params)
|
||||
:c2y (:cy params)}))
|
||||
|
||||
(defn quadratic->curve
|
||||
[sp ep cp]
|
||||
(let [cp1 (-> (gpt/to-vec sp cp)
|
||||
(gpt/scale (/ 2 3))
|
||||
(gpt/add sp))
|
||||
|
||||
cp2 (-> (gpt/to-vec ep cp)
|
||||
(gpt/scale (/ 2 3))
|
||||
(gpt/add ep))]
|
||||
|
||||
{:c1x (:x cp1)
|
||||
:c1y (:y cp1)
|
||||
:c2x (:x cp2)
|
||||
:c2y (:y cp2)}))
|
||||
|
||||
(defn arc->beziers*
|
||||
[from-x from-y x y large-arc-flag sweep-flag rx ry x-axis-rotation]
|
||||
(a2b/calculateBeziers from-x from-y x y large-arc-flag sweep-flag rx ry x-axis-rotation))
|
||||
|
||||
(defn arc->beziers [from-p command]
|
||||
(let [to-command
|
||||
(fn [[_ _ c1x c1y c2x c2y x y]]
|
||||
{:command :curve-to
|
||||
:relative (:relative command)
|
||||
:params {:c1x c1x :c1y c1y
|
||||
:c2x c2x :c2y c2y
|
||||
:x x :y y}})
|
||||
|
||||
{from-x :x from-y :y} from-p
|
||||
{:keys [rx ry x-axis-rotation large-arc-flag sweep-flag x y]} (:params command)
|
||||
result (arc->beziers* from-x from-y x y large-arc-flag sweep-flag rx ry x-axis-rotation)]
|
||||
(mapv to-command result)))
|
||||
|
||||
(defn simplify-commands
|
||||
"Removes some commands and convert relative to absolute coordinates"
|
||||
[commands]
|
||||
(let [simplify-command
|
||||
;; prev-pos : previous position for the current path. Necessary for relative commands
|
||||
;; prev-start : previous move-to necessary for Z commands
|
||||
;; prev-cc : previous command control point for cubic beziers
|
||||
;; prev-qc : previous command control point for quadratic curves
|
||||
(fn [[result prev-pos prev-start prev-cc prev-qc] [command _prev]]
|
||||
(let [command (assoc command :prev-pos prev-pos)
|
||||
|
||||
command
|
||||
(cond-> command
|
||||
(:relative command)
|
||||
(-> (assoc :relative false)
|
||||
(d/update-in-when [:params :c1x] + (:x prev-pos))
|
||||
(d/update-in-when [:params :c1y] + (:y prev-pos))
|
||||
|
||||
(d/update-in-when [:params :c2x] + (:x prev-pos))
|
||||
(d/update-in-when [:params :c2y] + (:y prev-pos))
|
||||
|
||||
(d/update-in-when [:params :cx] + (:x prev-pos))
|
||||
(d/update-in-when [:params :cy] + (:y prev-pos))
|
||||
|
||||
(d/update-in-when [:params :x] + (:x prev-pos))
|
||||
(d/update-in-when [:params :y] + (:y prev-pos))
|
||||
|
||||
(cond->
|
||||
(= :line-to-horizontal (:command command))
|
||||
(d/update-in-when [:params :value] + (:x prev-pos))
|
||||
|
||||
(= :line-to-vertical (:command command))
|
||||
(d/update-in-when [:params :value] + (:y prev-pos)))))
|
||||
|
||||
params (:params command)
|
||||
orig-command command
|
||||
|
||||
command
|
||||
(cond-> command
|
||||
(= :line-to-horizontal (:command command))
|
||||
(-> (assoc :command :line-to)
|
||||
(update :params dissoc :value)
|
||||
(assoc-in [:params :x] (:value params))
|
||||
(assoc-in [:params :y] (:y prev-pos)))
|
||||
|
||||
(= :line-to-vertical (:command command))
|
||||
(-> (assoc :command :line-to)
|
||||
(update :params dissoc :value)
|
||||
(assoc-in [:params :y] (:value params))
|
||||
(assoc-in [:params :x] (:x prev-pos)))
|
||||
|
||||
(= :smooth-curve-to (:command command))
|
||||
(-> (assoc :command :curve-to)
|
||||
(update :params dissoc :cx :cy)
|
||||
(update :params merge (smooth->curve command prev-pos prev-cc)))
|
||||
|
||||
(= :quadratic-bezier-curve-to (:command command))
|
||||
(-> (assoc :command :curve-to)
|
||||
(update :params dissoc :cx :cy)
|
||||
(update :params merge (quadratic->curve prev-pos (gpt/point params) (gpt/point (:cx params) (:cy params)))))
|
||||
|
||||
(= :smooth-quadratic-bezier-curve-to (:command command))
|
||||
(-> (assoc :command :curve-to)
|
||||
(update :params merge (quadratic->curve prev-pos (gpt/point params) (upg/calculate-opposite-handler prev-pos prev-qc)))))
|
||||
|
||||
result (if (= :elliptical-arc (:command command))
|
||||
(into result (arc->beziers prev-pos command))
|
||||
(conj result command))
|
||||
|
||||
next-cc (case (:command orig-command)
|
||||
:smooth-curve-to
|
||||
(gpt/point (get-in orig-command [:params :cx]) (get-in orig-command [:params :cy]))
|
||||
|
||||
:curve-to
|
||||
(gpt/point (get-in orig-command [:params :c2x]) (get-in orig-command [:params :c2y]))
|
||||
|
||||
(:line-to-horizontal :line-to-vertical)
|
||||
(gpt/point (get-in command [:params :x]) (get-in command [:params :y]))
|
||||
|
||||
(gpt/point (get-in orig-command [:params :x]) (get-in orig-command [:params :y])))
|
||||
|
||||
next-qc (case (:command orig-command)
|
||||
:quadratic-bezier-curve-to
|
||||
(gpt/point (get-in orig-command [:params :cx]) (get-in orig-command [:params :cy]))
|
||||
|
||||
:smooth-quadratic-bezier-curve-to
|
||||
(upg/calculate-opposite-handler prev-pos prev-qc)
|
||||
|
||||
(gpt/point (get-in orig-command [:params :x]) (get-in orig-command [:params :y])))
|
||||
|
||||
next-pos (if (= :close-path (:command command))
|
||||
prev-start
|
||||
(upc/command->point prev-pos command))
|
||||
|
||||
next-start (if (= :move-to (:command command)) next-pos prev-start)]
|
||||
|
||||
[result next-pos next-start next-cc next-qc]))
|
||||
|
||||
start (first commands)
|
||||
start (cond-> start
|
||||
(:relative start)
|
||||
(assoc :relative false))
|
||||
|
||||
start-pos (gpt/point (:params start))]
|
||||
|
||||
(->> (map vector (rest commands) commands)
|
||||
(reduce simplify-command [[start] start-pos start-pos start-pos start-pos])
|
||||
(first))))
|
||||
|
||||
(defn parse [path-str]
|
||||
(if (empty? path-str)
|
||||
path-str
|
||||
(let [clean-path-str
|
||||
(-> path-str
|
||||
(str/trim)
|
||||
;; Change "commas" for spaces
|
||||
(str/replace #"," " ")
|
||||
;; Remove all consecutive spaces
|
||||
(str/replace #"\s+" " "))
|
||||
commands (re-seq commands-regex clean-path-str)]
|
||||
(-> (mapcat parse-command commands)
|
||||
(simplify-commands)))))
|
||||
|
||||
@@ -12,15 +12,23 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes.path :as upg]
|
||||
[app.common.math :as mth]
|
||||
[app.common.svg :as csvg]
|
||||
[app.common.svg.path.command :as upc]
|
||||
[app.common.types.path.helpers :as path.helpers]
|
||||
[app.common.types.path.segment :as path.segment]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(def commands-regex #"(?i)[mzlhvcsqta][^mzlhvcsqta]*")
|
||||
(def regex #"[+-]?(\d+(\.\d+)?|\.\d+)(e[+-]?\d+)?")
|
||||
|
||||
(defn- get-point
|
||||
"Get a point for a segment"
|
||||
[prev-pos {:keys [relative params] :as segment}]
|
||||
(let [{:keys [x y] :or {x (:x prev-pos) y (:y prev-pos)}} params]
|
||||
(if relative
|
||||
(-> prev-pos (update :x + x) (update :y + y))
|
||||
(path.helpers/segment->point segment))))
|
||||
|
||||
(defn extract-params
|
||||
[data pattern]
|
||||
(loop [result []
|
||||
@@ -185,7 +193,7 @@
|
||||
|
||||
(defn smooth->curve
|
||||
[{:keys [params]} pos handler]
|
||||
(let [{c1x :x c1y :y} (upg/calculate-opposite-handler pos handler)]
|
||||
(let [{c1x :x c1y :y} (path.segment/calculate-opposite-handler pos handler)]
|
||||
{:c1x c1x
|
||||
:c1y c1y
|
||||
:c2x (:cx params)
|
||||
@@ -413,7 +421,7 @@
|
||||
|
||||
(= :smooth-quadratic-bezier-curve-to (:command command))
|
||||
(-> (assoc :command :curve-to)
|
||||
(update :params merge (quadratic->curve prev-pos (gpt/point params) (upg/calculate-opposite-handler prev-pos prev-qc)))))
|
||||
(update :params merge (quadratic->curve prev-pos (gpt/point params) (path.segment/calculate-opposite-handler prev-pos prev-qc)))))
|
||||
|
||||
result (if (= :elliptical-arc (:command command))
|
||||
(into result (arc->beziers prev-pos command))
|
||||
@@ -436,13 +444,13 @@
|
||||
(gpt/point (get-in orig-command [:params :cx]) (get-in orig-command [:params :cy]))
|
||||
|
||||
:smooth-quadratic-bezier-curve-to
|
||||
(upg/calculate-opposite-handler prev-pos prev-qc)
|
||||
(path.segment/calculate-opposite-handler prev-pos prev-qc)
|
||||
|
||||
(gpt/point (get-in orig-command [:params :x]) (get-in orig-command [:params :y])))
|
||||
|
||||
next-pos (if (= :close-path (:command command))
|
||||
prev-start
|
||||
(upc/command->point prev-pos command))
|
||||
(get-point prev-pos command))
|
||||
|
||||
next-start (if (= :move-to (:command command)) next-pos prev-start)]
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"Need that root is already a frame"
|
||||
(cfh/frame-shape? root))
|
||||
|
||||
(let [[_new-root _new-shapes updated-shapes]
|
||||
(let [[_new-root updated-shapes]
|
||||
(ctn/convert-shape-in-component root (:objects page) (:id file))
|
||||
|
||||
updated-root (first updated-shapes) ; Can't use new-root because it has a new id
|
||||
@@ -54,8 +54,7 @@
|
||||
:name name
|
||||
:path path
|
||||
:main-instance-id (:id updated-root)
|
||||
:main-instance-page (:id page)
|
||||
:shapes updated-shapes))))))))
|
||||
:main-instance-page (:id page)))))))))
|
||||
|
||||
(defn update-component
|
||||
[file component-label & {:keys [] :as params}]
|
||||
@@ -98,7 +97,6 @@
|
||||
component
|
||||
(:data library)
|
||||
(gpt/point 100 100)
|
||||
true
|
||||
{:force-id (thi/new-id! copy-root-label)
|
||||
:force-frame-id frame-id})
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
& {:keys [component-params root-params child-params]}]
|
||||
;; Generated shape tree:
|
||||
;; {:root-label} [:name Frame1] # [Component :component-label]
|
||||
;; :child-label [:name Rect1]
|
||||
;; :child-label [:name Rect1]
|
||||
(-> file
|
||||
(add-frame-with-child root-label child-label :frame-params root-params :child-params child-params)
|
||||
(thc/make-component component-label root-label component-params)))
|
||||
@@ -95,7 +95,7 @@
|
||||
& {:keys [component-params main-root-params main-child-params copy-root-params]}]
|
||||
;; Generated shape tree:
|
||||
;; {:main-root-label} [:name Frame1] # [Component :component-label]
|
||||
;; :main-child-label [:name Rect1]
|
||||
;; :main-child-label [:name Rect1]
|
||||
;;
|
||||
;; :copy-root-label [:name Frame1] #--> [Component :component-label] :main-root-label
|
||||
;; <no-label> [:name Rect1] ---> :main-child-label
|
||||
@@ -113,9 +113,9 @@
|
||||
& {:keys [component-params root-params child-params-list]}]
|
||||
;; Generated shape tree:
|
||||
;; {:root-label} [:name Frame1] # [Component :component-label]
|
||||
;; :child1-label [:name Rect1]
|
||||
;; :child2-label [:name Rect2]
|
||||
;; :child3-label [:name Rect3]
|
||||
;; :child1-label [:name Rect1]
|
||||
;; :child2-label [:name Rect2]
|
||||
;; :child3-label [:name Rect3]
|
||||
(as-> file $
|
||||
(add-frame $ root-label root-params)
|
||||
(reduce (fn [file [index [label params]]]
|
||||
@@ -134,9 +134,9 @@
|
||||
& {:keys [component-params main-root-params main-child-params-list copy-root-params]}]
|
||||
;; Generated shape tree:
|
||||
;; {:root-label} [:name Frame1] # [Component :component-label]
|
||||
;; :child1-label [:name Rect1]
|
||||
;; :child2-label [:name Rect2]
|
||||
;; :child3-label [:name Rect3]
|
||||
;; :child1-label [:name Rect1]
|
||||
;; :child2-label [:name Rect2]
|
||||
;; :child3-label [:name Rect3]
|
||||
;;
|
||||
;; :copy-root-label [:name Frame1] #--> [Component :component-label] :root-label
|
||||
;; <no-label> [:name Rect1] ---> :child1-label
|
||||
@@ -156,7 +156,7 @@
|
||||
& {:keys [component1-params root1-params main1-child-params component2-params main2-root-params nested-head-params]}]
|
||||
;; Generated shape tree:
|
||||
;; {:main1-root-label} [:name Frame1] # [Component :component1-label]
|
||||
;; :main1-child-label [:name Rect1]
|
||||
;; :main1-child-label [:name Rect1]
|
||||
;;
|
||||
;; {:main2-root-label} [:name Frame2] # [Component :component2-label]
|
||||
;; :nested-head-label [:name Frame1] @--> [Component :component1-label] :main1-root-label
|
||||
@@ -183,7 +183,7 @@
|
||||
& {:keys [component1-params root1-params main1-child-params component2-params main2-root-params nested-head-params copy2-root-params]}]
|
||||
;; Generated shape tree:
|
||||
;; {:main1-root-label} [:name Frame1] # [Component :component1-label]
|
||||
;; :main1-child-label [:name Rect1]
|
||||
;; :main1-child-label [:name Rect1]
|
||||
;;
|
||||
;; {:main2-root-label} [:name Frame2] # [Component :component2-label]
|
||||
;; :nested-head-label [:name Frame1] @--> [Component :component1-label] :main1-root-label
|
||||
@@ -336,8 +336,7 @@
|
||||
file
|
||||
{file-id file}
|
||||
(ctn/make-container container :page)
|
||||
(:id shape)
|
||||
true))
|
||||
(:id shape)))
|
||||
file' (thf/apply-changes file changes)]
|
||||
(if propagate-fn
|
||||
(propagate-fn file')
|
||||
@@ -361,7 +360,7 @@
|
||||
(:objects page)
|
||||
#{(-> (ths/get-shape file shape-tag :page-label page-label)
|
||||
:id)}
|
||||
{:components-v2 true})
|
||||
{})
|
||||
file' (thf/apply-changes file changes)]
|
||||
(if propagate-fn
|
||||
(propagate-fn file')
|
||||
@@ -380,7 +379,7 @@
|
||||
(gpt/point 0 0) ;; delta
|
||||
{(:id file) file} ;; libraries
|
||||
(:data file) ;; library-data
|
||||
(:id file)) ;; file-id
|
||||
(:id file)) ;; file-id
|
||||
(cll/generate-duplicate-changes-update-indices (:objects page) ;; objects
|
||||
#{(:id shape)}))
|
||||
file' (thf/apply-changes file changes)]
|
||||
|
||||
@@ -23,28 +23,32 @@
|
||||
|
||||
(defn sample-file
|
||||
[label & {:keys [page-label name view-only?] :as params}]
|
||||
(binding [ffeat/*current* #{"components/v2"}]
|
||||
(let [params (cond-> params
|
||||
label
|
||||
(assoc :id (thi/new-id! label))
|
||||
(let [params
|
||||
(cond-> params
|
||||
label
|
||||
(assoc :id (thi/new-id! label))
|
||||
|
||||
page-label
|
||||
(assoc :page-id (thi/new-id! page-label))
|
||||
(nil? name)
|
||||
(assoc :name "Test file")
|
||||
|
||||
(nil? name)
|
||||
(assoc :name "Test file"))
|
||||
:always
|
||||
(assoc :features ffeat/default-features))
|
||||
|
||||
file (-> (ctf/make-file (dissoc params :page-label))
|
||||
(assoc :features #{"components/v2"})
|
||||
(assoc :permissions {:can-edit (not (true? view-only?))}))
|
||||
opts
|
||||
(cond-> {}
|
||||
page-label
|
||||
(assoc :page-id (thi/new-id! page-label)))
|
||||
|
||||
page (-> file
|
||||
:data
|
||||
(ctpl/pages-seq)
|
||||
(first))]
|
||||
file (-> (ctf/make-file params opts)
|
||||
(assoc :permissions {:can-edit (not (true? view-only?))}))
|
||||
|
||||
(with-meta file
|
||||
{:current-page-id (:id page)}))))
|
||||
page (-> file
|
||||
:data
|
||||
(ctpl/pages-seq)
|
||||
(first))]
|
||||
|
||||
(with-meta file
|
||||
{:current-page-id (:id page)})))
|
||||
|
||||
(defn validate-file!
|
||||
([file] (validate-file! file {}))
|
||||
|
||||
@@ -22,4 +22,18 @@
|
||||
(thc/make-component component1-label root1-label)
|
||||
(thc/update-component component1-label {:variant-id variant-id :variant-properties [{:name "Property1" :value "Value1"}]})
|
||||
(thc/make-component component2-label root2-label)
|
||||
(thc/update-component component2-label {:variant-id variant-id :variant-properties [{:name "Property1" :value "Value1"}]}))))
|
||||
(thc/update-component component2-label {:variant-id variant-id :variant-properties [{:name "Property1" :value "Value2"}]}))))
|
||||
|
||||
(defn add-variant-two-properties
|
||||
[file variant-label component1-label root1-label component2-label root2-label
|
||||
& {:keys []}]
|
||||
(let [file (ths/add-sample-shape file variant-label :type :frame :is-variant-container true)
|
||||
variant-id (thi/id variant-label)]
|
||||
|
||||
(-> file
|
||||
(ths/add-sample-shape root2-label :type :frame :parent-label variant-label :variant-id variant-id :variant-name "p1v2, p2v2")
|
||||
(ths/add-sample-shape root1-label :type :frame :parent-label variant-label :variant-id variant-id :variant-name "p1v1, p2v1")
|
||||
(thc/make-component component1-label root1-label)
|
||||
(thc/update-component component1-label {:variant-id variant-id :variant-properties [{:name "Property1" :value "p1v1"} {:name "Property2" :value "p2v1"}]})
|
||||
(thc/make-component component2-label root2-label)
|
||||
(thc/update-component component2-label {:variant-id variant-id :variant-properties [{:name "Property1" :value "p1v2"} {:name "Property2" :value "p2v2"}]}))))
|
||||
|
||||
@@ -35,31 +35,32 @@
|
||||
(.. r (toString 16) (padStart 2 "0"))
|
||||
(.. g (toString 16) (padStart 2 "0"))
|
||||
(.. b (toString 16) (padStart 2 "0"))))))
|
||||
sg/any))
|
||||
sg/int))
|
||||
|
||||
(defn rgb-color-string?
|
||||
[o]
|
||||
(and (string? o) (some? (re-matches rgb-color-re o))))
|
||||
|
||||
(def ^:private type:rgb-color
|
||||
{:type :string
|
||||
:pred rgb-color-string?
|
||||
:type-properties
|
||||
{:title "rgb-color"
|
||||
:description "RGB Color String"
|
||||
:error/message "expected a valid RGB color"
|
||||
:error/code "errors.invalid-rgb-color"
|
||||
:gen/gen (generate-rgb-color)
|
||||
::oapi/type "integer"
|
||||
::oapi/format "int64"}})
|
||||
(def schema:rgb-color
|
||||
(sm/register!
|
||||
{:type ::rgb-color
|
||||
:pred rgb-color-string?
|
||||
:type-properties
|
||||
{:title "rgb-color"
|
||||
:description "RGB Color String"
|
||||
:error/message "expected a valid RGB color"
|
||||
:error/code "errors.invalid-rgb-color"
|
||||
:gen/gen (generate-rgb-color)
|
||||
::oapi/type "integer"
|
||||
::oapi/format "int64"}}))
|
||||
|
||||
(def schema:image-color
|
||||
(def schema:image
|
||||
[:map {:title "ImageColor"}
|
||||
[:name {:optional true} :string]
|
||||
[:width ::sm/int]
|
||||
[:height ::sm/int]
|
||||
[:mtype {:optional true} [:maybe :string]]
|
||||
[:mtype ::sm/text]
|
||||
[:id ::sm/uuid]
|
||||
[:name {:optional true} ::sm/text]
|
||||
[:keep-aspect-ratio {:optional true} :boolean]])
|
||||
|
||||
(def gradient-types
|
||||
@@ -76,7 +77,7 @@
|
||||
[:stops
|
||||
[:vector {:min 1 :gen/max 2}
|
||||
[:map {:title "GradientStop"}
|
||||
[:color ::rgb-color]
|
||||
[:color schema:rgb-color]
|
||||
[:opacity {:optional true} [:maybe ::sm/safe-number]]
|
||||
[:offset ::sm/safe-number]]]]])
|
||||
|
||||
@@ -86,13 +87,13 @@
|
||||
[:name {:optional true} :string]
|
||||
[:path {:optional true} [:maybe :string]]
|
||||
[:value {:optional true} [:maybe :string]]
|
||||
[:color {:optional true} [:maybe ::rgb-color]]
|
||||
[:color {:optional true} [:maybe schema:rgb-color]]
|
||||
[:opacity {:optional true} [:maybe ::sm/safe-number]]
|
||||
[:modified-at {:optional true} ::sm/inst]
|
||||
[:ref-id {:optional true} ::sm/uuid]
|
||||
[:ref-file {:optional true} ::sm/uuid]
|
||||
[:gradient {:optional true} [:maybe schema:gradient]]
|
||||
[:image {:optional true} [:maybe schema:image-color]]
|
||||
[:image {:optional true} [:maybe schema:image]]
|
||||
[:plugin-data {:optional true} ::ctpg/plugin-data]])
|
||||
|
||||
(def schema:color
|
||||
@@ -103,15 +104,21 @@
|
||||
[:and
|
||||
[:map {:title "RecentColor"}
|
||||
[:opacity {:optional true} [:maybe ::sm/safe-number]]
|
||||
[:color {:optional true} [:maybe ::rgb-color]]
|
||||
[:color {:optional true} [:maybe schema:rgb-color]]
|
||||
[:gradient {:optional true} [:maybe schema:gradient]]
|
||||
[:image {:optional true} [:maybe schema:image-color]]]
|
||||
[:image {:optional true} [:maybe schema:image]]]
|
||||
[::sm/contains-any {:strict true} [:color :gradient :image]]])
|
||||
|
||||
(sm/register! ::rgb-color type:rgb-color)
|
||||
;; Same as color but with :id prop required
|
||||
(def schema:library-color
|
||||
[:and
|
||||
(sm/required-keys schema:color-attrs [:id])
|
||||
[::sm/contains-any {:strict true} [:color :gradient :image]]])
|
||||
|
||||
;; FIXME: revisit if we really need this all registers
|
||||
(sm/register! ::color schema:color)
|
||||
(sm/register! ::gradient schema:gradient)
|
||||
(sm/register! ::image-color schema:image-color)
|
||||
(sm/register! ::image-color schema:image)
|
||||
(sm/register! ::recent-color schema:recent-color)
|
||||
(sm/register! ::color-attrs schema:color-attrs)
|
||||
|
||||
@@ -119,10 +126,13 @@
|
||||
(sm/lazy-validator schema:color))
|
||||
|
||||
(def check-color
|
||||
(sm/check-fn schema:color :hint "expected valid color struct"))
|
||||
(sm/check-fn schema:color :hint "expected valid color"))
|
||||
|
||||
(def check-library-color
|
||||
(sm/check-fn schema:library-color :hint "expected valid library color"))
|
||||
|
||||
(def check-recent-color
|
||||
(sm/check-fn schema:recent-color))
|
||||
(sm/check-fn schema:recent-color :hint "expected valid recent color"))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; HELPERS
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
[app.common.schema :as sm]
|
||||
[app.common.types.page :as ctp]
|
||||
[app.common.types.plugins :as ctpg]
|
||||
[app.common.types.variant :as ctv]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -17,17 +18,19 @@
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def schema:component
|
||||
[:map
|
||||
[:id ::sm/uuid]
|
||||
[:name :string]
|
||||
[:path {:optional true} [:maybe :string]]
|
||||
[:modified-at {:optional true} ::sm/inst]
|
||||
[:objects {:gen/max 10 :optional true} ::ctp/objects]
|
||||
[:main-instance-id ::sm/uuid]
|
||||
[:main-instance-page ::sm/uuid]
|
||||
[:plugin-data {:optional true} ::ctpg/plugin-data]])
|
||||
|
||||
(sm/register! ::component schema:component)
|
||||
(sm/register!
|
||||
^{::sm/type ::component}
|
||||
[:merge
|
||||
[:map
|
||||
[:id ::sm/uuid]
|
||||
[:name :string]
|
||||
[:path {:optional true} [:maybe :string]]
|
||||
[:modified-at {:optional true} ::sm/inst]
|
||||
[:objects {:gen/max 10 :optional true} ctp/schema:objects]
|
||||
[:main-instance-id ::sm/uuid]
|
||||
[:main-instance-page ::sm/uuid]
|
||||
[:plugin-data {:optional true} ctpg/schema:plugin-data]]
|
||||
ctv/schema:variant-component]))
|
||||
|
||||
(def check-component
|
||||
(sm/check-fn schema:component))
|
||||
@@ -90,8 +93,8 @@
|
||||
:constraints-h :constraints-group
|
||||
:constraints-v :constraints-group
|
||||
:fixed-scroll :constraints-group
|
||||
:bool-type :bool-group
|
||||
:bool-content :bool-group
|
||||
:bool-type :content-group
|
||||
:bool-content :content-group
|
||||
:exports :exports-group
|
||||
:grids :grids-group
|
||||
|
||||
@@ -179,10 +182,8 @@
|
||||
(= (:component-file shape) file-id)))
|
||||
|
||||
(defn is-main-of?
|
||||
[shape-main shape-inst components-v2]
|
||||
(or (= (:shape-ref shape-inst) (:id shape-main))
|
||||
(and (= (:shape-ref shape-inst) (:shape-ref shape-main))
|
||||
(not components-v2))))
|
||||
[shape-main shape-inst]
|
||||
(= (:shape-ref shape-inst) (:id shape-main)))
|
||||
|
||||
(defn main-instance?
|
||||
"Check if this shape is the root of the main instance of some
|
||||
@@ -286,7 +287,7 @@
|
||||
|
||||
(defn get-component-root
|
||||
[component]
|
||||
(if (true? (:main-instance-id component))
|
||||
(if (some? (:main-instance-id component))
|
||||
(get-in component [:objects (:main-instance-id component)])
|
||||
(get-in component [:objects (:id component)])))
|
||||
|
||||
@@ -333,8 +334,6 @@
|
||||
(let [parent (get objects (:parent-id shape))]
|
||||
;; We don't want to change the structure of component copies
|
||||
(and (not (in-component-copy-not-head? shape))
|
||||
;; We don't want to duplicate variants
|
||||
(not (is-variant? shape))
|
||||
;; Non instance, non copy. We allow
|
||||
(or (not (instance-head? shape))
|
||||
(not (in-component-copy? parent))))))
|
||||
|
||||
@@ -34,20 +34,12 @@
|
||||
(assoc component :modified-at (dt/now)))
|
||||
|
||||
(defn add-component
|
||||
[fdata {:keys [id name path main-instance-id main-instance-page shapes annotation variant-id variant-properties]}]
|
||||
(let [components-v2 (dm/get-in fdata [:options :components-v2])
|
||||
fdata (update fdata :components assoc id (touch {:id id :name name :path path}))]
|
||||
(if components-v2
|
||||
(cond-> (update-in fdata [:components id] assoc :main-instance-id main-instance-id :main-instance-page main-instance-page)
|
||||
annotation (update-in [:components id] assoc :annotation annotation)
|
||||
variant-id (update-in [:components id] assoc :variant-id variant-id)
|
||||
variant-properties (update-in [:components id] assoc :variant-properties variant-properties))
|
||||
|
||||
(let [wrap-object-fn cfeat/*wrap-with-objects-map-fn*]
|
||||
(assoc-in fdata [:components id :objects]
|
||||
(->> shapes
|
||||
(d/index-by :id)
|
||||
(wrap-object-fn)))))))
|
||||
[fdata {:keys [id name path main-instance-id main-instance-page annotation variant-id variant-properties]}]
|
||||
(let [fdata (update fdata :components assoc id (touch {:id id :name name :path path}))]
|
||||
(cond-> (update-in fdata [:components id] assoc :main-instance-id main-instance-id :main-instance-page main-instance-page)
|
||||
annotation (update-in [:components id] assoc :annotation annotation)
|
||||
variant-id (update-in [:components id] assoc :variant-id variant-id)
|
||||
variant-properties (update-in [:components id] assoc :variant-properties variant-properties))))
|
||||
|
||||
(defn mod-component
|
||||
[file-data {:keys [id name path main-instance-id main-instance-page objects annotation variant-id variant-properties modified-at]}]
|
||||
@@ -119,7 +111,6 @@
|
||||
[file-data component-id f & args]
|
||||
(d/update-in-when file-data [:components component-id] #(-> (apply f % args)
|
||||
(touch))))
|
||||
|
||||
(defn set-component-modified
|
||||
[file-data component-id]
|
||||
(update-component file-data component-id identity))
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
[:map-of {:gen/max 10} ::sm/uuid :map]]
|
||||
[:plugin-data {:optional true} ::ctpg/plugin-data]])
|
||||
|
||||
(def check-container!
|
||||
(def check-container
|
||||
(sm/check-fn ::container))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -62,9 +62,9 @@
|
||||
|
||||
(defn get-container
|
||||
[file type id]
|
||||
(dm/assert! (map? file))
|
||||
(dm/assert! (contains? valid-container-types type))
|
||||
(dm/assert! (uuid? id))
|
||||
(assert (map? file))
|
||||
(assert (contains? valid-container-types type))
|
||||
(assert (uuid? id))
|
||||
|
||||
(-> (if (= type :page)
|
||||
(ctpl/get-page file id)
|
||||
@@ -74,13 +74,9 @@
|
||||
(defn get-shape
|
||||
[container shape-id]
|
||||
|
||||
(dm/assert!
|
||||
"expected valid container"
|
||||
(check-container! container))
|
||||
|
||||
(dm/assert!
|
||||
"expected valid uuid for `shape-id`"
|
||||
(uuid? shape-id))
|
||||
(assert (check-container container))
|
||||
(assert (uuid? shape-id)
|
||||
"expected valid uuid for `shape-id`")
|
||||
|
||||
(-> container
|
||||
(get :objects)
|
||||
@@ -267,67 +263,8 @@
|
||||
new-children (->> (cfh/get-children objects (:id root))
|
||||
(map #(dissoc % :component-root)))]
|
||||
[(assoc new-root :id new-id)
|
||||
nil
|
||||
(into [new-root] new-children)]))
|
||||
|
||||
(defn make-component-shape ;; Only used for components v1
|
||||
"Clone the shape and all children. Generate new ids and detach
|
||||
from parent and frame. Update the original shapes to have links
|
||||
to the new ones."
|
||||
[shape objects file-id components-v2]
|
||||
(assert (nil? (:component-id shape)))
|
||||
(assert (nil? (:component-file shape)))
|
||||
(assert (nil? (:shape-ref shape)))
|
||||
(let [frame-ids-map (volatile! {})
|
||||
|
||||
;; Ensure that the component root is not an instance
|
||||
update-new-shape (fn [new-shape original-shape]
|
||||
(when (= (:type original-shape) :frame)
|
||||
(vswap! frame-ids-map assoc (:id original-shape) (:id new-shape)))
|
||||
|
||||
(cond-> new-shape
|
||||
true
|
||||
(dissoc :component-root)
|
||||
|
||||
(nil? (:parent-id new-shape))
|
||||
(dissoc :component-id
|
||||
:component-file
|
||||
:shape-ref)))
|
||||
|
||||
;; Make the original shape an instance of the new component.
|
||||
;; If one of the original shape children already was a component
|
||||
;; instance, maintain this instanceness untouched.
|
||||
update-original-shape (fn [original-shape new-shape]
|
||||
(cond-> original-shape
|
||||
(nil? (:shape-ref original-shape))
|
||||
(-> (assoc :shape-ref (:id new-shape))
|
||||
(dissoc :touched))
|
||||
|
||||
(nil? (:parent-id new-shape))
|
||||
(assoc :component-id (:id new-shape)
|
||||
:component-file file-id
|
||||
:component-root true)
|
||||
|
||||
(and (nil? (:parent-id new-shape)) components-v2)
|
||||
(assoc :main-instance true)
|
||||
|
||||
(some? (:parent-id new-shape))
|
||||
(dissoc :component-root)))
|
||||
|
||||
[new-root-shape new-shapes updated-shapes]
|
||||
(ctst/clone-shape shape
|
||||
nil
|
||||
objects
|
||||
:update-new-shape update-new-shape
|
||||
:update-original-shape update-original-shape)
|
||||
|
||||
;; If frame-id points to a shape inside the component, remap it to the
|
||||
;; corresponding new frame shape. If not, set it to nil.
|
||||
remap-frame-id (fn [shape]
|
||||
(update shape :frame-id #(get @frame-ids-map % nil)))]
|
||||
|
||||
[new-root-shape (map remap-frame-id new-shapes) updated-shapes]))
|
||||
|
||||
(defn remove-swap-keep-attrs
|
||||
"Remove flex children properties except the fit-content for flex layouts. These are properties
|
||||
that we don't have to propagate to copies but will be respected when swapping components"
|
||||
@@ -352,20 +289,18 @@
|
||||
WARNING: This process does not remap media references (on fills, strokes, ...); that is
|
||||
delegated to an async process on the backend side that checks unreferenced shapes and
|
||||
automatically creates correct references."
|
||||
([page component library-data position components-v2]
|
||||
(make-component-instance page component library-data position components-v2 {}))
|
||||
([page component library-data position components-v2
|
||||
([page component library-data position]
|
||||
(make-component-instance page component library-data position {}))
|
||||
([page component library-data position
|
||||
{:keys [main-instance? force-id force-frame-id keep-ids?]
|
||||
:or {main-instance? false force-id nil force-frame-id nil keep-ids? false}}]
|
||||
(let [component-page (when components-v2
|
||||
(ctpl/get-page library-data (:main-instance-page component)))
|
||||
(let [component-page (ctpl/get-page library-data (:main-instance-page component))
|
||||
|
||||
component-shape (-> (get-shape component-page (:main-instance-id component))
|
||||
(assoc :parent-id nil) ;; On v2 we force parent-id to nil in order to behave like v1
|
||||
(assoc :frame-id uuid/zero)
|
||||
(remove-swap-keep-attrs))
|
||||
|
||||
component-shape (if components-v2
|
||||
(-> (get-shape component-page (:main-instance-id component))
|
||||
(assoc :parent-id nil) ;; On v2 we force parent-id to nil in order to behave like v1
|
||||
(assoc :frame-id uuid/zero)
|
||||
(remove-swap-keep-attrs))
|
||||
(get-shape component (:id component)))
|
||||
|
||||
orig-pos (gpt/point (:x component-shape) (:y component-shape))
|
||||
delta (gpt/subtract position orig-pos)
|
||||
@@ -395,8 +330,7 @@
|
||||
update-new-shape
|
||||
(fn [new-shape original-shape]
|
||||
(let [new-name (:name new-shape)
|
||||
root? (or (ctk/instance-root? original-shape) ; If shape is inside a component (not components-v2)
|
||||
(nil? (:parent-id original-shape)))] ; we detect it by having no parent)
|
||||
root? (ctk/instance-root? original-shape)]
|
||||
|
||||
(when root?
|
||||
(vswap! unames conj new-name))
|
||||
@@ -417,10 +351,8 @@
|
||||
main-instance?
|
||||
(dissoc :shape-ref)
|
||||
|
||||
(and (not main-instance?)
|
||||
(or components-v2 ; In v1, shape-ref points to the remote instance
|
||||
(nil? (:shape-ref original-shape)))) ; in v2, shape-ref points to the near instance
|
||||
(assoc :shape-ref (:id original-shape))
|
||||
(not main-instance?)
|
||||
(assoc :shape-ref (:id original-shape)) ; shape-ref points to the near instance
|
||||
|
||||
(nil? (:parent-id original-shape))
|
||||
(assoc :component-id (:id component)
|
||||
@@ -428,14 +360,14 @@
|
||||
:component-root true
|
||||
:name new-name)
|
||||
|
||||
(or (some? (:parent-id original-shape)) ; On v2 we have removed the parent-id for component roots (see above)
|
||||
(or (some? (:parent-id original-shape)) ; On v2 we have removed the parent-id for component roots
|
||||
(some? component-frame))
|
||||
(dissoc :component-root))))
|
||||
|
||||
[new-shape new-shapes _]
|
||||
(ctst/clone-shape component-shape
|
||||
frame-id
|
||||
(if components-v2 (:objects component-page) (:objects component))
|
||||
(:objects component-page)
|
||||
:update-new-shape update-new-shape
|
||||
:force-id force-id
|
||||
:keep-ids? keep-ids?
|
||||
@@ -543,14 +475,41 @@
|
||||
;; We can always move the children to the parent they already have.
|
||||
;; But if we are pasting, those are new items, so it is considered a change
|
||||
no-changes?
|
||||
(and (->> children (every? #(= parent-id (:parent-id %))))
|
||||
(and (every? #(= parent-id (:parent-id %)) children)
|
||||
(not pasting?))
|
||||
|
||||
;; When pasting frames, children have the frames and their children
|
||||
;; We need to check only the top shapes
|
||||
children-ids (set (map :id children))
|
||||
top-children (remove #(contains? children-ids (:parent-id %)) children)
|
||||
|
||||
;; Are all the top-children a main-instance of a component?
|
||||
all-main?
|
||||
(->> children (every? #(ctk/main-instance? %)))]
|
||||
(every? ctk/main-instance? top-children)
|
||||
|
||||
any-main-descendant
|
||||
(some
|
||||
(fn [shape]
|
||||
(some ctk/main-instance? (cfh/get-children-with-self objects (:id shape))))
|
||||
children)
|
||||
|
||||
;; Are all the top-children a main-instance of a cutted component?
|
||||
all-comp-cut?
|
||||
(when all-main?
|
||||
(->> top-children
|
||||
(map #(ctkl/get-component (dm/get-in libraries [(:component-file %) :data])
|
||||
(:component-id %)
|
||||
true))
|
||||
(every? :deleted)))]
|
||||
(if (or no-changes?
|
||||
(and (not (invalid-structure-for-component? objects parent children pasting? libraries))
|
||||
;; If we are moving into a main component, no descendant can be main
|
||||
(or (nil? any-main-descendant) (not (ctk/main-instance? parent)))
|
||||
;; If we are moving into a variant-container, all the items should be main
|
||||
(or all-main? (not (ctk/is-variant-container? parent)))))
|
||||
;; so if we are pasting, only allow main instances that are cut-and-pasted
|
||||
(or (not (ctk/is-variant-container? parent))
|
||||
(and (not pasting?) all-main?)
|
||||
all-comp-cut?)))
|
||||
[parent-id (get-frame parent-id)]
|
||||
(recur (:parent-id parent) objects children pasting? libraries))))))
|
||||
|
||||
@@ -593,8 +552,7 @@
|
||||
|
||||
;; TODO: the check of :width and :height probably may be
|
||||
;; removed after the check added in
|
||||
;; data/workspace/modifiers/check-delta function. Better check
|
||||
;; it and test toroughly when activating components-v2 mode.
|
||||
;; data/workspace/modifiers/check-delta function.
|
||||
in-copy?
|
||||
(ctk/in-component-copy? shape)
|
||||
|
||||
|
||||
@@ -32,24 +32,31 @@
|
||||
[app.common.uuid :as uuid]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; CONSTANTS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defonce BASE-FONT-SIZE "16px")
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; SCHEMA
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def schema:media
|
||||
"A schema that represents the file media object"
|
||||
[:map {:title "FileMediaObject"}
|
||||
[:map {:title "FileMedia"}
|
||||
[:id ::sm/uuid]
|
||||
[:created-at ::sm/inst]
|
||||
[:created-at {:optional true} ::sm/inst]
|
||||
[:deleted-at {:optional true} ::sm/inst]
|
||||
[:name :string]
|
||||
[:width ::sm/safe-int]
|
||||
[:height ::sm/safe-int]
|
||||
[:mtype :string]
|
||||
[:file-id {:optional true} ::sm/uuid]
|
||||
[:media-id ::sm/uuid]
|
||||
[:file-id {:optional true} ::sm/uuid]
|
||||
[:thumbnail-id {:optional true} ::sm/uuid]
|
||||
[:is-local :boolean]])
|
||||
[:is-local {:optional true} :boolean]])
|
||||
|
||||
(def schema:colors
|
||||
[:map-of {:gen/max 5} ::sm/uuid ::ctc/color])
|
||||
@@ -65,7 +72,8 @@
|
||||
|
||||
(def schema:options
|
||||
[:map {:title "FileOptions"}
|
||||
[:components-v2 {:optional true} ::sm/boolean]])
|
||||
[:components-v2 {:optional true} ::sm/boolean]
|
||||
[:base-font-size {:optional true} :string]])
|
||||
|
||||
(def schema:data
|
||||
[:map {:title "FileData"}
|
||||
@@ -83,6 +91,7 @@
|
||||
because sometimes we want to validate file without the data."
|
||||
[:map {:title "file"}
|
||||
[:id ::sm/uuid]
|
||||
[:name :string]
|
||||
[:revn {:optional true} :int]
|
||||
[:vern {:optional true} :int]
|
||||
[:created-at {:optional true} ::sm/inst]
|
||||
@@ -102,12 +111,13 @@
|
||||
(sm/register! ::colors schema:colors)
|
||||
(sm/register! ::typographies schema:typographies)
|
||||
|
||||
(sm/register! ::media-object schema:media)
|
||||
(def check-file
|
||||
(sm/check-fn schema:file :hint "check error on validating file"))
|
||||
|
||||
(def check-file-data!
|
||||
(sm/check-fn ::data))
|
||||
(def check-file-data
|
||||
(sm/check-fn schema:data))
|
||||
|
||||
(def check-media-object!
|
||||
(def check-file-media
|
||||
(sm/check-fn schema:media))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -127,40 +137,44 @@
|
||||
(ctp/make-empty-page {:id page-id :name "Page 1"}))]
|
||||
|
||||
(cond-> (assoc empty-file-data :id file-id)
|
||||
(some? page-id)
|
||||
(some? page)
|
||||
(ctpl/add-page page)
|
||||
|
||||
(contains? cfeat/*current* "components/v2")
|
||||
(assoc-in [:options :components-v2] true)))))
|
||||
:always
|
||||
(update :options merge {:components-v2 true
|
||||
:base-font-size BASE-FONT-SIZE})))))
|
||||
|
||||
(defn make-file
|
||||
[{:keys [id project-id name revn is-shared features
|
||||
ignore-sync-until modified-at deleted-at
|
||||
create-page page-id]
|
||||
:or {is-shared false revn 0 create-page true}}]
|
||||
[{:keys [id project-id name revn is-shared features migrations
|
||||
ignore-sync-until modified-at deleted-at]
|
||||
:or {is-shared false revn 0}}
|
||||
|
||||
& {:keys [create-page page-id]
|
||||
:or {create-page true}}]
|
||||
|
||||
(let [id (or id (uuid/next))
|
||||
|
||||
data (if create-page
|
||||
(if page-id
|
||||
(make-file-data id page-id)
|
||||
(make-file-data id))
|
||||
(make-file-data id nil))
|
||||
|
||||
file {:id id
|
||||
:project-id project-id
|
||||
:name name
|
||||
:revn revn
|
||||
:vern 0
|
||||
:is-shared is-shared
|
||||
:version version
|
||||
:data data
|
||||
:features features
|
||||
:ignore-sync-until ignore-sync-until
|
||||
:modified-at modified-at
|
||||
:deleted-at deleted-at}]
|
||||
file (d/without-nils
|
||||
{:id id
|
||||
:project-id project-id
|
||||
:name name
|
||||
:revn revn
|
||||
:vern 0
|
||||
:is-shared is-shared
|
||||
:version version
|
||||
:data data
|
||||
:features features
|
||||
:migrations migrations
|
||||
:ignore-sync-until ignore-sync-until
|
||||
:modified-at modified-at
|
||||
:deleted-at deleted-at})]
|
||||
|
||||
(d/without-nils file)))
|
||||
(check-file file)))
|
||||
|
||||
;; Helpers
|
||||
|
||||
@@ -221,48 +235,45 @@
|
||||
(ctpl/get-page file-data (:main-instance-page component)))
|
||||
|
||||
(defn get-component-container
|
||||
"Retrieve the container that holds the component shapes (the page in components-v2
|
||||
or the component itself in v1 or deleted component)."
|
||||
"Retrieve the container that holds the component shapes (the page
|
||||
or the component itself on deleted component)."
|
||||
[file-data component]
|
||||
(let [components-v2 (dm/get-in file-data [:options :components-v2])]
|
||||
(if (and components-v2 (not (:deleted component)))
|
||||
(let [component-page (get-component-page file-data component)]
|
||||
(cfh/make-container component-page :page))
|
||||
(cfh/make-container component :component))))
|
||||
(if (not (:deleted component))
|
||||
(let [component-page (get-component-page file-data component)]
|
||||
(cfh/make-container component-page :page))
|
||||
(cfh/make-container component :component)))
|
||||
|
||||
(defn get-component-root
|
||||
"Retrieve the root shape of the component."
|
||||
[file-data component]
|
||||
(let [components-v2 (dm/get-in file-data [:options :components-v2])]
|
||||
(if (and components-v2 (not (:deleted component)))
|
||||
(-> file-data
|
||||
(get-component-page component)
|
||||
(ctn/get-shape (:main-instance-id component)))
|
||||
(ctk/get-component-root component))))
|
||||
(if (not (:deleted component))
|
||||
(-> file-data
|
||||
(get-component-page component)
|
||||
(ctn/get-shape (:main-instance-id component)))
|
||||
(ctk/get-component-root component)))
|
||||
|
||||
(defn get-component-shape
|
||||
"Retrieve one shape in the component by id. If with-context? is true, add the
|
||||
file and container where the shape resides in its metadata."
|
||||
[file-data component shape-id & {:keys [with-context?] :or {with-context? false}}]
|
||||
(let [components-v2 (dm/get-in file-data [:options :components-v2])]
|
||||
(if (and components-v2 (not (:deleted component)))
|
||||
(let [component-page (get-component-page file-data component)]
|
||||
(when component-page
|
||||
(let [child (cfh/get-child (:objects component-page)
|
||||
(:main-instance-id component)
|
||||
shape-id)]
|
||||
(cond-> child
|
||||
(and child with-context?)
|
||||
(with-meta {:file {:id (:id file-data)
|
||||
:data file-data}
|
||||
:container (ctn/make-container component-page :page)})))))
|
||||
(if (not (:deleted component))
|
||||
(let [component-page (get-component-page file-data component)]
|
||||
(when component-page
|
||||
(let [child (cfh/get-child (:objects component-page)
|
||||
(:main-instance-id component)
|
||||
shape-id)]
|
||||
(cond-> child
|
||||
(and child with-context?)
|
||||
(with-meta {:file {:id (:id file-data)
|
||||
:data file-data}
|
||||
:container (ctn/make-container component-page :page)})))))
|
||||
|
||||
(let [shape (dm/get-in component [:objects shape-id])]
|
||||
(cond-> shape
|
||||
(and shape with-context?)
|
||||
(with-meta {:file {:id (:id file-data)
|
||||
:data file-data}
|
||||
:container (ctn/make-container component :component)}))))))
|
||||
(let [shape (dm/get-in component [:objects shape-id])]
|
||||
(cond-> shape
|
||||
(and shape with-context?)
|
||||
(with-meta {:file {:id (:id file-data)
|
||||
:data file-data}
|
||||
:container (ctn/make-container component :component)})))))
|
||||
|
||||
(defn get-ref-shape
|
||||
"Retrieve the shape in the component that is referenced by the instance shape."
|
||||
@@ -384,12 +395,11 @@
|
||||
(defn get-component-shapes
|
||||
"Retrieve all shapes of the component"
|
||||
[file-data component]
|
||||
(let [components-v2 (dm/get-in file-data [:options :components-v2])]
|
||||
(if (and components-v2
|
||||
(not (:deleted component))) ;; the deleted components have its children in the :objects property
|
||||
(let [instance-page (get-component-page file-data component)]
|
||||
(cfh/get-children-with-self (:objects instance-page) (:main-instance-id component)))
|
||||
(vals (:objects component)))))
|
||||
|
||||
(if (not (:deleted component)) ;; the deleted components have its children in the :objects property
|
||||
(let [instance-page (get-component-page file-data component)]
|
||||
(cfh/get-children-with-self (:objects instance-page) (:main-instance-id component)))
|
||||
(vals (:objects component))))
|
||||
|
||||
;; Return true if the object is a component that exists on the file or its libraries (even a deleted one)
|
||||
(defn is-main-of-known-component?
|
||||
@@ -403,44 +413,52 @@
|
||||
|
||||
(defn load-component-objects
|
||||
"Add an :objects property to the component, with only the shapes that belong to it"
|
||||
[file-data component]
|
||||
(let [components-v2 (dm/get-in file-data [:options :components-v2])]
|
||||
(if (and components-v2 component (empty? (:objects component))) ;; This operation may be called twice, e.g. in an idempotent change
|
||||
(let [component-page (get-component-page file-data component)
|
||||
page-objects (:objects component-page)
|
||||
objects (->> (cons (:main-instance-id component)
|
||||
(cfh/get-children-ids page-objects (:main-instance-id component)))
|
||||
(map #(get page-objects %))
|
||||
(d/index-by :id))]
|
||||
(assoc component :objects objects))
|
||||
component)))
|
||||
([file-data component]
|
||||
(load-component-objects file-data component (gpt/point 0 0)))
|
||||
([file-data component delta]
|
||||
(if (and component (empty? (:objects component))) ;; This operation may be called twice, e.g. in an idempotent change
|
||||
(let [component-page (get-component-page file-data component)
|
||||
page-objects (:objects component-page)
|
||||
objects (->> (cons (:main-instance-id component)
|
||||
(cfh/get-children-ids page-objects (:main-instance-id component)))
|
||||
(map #(get page-objects %))
|
||||
;; when it is an undo of a cut-paste, we need to undo the movement
|
||||
;; of the shapes so we need to move them delta
|
||||
(map #(gsh/move % delta))
|
||||
(d/index-by :id))]
|
||||
(assoc component :objects objects))
|
||||
component)))
|
||||
|
||||
(defn delete-component
|
||||
"Mark a component as deleted and store the main instance shapes iside it, to
|
||||
be able to be recovered later."
|
||||
[file-data component-id skip-undelete? main-instance]
|
||||
(let [components-v2 (dm/get-in file-data [:options :components-v2])]
|
||||
(if (or (not components-v2) skip-undelete?)
|
||||
[file-data component-id skip-undelete? delta]
|
||||
(let [delta (or delta (gpt/point 0 0))]
|
||||
(if skip-undelete?
|
||||
(ctkl/delete-component file-data component-id)
|
||||
(let [set-main-instance ;; If there is a saved main-instance, restore it. This happens on the restore-component action
|
||||
#(if main-instance
|
||||
(assoc-in % [:objects (:main-instance-id %)] main-instance)
|
||||
%)]
|
||||
(-> file-data
|
||||
(ctkl/update-component component-id (partial load-component-objects file-data))
|
||||
(ctkl/update-component component-id set-main-instance)
|
||||
(ctkl/mark-component-deleted component-id))))))
|
||||
(-> file-data
|
||||
(ctkl/update-component component-id #(load-component-objects file-data % delta))
|
||||
(ctkl/mark-component-deleted component-id)))))
|
||||
|
||||
(defn restore-component
|
||||
"Recover a deleted component and all its shapes and put all this again in place."
|
||||
[file-data component-id page-id]
|
||||
(let [components-v2 (dm/get-in file-data [:options :components-v2])
|
||||
update-page? (and components-v2 (not (nil? page-id)))]
|
||||
(-> file-data
|
||||
(ctkl/update-component component-id #(dissoc % :objects))
|
||||
(ctkl/mark-component-undeleted component-id)
|
||||
(cond-> update-page?
|
||||
(ctkl/update-component component-id #(assoc % :main-instance-page page-id))))))
|
||||
(let [update-page? (not (nil? page-id))
|
||||
component (ctkl/get-component file-data component-id true)
|
||||
main-instance-page (or page-id (:main-instance-page component))
|
||||
main-instance (dm/get-in file-data [:pages-index main-instance-page
|
||||
:objects (:main-instance-id component)])]
|
||||
(cond-> file-data
|
||||
:always
|
||||
(->
|
||||
(ctkl/update-component component-id #(dissoc % :objects))
|
||||
(ctkl/mark-component-undeleted component-id))
|
||||
|
||||
update-page?
|
||||
(ctkl/update-component component-id #(assoc % :main-instance-page page-id))
|
||||
|
||||
(ctk/is-variant? component)
|
||||
(ctkl/update-component component-id #(assoc % :variant-id (:variant-id main-instance))))))
|
||||
|
||||
(defn purge-component
|
||||
"Remove permanently a component."
|
||||
@@ -557,7 +575,6 @@
|
||||
component
|
||||
library-data
|
||||
position
|
||||
(dm/get-in file-data [:options :components-v2])
|
||||
{:main-instance? true
|
||||
:keep-ids? true})
|
||||
|
||||
@@ -589,8 +606,7 @@
|
||||
:name (:name component)
|
||||
:path (:path component)
|
||||
:main-instance-id (:id main-instance-shape)
|
||||
:main-instance-page page-id
|
||||
:shapes (get-component-shapes library-data component)}))
|
||||
:main-instance-page page-id}))
|
||||
|
||||
; Change all existing instances to point to the local file
|
||||
remap-instances
|
||||
@@ -1021,3 +1037,14 @@
|
||||
|
||||
(-> file
|
||||
(update-in [:data :pages-index] detach-pages))))
|
||||
|
||||
;; Base font size
|
||||
|
||||
(defn get-base-font-size
|
||||
"Retrieve the base font size value or token reference."
|
||||
[file-data]
|
||||
(get-in file-data [:options :base-font-size] BASE-FONT-SIZE))
|
||||
|
||||
(defn set-base-font-size
|
||||
[file-data base-font-size]
|
||||
(assoc-in file-data [:options :base-font-size] base-font-size))
|
||||
@@ -70,7 +70,7 @@
|
||||
(def valid-guide?
|
||||
(sm/lazy-validator schema:guide))
|
||||
|
||||
(def check-page!
|
||||
(def check-page
|
||||
(sm/check-fn schema:page))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -82,8 +82,7 @@
|
||||
(def root uuid/zero)
|
||||
|
||||
(def empty-page-data
|
||||
{:options {}
|
||||
:objects {root
|
||||
{:objects {root
|
||||
(cts/setup-shape {:id root
|
||||
:type :frame
|
||||
:parent-id root
|
||||
@@ -91,10 +90,12 @@
|
||||
:name "Root Frame"})}})
|
||||
|
||||
(defn make-empty-page
|
||||
[{:keys [id name]}]
|
||||
[{:keys [id name background]}]
|
||||
(-> empty-page-data
|
||||
(assoc :id (or id (uuid/next)))
|
||||
(assoc :name (or name "Page 1"))))
|
||||
(assoc :name (d/nilv name "Page 1"))
|
||||
(cond-> background
|
||||
(assoc :background background))))
|
||||
|
||||
(defn get-frame-flow
|
||||
[flows frame-id]
|
||||
|
||||
219
common/src/app/common/types/path.cljc
Normal file
219
common/src/app/common/types/path.cljc
Normal file
@@ -0,0 +1,219 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.common.types.path
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.files.helpers :as cpf]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.rect :as grc]
|
||||
[app.common.geom.shapes.common :as gco]
|
||||
[app.common.types.path.bool :as bool]
|
||||
[app.common.types.path.helpers :as helpers]
|
||||
[app.common.types.path.impl :as impl]
|
||||
[app.common.types.path.segment :as segment]
|
||||
[app.common.types.path.shape-to-path :as stp]
|
||||
[app.common.types.path.subpath :as subpath]))
|
||||
|
||||
#?(:clj (set! *warn-on-reflection* true))
|
||||
|
||||
(defn content?
|
||||
[o]
|
||||
(impl/path-data? o))
|
||||
|
||||
(defn content
|
||||
"Create path content from plain data or bytes, returns itself if it
|
||||
is already PathData instance"
|
||||
[data]
|
||||
(impl/path-data data))
|
||||
|
||||
(defn from-bytes
|
||||
[data]
|
||||
(impl/from-bytes data))
|
||||
|
||||
(defn from-string
|
||||
[data]
|
||||
(impl/from-string data))
|
||||
|
||||
(defn check-path-content
|
||||
[content]
|
||||
(impl/check-content-like content))
|
||||
|
||||
(defn get-byte-size
|
||||
"Get byte size of a path content"
|
||||
[content]
|
||||
(impl/-get-byte-size content))
|
||||
|
||||
(defn write-to
|
||||
[content buffer offset]
|
||||
(impl/-write-to content buffer offset))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; TRANSFORMATIONS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn close-subpaths
|
||||
"Given a content, searches a path for possible subpaths that can
|
||||
create closed loops and merge them; then return the transformed path
|
||||
conten as PathData instance"
|
||||
[content]
|
||||
(-> (subpath/close-subpaths content)
|
||||
(impl/from-plain)))
|
||||
|
||||
(defn apply-content-modifiers
|
||||
"Apply delta modifiers over the path content"
|
||||
[content modifiers]
|
||||
(assert (impl/check-content-like content))
|
||||
|
||||
(letfn [(apply-to-index [content [index params]]
|
||||
(if (contains? content index)
|
||||
(cond-> content
|
||||
(and
|
||||
(or (:c1x params) (:c1y params) (:c2x params) (:c2y params))
|
||||
(= :line-to (get-in content [index :command])))
|
||||
|
||||
(-> (assoc-in [index :command] :curve-to)
|
||||
(assoc-in [index :params]
|
||||
(helpers/make-curve-params
|
||||
(get-in content [index :params])
|
||||
(get-in content [(dec index) :params]))))
|
||||
|
||||
(:x params) (update-in [index :params :x] + (:x params))
|
||||
(:y params) (update-in [index :params :y] + (:y params))
|
||||
|
||||
(:c1x params) (update-in [index :params :c1x] + (:c1x params))
|
||||
(:c1y params) (update-in [index :params :c1y] + (:c1y params))
|
||||
|
||||
(:c2x params) (update-in [index :params :c2x] + (:c2x params))
|
||||
(:c2y params) (update-in [index :params :c2y] + (:c2y params)))
|
||||
content))]
|
||||
|
||||
(impl/path-data
|
||||
(reduce apply-to-index (vec content) modifiers))))
|
||||
|
||||
(defn transform-content
|
||||
"Applies a transformation matrix over content and returns a new
|
||||
content as PathData instance."
|
||||
[content transform]
|
||||
(segment/transform-content content transform))
|
||||
|
||||
(defn move-content
|
||||
[content move-vec]
|
||||
(if (gpt/zero? move-vec)
|
||||
content
|
||||
(segment/move-content content move-vec)))
|
||||
|
||||
(defn update-geometry
|
||||
"Update shape with new geometry calculated from provided content"
|
||||
([shape content]
|
||||
(update-geometry (assoc shape :content content)))
|
||||
([shape]
|
||||
(let [flip-x
|
||||
(get shape :flip-x)
|
||||
|
||||
flip-y
|
||||
(get shape :flip-y)
|
||||
|
||||
;; NOTE: we ensure that content is PathData instance
|
||||
content
|
||||
(impl/path-data
|
||||
(get shape :content))
|
||||
|
||||
;; Ensure plain format once
|
||||
transform
|
||||
(cond-> (:transform shape (gmt/matrix))
|
||||
flip-x (gmt/scale (gpt/point -1 1))
|
||||
flip-y (gmt/scale (gpt/point 1 -1)))
|
||||
|
||||
transform-inverse
|
||||
(cond-> (gmt/matrix)
|
||||
flip-x (gmt/scale (gpt/point -1 1))
|
||||
flip-y (gmt/scale (gpt/point 1 -1))
|
||||
:always (gmt/multiply (:transform-inverse shape (gmt/matrix))))
|
||||
|
||||
center
|
||||
(or (some-> (dm/get-prop shape :selrect) grc/rect->center)
|
||||
(segment/content-center content))
|
||||
|
||||
base-content
|
||||
(segment/transform-content content (gmt/transform-in center transform-inverse))
|
||||
|
||||
;; Calculates the new selrect with points given the old center
|
||||
points
|
||||
(-> (segment/content->selrect base-content)
|
||||
(grc/rect->points)
|
||||
(gco/transform-points center transform))
|
||||
|
||||
points-center
|
||||
(gco/points->center points)
|
||||
|
||||
;; Points is now the selrect but the center is different so we can create the selrect
|
||||
;; through points
|
||||
selrect
|
||||
(-> points
|
||||
(gco/transform-points points-center transform-inverse)
|
||||
(grc/points->rect))]
|
||||
|
||||
(-> shape
|
||||
(assoc :content content)
|
||||
(assoc :points points)
|
||||
(assoc :selrect selrect)))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; PATH SHAPE HELPERS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn get-points
|
||||
"Returns points for the given segment, faster version of
|
||||
the `content->points`."
|
||||
[content]
|
||||
(some-> content segment/get-points))
|
||||
|
||||
(defn- calc-bool-content*
|
||||
"Calculate the boolean content from shape and objects. Returns plain
|
||||
vector of segments"
|
||||
[shape objects]
|
||||
(let [extract-content-xf
|
||||
(comp (map (d/getf objects))
|
||||
(remove :hidden)
|
||||
(remove cpf/svg-raw-shape?)
|
||||
(map #(stp/convert-to-path % objects))
|
||||
(map :content))
|
||||
|
||||
contents
|
||||
(sequence extract-content-xf (:shapes shape))]
|
||||
|
||||
(bool/calculate-content (:bool-type shape) contents)))
|
||||
|
||||
(defn calc-bool-content
|
||||
"Calculate the boolean content from shape and objects. Returns a
|
||||
packed PathData instance"
|
||||
[shape objects]
|
||||
(-> (calc-bool-content* shape objects)
|
||||
(impl/path-data)))
|
||||
|
||||
(defn shape-with-open-path?
|
||||
[shape]
|
||||
(let [svg? (contains? shape :svg-attrs)
|
||||
;; No close subpaths for svgs imported
|
||||
maybe-close (if svg? identity subpath/close-subpaths)]
|
||||
(and (= :path (:type shape))
|
||||
(not (->> shape
|
||||
:content
|
||||
(maybe-close)
|
||||
(subpath/get-subpaths)
|
||||
(every? subpath/is-closed?))))))
|
||||
|
||||
(defn convert-to-path
|
||||
"Transform a shape to a path shape"
|
||||
([shape]
|
||||
(convert-to-path shape {}))
|
||||
([shape objects]
|
||||
(-> (stp/convert-to-path shape objects)
|
||||
(update :content impl/path-data))))
|
||||
|
||||
438
common/src/app/common/types/path/bool.cljc
Normal file
438
common/src/app/common/types/path/bool.cljc
Normal file
@@ -0,0 +1,438 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.common.types.path.bool
|
||||
(:require
|
||||
[app.common.colors :as clr]
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.rect :as grc]
|
||||
[app.common.math :as mth]
|
||||
[app.common.types.path.helpers :as helpers]
|
||||
[app.common.types.path.segment :as segment]
|
||||
[app.common.types.path.subpath :as subpath]))
|
||||
|
||||
(def default-fills
|
||||
[{:fill-color clr/black}])
|
||||
|
||||
(def style-group-properties
|
||||
[:shadow :blur])
|
||||
|
||||
(def style-properties
|
||||
(into style-group-properties
|
||||
[:fill-color
|
||||
:fill-opacity
|
||||
:fill-color-gradient
|
||||
:fill-color-ref-file
|
||||
:fill-color-ref-id
|
||||
:fill-image
|
||||
:fills
|
||||
:stroke-color
|
||||
:stroke-color-ref-file
|
||||
:stroke-color-ref-id
|
||||
:stroke-opacity
|
||||
:stroke-style
|
||||
:stroke-width
|
||||
:stroke-alignment
|
||||
:stroke-cap-start
|
||||
:stroke-cap-end
|
||||
:strokes]))
|
||||
|
||||
(defn add-previous
|
||||
([content]
|
||||
(add-previous content nil))
|
||||
([content first]
|
||||
(->> (d/with-prev content)
|
||||
(mapv (fn [[cmd prev]]
|
||||
(cond-> cmd
|
||||
(and (nil? prev) (some? first))
|
||||
(assoc :prev first)
|
||||
|
||||
(some? prev)
|
||||
(assoc :prev (helpers/segment->point prev))))))))
|
||||
|
||||
(defn close-paths
|
||||
"Removes the :close-path commands and replace them for line-to so we can calculate
|
||||
the intersections"
|
||||
[content]
|
||||
|
||||
(loop [segments (seq content)
|
||||
result []
|
||||
last-move nil
|
||||
last-point nil]
|
||||
(if-let [segment (first segments)]
|
||||
(let [point
|
||||
(helpers/segment->point segment)
|
||||
|
||||
segment
|
||||
(cond
|
||||
(and (= :close-path (:command segment))
|
||||
(or (nil? last-point) ;; Ignore consecutive close-paths
|
||||
(< (gpt/distance last-point last-move) 0.01)))
|
||||
nil
|
||||
|
||||
(= :close-path (:command segment))
|
||||
(helpers/make-line-to last-move)
|
||||
|
||||
:else
|
||||
segment)]
|
||||
|
||||
(recur (rest segments)
|
||||
(cond-> result (some? segment) (conj segment))
|
||||
(if (= :move-to (:command segment))
|
||||
point
|
||||
last-move)
|
||||
point))
|
||||
result)))
|
||||
|
||||
(defn- split-command
|
||||
[cmd values]
|
||||
(case (:command cmd)
|
||||
:line-to (helpers/split-line-to-ranges (:prev cmd) cmd values)
|
||||
:curve-to (helpers/split-curve-to-ranges (:prev cmd) cmd values)
|
||||
[cmd]))
|
||||
|
||||
(defn- split-ts
|
||||
[seg-1 seg-2]
|
||||
(let [cmd-1 (get seg-1 :command)
|
||||
cmd-2 (get seg-2 :command)]
|
||||
(cond
|
||||
(and (= :line-to cmd-1)
|
||||
(= :line-to cmd-2))
|
||||
(helpers/line-line-intersect (helpers/command->line seg-1)
|
||||
(helpers/command->line seg-2))
|
||||
|
||||
(and (= :line-to cmd-1)
|
||||
(= :curve-to cmd-2))
|
||||
(helpers/line-curve-intersect (helpers/command->line seg-1)
|
||||
(helpers/command->bezier seg-2))
|
||||
|
||||
(and (= :curve-to cmd-1)
|
||||
(= :line-to cmd-2))
|
||||
(let [[seg-2' seg-1']
|
||||
(helpers/line-curve-intersect (helpers/command->line seg-2)
|
||||
(helpers/command->bezier seg-1))]
|
||||
;; Need to reverse because we send the arguments reversed
|
||||
[seg-1' seg-2'])
|
||||
|
||||
(and (= :curve-to cmd-1)
|
||||
(= :curve-to cmd-2))
|
||||
(helpers/curve-curve-intersect (helpers/command->bezier seg-1)
|
||||
(helpers/command->bezier seg-2))
|
||||
|
||||
:else
|
||||
[[] []])))
|
||||
|
||||
(defn content-intersect-split
|
||||
[content-a content-b sr-a sr-b]
|
||||
|
||||
(let [command->selrect (memoize helpers/command->selrect)]
|
||||
|
||||
(letfn [(overlap-segment-selrect? [segment selrect]
|
||||
(if (= :move-to (:command segment))
|
||||
false
|
||||
(let [r1 (command->selrect segment)]
|
||||
(grc/overlaps-rects? r1 selrect))))
|
||||
|
||||
(overlap-segments? [seg-1 seg-2]
|
||||
(if (or (= :move-to (:command seg-1))
|
||||
(= :move-to (:command seg-2)))
|
||||
false
|
||||
(let [r1 (command->selrect seg-1)
|
||||
r2 (command->selrect seg-2)]
|
||||
(grc/overlaps-rects? r1 r2))))
|
||||
|
||||
(split [seg-1 seg-2]
|
||||
(if (not (overlap-segments? seg-1 seg-2))
|
||||
[seg-1]
|
||||
(let [[ts-seg-1 _] (split-ts seg-1 seg-2)]
|
||||
(-> (split-command seg-1 ts-seg-1)
|
||||
(add-previous (:prev seg-1))))))
|
||||
|
||||
(split-segment-on-content [segment content content-sr]
|
||||
(if (overlap-segment-selrect? segment content-sr)
|
||||
(->> content
|
||||
(filter #(overlap-segments? segment %))
|
||||
(reduce
|
||||
(fn [result current]
|
||||
(into [] (mapcat #(split % current)) result))
|
||||
[segment]))
|
||||
[segment]))
|
||||
|
||||
(split-content [content-a content-b sr-b]
|
||||
(into []
|
||||
(mapcat #(split-segment-on-content % content-b sr-b))
|
||||
content-a))]
|
||||
|
||||
[(split-content content-a content-b sr-b)
|
||||
(split-content content-b content-a sr-a)])))
|
||||
|
||||
(defn is-segment?
|
||||
[cmd]
|
||||
(and (contains? cmd :prev)
|
||||
(contains? #{:line-to :curve-to} (:command cmd))))
|
||||
|
||||
(defn contains-segment?
|
||||
[segment content content-sr content-geom]
|
||||
|
||||
(let [point (case (:command segment)
|
||||
:line-to (-> (helpers/command->line segment)
|
||||
(helpers/line-values 0.5))
|
||||
|
||||
:curve-to (-> (helpers/command->bezier segment)
|
||||
(helpers/curve-values 0.5)))]
|
||||
|
||||
(and (grc/contains-point? content-sr point)
|
||||
(or
|
||||
(helpers/is-point-in-geom-data? point content-geom)
|
||||
(helpers/is-point-in-border? point content)))))
|
||||
|
||||
(defn inside-segment?
|
||||
[segment content-sr content-geom]
|
||||
(let [point (case (:command segment)
|
||||
:line-to (-> (helpers/command->line segment)
|
||||
(helpers/line-values 0.5))
|
||||
|
||||
:curve-to (-> (helpers/command->bezier segment)
|
||||
(helpers/curve-values 0.5)))]
|
||||
|
||||
(and (grc/contains-point? content-sr point)
|
||||
(helpers/is-point-in-geom-data? point content-geom))))
|
||||
|
||||
(defn overlap-segment?
|
||||
"Finds if the current segment is overlapping against other
|
||||
segment meaning they have the same coordinates"
|
||||
[segment content]
|
||||
|
||||
(let [overlap-single?
|
||||
(fn [other]
|
||||
(when (and (= (:command segment) (:command other))
|
||||
(contains? #{:line-to :curve-to} (:command segment)))
|
||||
|
||||
(case (:command segment)
|
||||
:line-to (let [[p1 q1] (helpers/command->line segment)
|
||||
[p2 q2] (helpers/command->line other)]
|
||||
|
||||
(when (or (and (< (gpt/distance p1 p2) 0.1)
|
||||
(< (gpt/distance q1 q2) 0.1))
|
||||
(and (< (gpt/distance p1 q2) 0.1)
|
||||
(< (gpt/distance q1 p2) 0.1)))
|
||||
[segment other]))
|
||||
|
||||
:curve-to (let [[p1 q1 h11 h21] (helpers/command->bezier segment)
|
||||
[p2 q2 h12 h22] (helpers/command->bezier other)]
|
||||
|
||||
(when (or (and (< (gpt/distance p1 p2) 0.1)
|
||||
(< (gpt/distance q1 q2) 0.1)
|
||||
(< (gpt/distance h11 h12) 0.1)
|
||||
(< (gpt/distance h21 h22) 0.1))
|
||||
|
||||
(and (< (gpt/distance p1 q2) 0.1)
|
||||
(< (gpt/distance q1 p2) 0.1)
|
||||
(< (gpt/distance h11 h22) 0.1)
|
||||
(< (gpt/distance h21 h12) 0.1)))
|
||||
|
||||
[segment other])))))]
|
||||
|
||||
(->> content
|
||||
(d/seek overlap-single?)
|
||||
(some?))))
|
||||
|
||||
(defn fix-move-to
|
||||
[content]
|
||||
;; Remove the field `:prev` and makes the necessaries `move-to`
|
||||
;; then clean the subpaths
|
||||
|
||||
(loop [current (first content)
|
||||
content (rest content)
|
||||
prev nil
|
||||
result []]
|
||||
|
||||
(if (nil? current)
|
||||
result
|
||||
|
||||
(let [result (if (not= (:prev current) prev)
|
||||
(conj result (helpers/make-move-to (:prev current)))
|
||||
result)]
|
||||
(recur (first content)
|
||||
(rest content)
|
||||
(helpers/segment->point current)
|
||||
(conj result (dissoc current :prev)))))))
|
||||
|
||||
(defn remove-duplicated-segments
|
||||
"Remove from the content segments"
|
||||
[content]
|
||||
(letfn [;; This is a comparator for float points with a precission
|
||||
;; used to remove already existing segments
|
||||
(comparator [[fx1 fy1 tx1 ty1 :as v1] [fx2 fy2 tx2 ty2 :as v2]]
|
||||
(if (and (mth/close? tx1 tx2)
|
||||
(mth/close? ty1 ty2)
|
||||
(mth/close? fx1 fx2)
|
||||
(mth/close? fy1 fy2))
|
||||
0 ;; equal
|
||||
(compare v1 v2)))]
|
||||
|
||||
(loop [current (first content)
|
||||
content (rest content)
|
||||
segments (sorted-set-by comparator)
|
||||
result []]
|
||||
|
||||
(if (nil? current)
|
||||
result
|
||||
|
||||
(let [fx (-> current :prev :x)
|
||||
fy (-> current :prev :y)
|
||||
tx (-> current :params :x)
|
||||
ty (-> current :params :y)
|
||||
|
||||
result
|
||||
(cond-> result
|
||||
(and (not (contains? segments [fx fy tx ty]))
|
||||
(not (contains? segments [tx ty fx fy])))
|
||||
(conj current))
|
||||
|
||||
segments (conj segments [fx fy tx ty])]
|
||||
|
||||
(recur (first content)
|
||||
(rest content)
|
||||
segments
|
||||
result))))))
|
||||
|
||||
(defn close-content
|
||||
[content]
|
||||
(into []
|
||||
(mapcat :data)
|
||||
(->> content
|
||||
(subpath/close-subpaths)
|
||||
(subpath/get-subpaths))))
|
||||
|
||||
(defn- content->geom-data
|
||||
[content]
|
||||
|
||||
(->> content
|
||||
(close-content)
|
||||
(filter #(not= (= :line-to (:command %))
|
||||
(= :curve-to (:command %))))
|
||||
(mapv (fn [segment]
|
||||
{:command (:command segment)
|
||||
:segment segment
|
||||
:geom (if (= :line-to (:command segment))
|
||||
(helpers/command->line segment)
|
||||
(helpers/command->bezier segment))
|
||||
:selrect (helpers/command->selrect segment)}))))
|
||||
|
||||
(defn create-union [content-a content-a-split content-b content-b-split sr-a sr-b]
|
||||
;; Pick all segments in content-a that are not inside content-b
|
||||
;; Pick all segments in content-b that are not inside content-a
|
||||
(let [content-a-geom (content->geom-data content-a)
|
||||
content-b-geom (content->geom-data content-b)
|
||||
|
||||
content
|
||||
(concat
|
||||
(->> content-a-split (filter #(not (contains-segment? % content-b sr-b content-b-geom))))
|
||||
(->> content-b-split (filter #(not (contains-segment? % content-a sr-a content-a-geom)))))
|
||||
|
||||
content-geom (content->geom-data content)
|
||||
|
||||
content-sr (segment/content->selrect (fix-move-to content))
|
||||
|
||||
;; Overlapping segments should be added when they are part of the border
|
||||
border-content
|
||||
(->> content-b-split
|
||||
(filter #(and (contains-segment? % content-a sr-a content-a-geom)
|
||||
(overlap-segment? % content-a-split)
|
||||
(not (inside-segment? % content-sr content-geom)))))]
|
||||
|
||||
;; Ensure that the output is always a vector
|
||||
(d/concat-vec content border-content)))
|
||||
|
||||
(defn create-difference [content-a content-a-split content-b content-b-split sr-a sr-b]
|
||||
;; Pick all segments in content-a that are not inside content-b
|
||||
;; Pick all segments in content b that are inside content-a
|
||||
;; removing overlapping
|
||||
(let [content-a-geom (content->geom-data content-a)
|
||||
content-b-geom (content->geom-data content-b)]
|
||||
(d/concat-vec
|
||||
(->> content-a-split (filter #(not (contains-segment? % content-b sr-b content-b-geom))))
|
||||
|
||||
;; Reverse second content so we can have holes inside other shapes
|
||||
(->> content-b-split
|
||||
(filter #(and (contains-segment? % content-a sr-a content-a-geom)
|
||||
(not (overlap-segment? % content-a-split))))))))
|
||||
|
||||
(defn create-intersection [content-a content-a-split content-b content-b-split sr-a sr-b]
|
||||
;; Pick all segments in content-a that are inside content-b
|
||||
;; Pick all segments in content-b that are inside content-a
|
||||
(let [content-a-geom (content->geom-data content-a)
|
||||
content-b-geom (content->geom-data content-b)]
|
||||
(d/concat-vec
|
||||
(->> content-a-split (filter #(contains-segment? % content-b sr-b content-b-geom)))
|
||||
(->> content-b-split (filter #(contains-segment? % content-a sr-a content-a-geom))))))
|
||||
|
||||
(defn create-exclusion [content-a content-b]
|
||||
;; Pick all segments
|
||||
(d/concat-vec content-a content-b))
|
||||
|
||||
(defn content-bool-pair
|
||||
[bool-type content-a content-b]
|
||||
|
||||
(let [;; We need to reverse the second path when making a difference/intersection/exclude
|
||||
;; and both shapes are in the same direction
|
||||
should-reverse?
|
||||
(and (not= :union bool-type)
|
||||
(= (subpath/clockwise? content-b)
|
||||
(subpath/clockwise? content-a)))
|
||||
|
||||
content-a
|
||||
(-> content-a
|
||||
(close-paths)
|
||||
(add-previous))
|
||||
|
||||
content-b
|
||||
(-> content-b
|
||||
(close-paths)
|
||||
(cond-> should-reverse? (subpath/reverse-content))
|
||||
(add-previous))
|
||||
|
||||
sr-a
|
||||
(segment/content->selrect content-a)
|
||||
|
||||
sr-b
|
||||
(segment/content->selrect content-b)
|
||||
|
||||
;; Split content in new segments in the intersection with the other path
|
||||
[content-a-split content-b-split]
|
||||
(content-intersect-split content-a content-b sr-a sr-b)
|
||||
|
||||
content-a-split
|
||||
(->> content-a-split add-previous (filter is-segment?))
|
||||
|
||||
content-b-split
|
||||
(->> content-b-split add-previous (filter is-segment?))
|
||||
|
||||
content
|
||||
(case bool-type
|
||||
:union (create-union content-a content-a-split content-b content-b-split sr-a sr-b)
|
||||
:difference (create-difference content-a content-a-split content-b content-b-split sr-a sr-b)
|
||||
:intersection (create-intersection content-a content-a-split content-b content-b-split sr-a sr-b)
|
||||
:exclude (create-exclusion content-a-split content-b-split))]
|
||||
|
||||
(-> content
|
||||
remove-duplicated-segments
|
||||
fix-move-to
|
||||
subpath/close-subpaths)))
|
||||
|
||||
(defn calculate-content
|
||||
"Create a bool content from a collection of contents and specified
|
||||
type."
|
||||
[bool-type contents]
|
||||
;; We apply the boolean operation in to each pair and the result to the next
|
||||
;; element
|
||||
(if (seq contents)
|
||||
(->> contents
|
||||
(reduce (partial content-bool-pair bool-type))
|
||||
(vec))
|
||||
[]))
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user