Compare commits
684 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0b7db0a70 | ||
|
|
6438b46ea0 | ||
|
|
2bf73db9d3 | ||
|
|
03c4974c9f | ||
|
|
760fa3a451 | ||
|
|
018d8f5330 | ||
|
|
c9730e1a11 | ||
|
|
1f6e19e544 | ||
|
|
a1a0777791 | ||
|
|
887e3f76d2 | ||
|
|
8b622050fa | ||
|
|
20369aaf43 | ||
|
|
07c0e84b7d | ||
|
|
6770acaffd | ||
|
|
6201ccc7d1 | ||
|
|
965d1c469e | ||
|
|
b448ca8cb0 | ||
|
|
2204eb6c75 | ||
|
|
0abe063899 | ||
|
|
6f9b357473 | ||
|
|
152d842043 | ||
|
|
d16f97d574 | ||
|
|
d28eabaca5 | ||
|
|
b730f677be | ||
|
|
8f8806417a | ||
|
|
06694c9e89 | ||
|
|
e9b0ce3db2 | ||
|
|
4c72608398 | ||
|
|
7e99882fbf | ||
|
|
0d72d8749a | ||
|
|
d5b7a74520 | ||
|
|
0202027edb | ||
|
|
d1d32b6292 | ||
|
|
b32485e122 | ||
|
|
36d578f4d4 | ||
|
|
42bd5c2830 | ||
|
|
44548618e8 | ||
|
|
681b1be360 | ||
|
|
30dee9978c | ||
|
|
47aafc7948 | ||
|
|
1d8e2efdff | ||
|
|
dd504d32ca | ||
|
|
267c7b81c3 | ||
|
|
c4a6fd9d86 | ||
|
|
6e46152e4c | ||
|
|
f2f296e494 | ||
|
|
34f9eeff2c | ||
|
|
1278fefea2 | ||
|
|
48d46afcff | ||
|
|
6742dacaf2 | ||
|
|
3337ecc03a | ||
|
|
b9eaacd1f7 | ||
|
|
a0ada51325 | ||
|
|
8e03911957 | ||
|
|
0b3db0fd0d | ||
|
|
bb2862ed4f | ||
|
|
8c3be98b54 | ||
|
|
b76224c010 | ||
|
|
bd9f8d714a | ||
|
|
6900968555 | ||
|
|
741d334a92 | ||
|
|
b583108afa | ||
|
|
f7ace4dfe9 | ||
|
|
7ce2bd5629 | ||
|
|
41716df7b2 | ||
|
|
fe9b9829e2 | ||
|
|
4365c2a008 | ||
|
|
7896e072e7 | ||
|
|
940397c673 | ||
|
|
d57ea781f0 | ||
|
|
b74f2811a7 | ||
|
|
66223f952b | ||
|
|
0443d5e3d5 | ||
|
|
0a0b6a08e9 | ||
|
|
63af4a19e6 | ||
|
|
51f0446c18 | ||
|
|
e965223284 | ||
|
|
dbc15952cc | ||
|
|
93b70509ec | ||
|
|
798883d4e0 | ||
|
|
3e88e65bbd | ||
|
|
0906817cd3 | ||
|
|
37235fa7d0 | ||
|
|
72935d8f2b | ||
|
|
8a4095dae1 | ||
|
|
b59f228f2e | ||
|
|
a28cb3f424 | ||
|
|
05bb797a91 | ||
|
|
eb0029a877 | ||
|
|
ef0bfa85aa | ||
|
|
b5721e30a0 | ||
|
|
2c3b076b58 | ||
|
|
01e12f4524 | ||
|
|
a814d3d99a | ||
|
|
6a4508dceb | ||
|
|
b7d7274a5a | ||
|
|
71359a4328 | ||
|
|
3a3691bcca | ||
|
|
3d649e36cc | ||
|
|
a24db3c447 | ||
|
|
699a055707 | ||
|
|
1f8ed1c31c | ||
|
|
e2fc27663c | ||
|
|
b3cd0b9349 | ||
|
|
1c545b8c37 | ||
|
|
d660dd1c4a | ||
|
|
9c0acf7302 | ||
|
|
415df555c7 | ||
|
|
5ef13239da | ||
|
|
13ef873650 | ||
|
|
78b9834f6c | ||
|
|
672a695b20 | ||
|
|
e9951ed79a | ||
|
|
0c16b2f104 | ||
|
|
7628075420 | ||
|
|
6f896bdaad | ||
|
|
98930db4b6 | ||
|
|
c7493372e9 | ||
|
|
707aa7f570 | ||
|
|
99ff6f9f0a | ||
|
|
be8193e235 | ||
|
|
21b6dbee4c | ||
|
|
f60418fecb | ||
|
|
1180c735c8 | ||
|
|
3d8f6547e0 | ||
|
|
99ec2704fe | ||
|
|
dbd2cef766 | ||
|
|
212ecc2ff3 | ||
|
|
7806f9298d | ||
|
|
3bc57a8990 | ||
|
|
67c9ea9291 | ||
|
|
f404acc834 | ||
|
|
8e1f586d4f | ||
|
|
5dd1b84943 | ||
|
|
ad190d8952 | ||
|
|
ddb11808a7 | ||
|
|
9b1e26b2ac | ||
|
|
c8e35a9aed | ||
|
|
4786b0434c | ||
|
|
40bde84648 | ||
|
|
ebb2593526 | ||
|
|
6a74204ba1 | ||
|
|
78c6c47300 | ||
|
|
746a3decc8 | ||
|
|
6dfd25b760 | ||
|
|
a697ccc923 | ||
|
|
5bfa625868 | ||
|
|
0762a90fa9 | ||
|
|
a4926d905b | ||
|
|
b08b6752cd | ||
|
|
50b1c66e27 | ||
|
|
65508782c6 | ||
|
|
d02ab536b6 | ||
|
|
db3700bda3 | ||
|
|
6dea656e5e | ||
|
|
7bc82ab318 | ||
|
|
3eb9b1d008 | ||
|
|
eca59dc67b | ||
|
|
93b806dccd | ||
|
|
89c31d7c77 | ||
|
|
0a5432bb30 | ||
|
|
2eded25c03 | ||
|
|
fa00f0064e | ||
|
|
ddd3e94698 | ||
|
|
d7ee88f7f6 | ||
|
|
1d7a402749 | ||
|
|
86bcb079b0 | ||
|
|
390416df06 | ||
|
|
1344b34d08 | ||
|
|
497103bdb6 | ||
|
|
04953d13b6 | ||
|
|
b272d9802e | ||
|
|
bd5a537058 | ||
|
|
64a3caf938 | ||
|
|
ca34494483 | ||
|
|
e717eab6df | ||
|
|
068734549c | ||
|
|
3d1fa38eb3 | ||
|
|
bff3b39371 | ||
|
|
a44161053b | ||
|
|
4b64ec74c2 | ||
|
|
1f56e2f03c | ||
|
|
311b09cc63 | ||
|
|
cd7f841e38 | ||
|
|
df798b481d | ||
|
|
578a9ad3f3 | ||
|
|
b765b5f7ea | ||
|
|
f1c013bd82 | ||
|
|
65c1012205 | ||
|
|
f1cd9e25e5 | ||
|
|
b2861ffb93 | ||
|
|
b01d112d63 | ||
|
|
58b13d00c1 | ||
|
|
0f5539ad41 | ||
|
|
6d18ce882c | ||
|
|
e1ea95c195 | ||
|
|
48ee61d1c6 | ||
|
|
3ca22c3f00 | ||
|
|
593d9b771c | ||
|
|
35f9357355 | ||
|
|
427d18df4c | ||
|
|
7d1a1f1aeb | ||
|
|
d7017f8984 | ||
|
|
7b58eeafe3 | ||
|
|
0e2eb3fbb1 | ||
|
|
1f65dfb854 | ||
|
|
522bd26581 | ||
|
|
8849ddec54 | ||
|
|
5039bc666f | ||
|
|
7bd3e3669b | ||
|
|
0dcf8c5577 | ||
|
|
9a62d7eb53 | ||
|
|
5f189a066d | ||
|
|
2287542522 | ||
|
|
333c5e9981 | ||
|
|
0e7e7dd5d9 | ||
|
|
6fe639d6dd | ||
|
|
6114d4c954 | ||
|
|
7ec938b415 | ||
|
|
b3010360b4 | ||
|
|
125ce6fa86 | ||
|
|
7f61ebda78 | ||
|
|
1092f37a02 | ||
|
|
9be77ba8bd | ||
|
|
28fd11cf3e | ||
|
|
16d4388f3e | ||
|
|
42cb782a96 | ||
|
|
fffe69c49f | ||
|
|
707bdf7d33 | ||
|
|
54f9a054cf | ||
|
|
550dafb2c2 | ||
|
|
5aaca437af | ||
|
|
668b264836 | ||
|
|
6fbf902756 | ||
|
|
f7940d1223 | ||
|
|
10008166ac | ||
|
|
44b5bf8613 | ||
|
|
39dae5cdb6 | ||
|
|
d7f7c7116c | ||
|
|
17257a0ffe | ||
|
|
d0f629d9ba | ||
|
|
cd652a72a1 | ||
|
|
828ff30dbf | ||
|
|
f452fe8a71 | ||
|
|
15e29a3b8a | ||
|
|
5c6e98f5e1 | ||
|
|
ef06073119 | ||
|
|
bca8b0ad85 | ||
|
|
4983b1fa88 | ||
|
|
f6b6d9f0e7 | ||
|
|
12a168df92 | ||
|
|
e06d2012d6 | ||
|
|
faed2448eb | ||
|
|
a80aed54a0 | ||
|
|
3f9c4dfe99 | ||
|
|
3c91f335fc | ||
|
|
1ea4586360 | ||
|
|
45e08b5b60 | ||
|
|
43ce5a5288 | ||
|
|
887315ceda | ||
|
|
30b5c8dd74 | ||
|
|
ef7ef76a59 | ||
|
|
a9834bcc13 | ||
|
|
ec2752464d | ||
|
|
7b6a9b2a0f | ||
|
|
8eda4f0849 | ||
|
|
cde25214ae | ||
|
|
9f1f66776c | ||
|
|
c93b78528d | ||
|
|
e567e61383 | ||
|
|
7c3184d88a | ||
|
|
e6f38657a3 | ||
|
|
ebcd61a9a2 | ||
|
|
179fbfe84f | ||
|
|
79441abff3 | ||
|
|
c3db8cd4ea | ||
|
|
27c58ffd87 | ||
|
|
244249db2b | ||
|
|
33b937da87 | ||
|
|
058834e8a6 | ||
|
|
b134af3727 | ||
|
|
6d2eafb4f1 | ||
|
|
9f618804df | ||
|
|
61a3a62b65 | ||
|
|
ced25dec87 | ||
|
|
60d5f3361a | ||
|
|
c8ee4cf0c9 | ||
|
|
177434e503 | ||
|
|
58b45cde31 | ||
|
|
ff87030894 | ||
|
|
7635adf637 | ||
|
|
ca7a40044c | ||
|
|
ce5be30f1d | ||
|
|
1cb8f077f5 | ||
|
|
61794cf784 | ||
|
|
045b191e4d | ||
|
|
6116f6a4be | ||
|
|
ca7e94a306 | ||
|
|
165be38cfe | ||
|
|
f1fb5fe5e1 | ||
|
|
17554d3a8e | ||
|
|
054ac3939b | ||
|
|
9566c0a6f5 | ||
|
|
b8509ccb69 | ||
|
|
1e043e4faa | ||
|
|
5b1c177f58 | ||
|
|
794de783db | ||
|
|
7f6aa26986 | ||
|
|
fcd809fda5 | ||
|
|
71ff8007fb | ||
|
|
a8261f4f58 | ||
|
|
bb6a6f3664 | ||
|
|
9c6af07d32 | ||
|
|
afc011adbb | ||
|
|
da42c14d35 | ||
|
|
a7438e2026 | ||
|
|
47e5314bb3 | ||
|
|
1978c1da11 | ||
|
|
914c9d0ea3 | ||
|
|
7134aebb7f | ||
|
|
abb32e9ed2 | ||
|
|
2c77e385c5 | ||
|
|
ba0f5ac124 | ||
|
|
c4f17e3f06 | ||
|
|
b802ec252a | ||
|
|
68779caab0 | ||
|
|
5d819114d0 | ||
|
|
46b3937236 | ||
|
|
6803b8cf4a | ||
|
|
bd9c5a176b | ||
|
|
76c1d96c87 | ||
|
|
304af805cb | ||
|
|
027d3af619 | ||
|
|
c612c01ac7 | ||
|
|
cd7855a877 | ||
|
|
cdd81e0bfb | ||
|
|
750a8b8aff | ||
|
|
cd86155878 | ||
|
|
16739c39d6 | ||
|
|
c7971a9829 | ||
|
|
44c4fc4b21 | ||
|
|
6f1c8e3320 | ||
|
|
8f818416ba | ||
|
|
de6858ca48 | ||
|
|
3fd3d2a378 | ||
|
|
706c9125e6 | ||
|
|
5f026b000c | ||
|
|
0b0d036f08 | ||
|
|
6b9017d535 | ||
|
|
5eb47e33ac | ||
|
|
4d31335da0 | ||
|
|
0b5e76a448 | ||
|
|
775268c01a | ||
|
|
b0b1c85047 | ||
|
|
5f08fc6695 | ||
|
|
2de3bdf12b | ||
|
|
3a424c7dc1 | ||
|
|
c3e2b37072 | ||
|
|
65bd11a346 | ||
|
|
e6e7fc539f | ||
|
|
6522d415b7 | ||
|
|
378c66a333 | ||
|
|
07204417a5 | ||
|
|
c9c909cdf9 | ||
|
|
a9f59a4d2f | ||
|
|
1d2513cef5 | ||
|
|
febb797ae2 | ||
|
|
68d279a7c3 | ||
|
|
d2d8b7955c | ||
|
|
2a55fd9c36 | ||
|
|
695d3c0735 | ||
|
|
ce95db469b | ||
|
|
5d187cf80f | ||
|
|
e704ebc224 | ||
|
|
ee36f8981c | ||
|
|
09dd220abf | ||
|
|
15bc2dc3b8 | ||
|
|
1deb74eca9 | ||
|
|
de76ce64ab | ||
|
|
94b4ba33e6 | ||
|
|
7ce8a115f4 | ||
|
|
c036a4bb45 | ||
|
|
aa62f30b05 | ||
|
|
3d967c9812 | ||
|
|
e87e9efb99 | ||
|
|
8c1f6fb4a6 | ||
|
|
df82ed8a00 | ||
|
|
d01e3920ba | ||
|
|
777cd5ea3f | ||
|
|
3b73aea5c0 | ||
|
|
168f7a8173 | ||
|
|
04437e2de2 | ||
|
|
2ec83b2e87 | ||
|
|
90bfaa7b56 | ||
|
|
2ae175abd0 | ||
|
|
e2a84d8f88 | ||
|
|
b6f9cd0c7c | ||
|
|
61b7b90722 | ||
|
|
093c159753 | ||
|
|
32d36c0757 | ||
|
|
94dabebf2b | ||
|
|
8e875d4f7e | ||
|
|
86c383f2cf | ||
|
|
b404d8e79a | ||
|
|
d32b480ef2 | ||
|
|
3654825f27 | ||
|
|
d7231e8a09 | ||
|
|
98161aaf2e | ||
|
|
7a942b16bc | ||
|
|
067719c69b | ||
|
|
f3fce53b91 | ||
|
|
e35903f436 | ||
|
|
64cfe5fdd7 | ||
|
|
dbe586cff8 | ||
|
|
3104733db0 | ||
|
|
9d4bdd1028 | ||
|
|
f8030b1645 | ||
|
|
0a999ceb41 | ||
|
|
64bd9907cb | ||
|
|
580eaf990d | ||
|
|
5ab232a961 | ||
|
|
e596cd7ea4 | ||
|
|
05c47a4daa | ||
|
|
5a8783b5f4 | ||
|
|
08bc171a72 | ||
|
|
7372f5fe08 | ||
|
|
6f032bdd05 | ||
|
|
153d3603d2 | ||
|
|
95261e6907 | ||
|
|
17b344376d | ||
|
|
0ed4e44878 | ||
|
|
b42c1832f0 | ||
|
|
058534ba67 | ||
|
|
204dcd6498 | ||
|
|
2c846c0db9 | ||
|
|
2faeb044e0 | ||
|
|
09c8e4f779 | ||
|
|
6994fa6be8 | ||
|
|
cc7beb7670 | ||
|
|
510653732d | ||
|
|
93e8178d67 | ||
|
|
cebc4864cc | ||
|
|
6ad0449376 | ||
|
|
c33c977326 | ||
|
|
f0299d365a | ||
|
|
6ecdca73f5 | ||
|
|
af6a709b2c | ||
|
|
d5c27450ef | ||
|
|
d10269fb07 | ||
|
|
53d987476e | ||
|
|
8b7866d37f | ||
|
|
bb529729b6 | ||
|
|
b7735d9ba8 | ||
|
|
ce744e2b84 | ||
|
|
631c92da3f | ||
|
|
b7063804e9 | ||
|
|
75d47c8419 | ||
|
|
114fb05e80 | ||
|
|
1fec65b37d | ||
|
|
9498e8f334 | ||
|
|
83b8411929 | ||
|
|
e9accd13b3 | ||
|
|
9e27a19258 | ||
|
|
252f3a5bea | ||
|
|
e0626bb126 | ||
|
|
7ff62c7f40 | ||
|
|
4b07e02acb | ||
|
|
4654d1d9c2 | ||
|
|
ce1f75e8a5 | ||
|
|
4d9aebc758 | ||
|
|
e814368ef3 | ||
|
|
bbbb02500f | ||
|
|
404f255f14 | ||
|
|
7a15e0d38a | ||
|
|
bfe669bdd9 | ||
|
|
c0553042fd | ||
|
|
af8b02654b | ||
|
|
4779ec91d0 | ||
|
|
14d7f4af0e | ||
|
|
f9888b23dd | ||
|
|
411cdbb00f | ||
|
|
d859f07469 | ||
|
|
c111819093 | ||
|
|
aa8321d13c | ||
|
|
5e3bd972e5 | ||
|
|
ad39a04fff | ||
|
|
9a97e84296 | ||
|
|
1b7b0dcb13 | ||
|
|
d365a5060b | ||
|
|
b69589394a | ||
|
|
00f5791766 | ||
|
|
38cab2eda7 | ||
|
|
0db4d6e4e0 | ||
|
|
549712962f | ||
|
|
34b7002faf | ||
|
|
0e6f47b23c | ||
|
|
a372a89b5e | ||
|
|
239847aee7 | ||
|
|
813249c6a7 | ||
|
|
293ab28bce | ||
|
|
98e73cdec5 | ||
|
|
6d34bb9d25 | ||
|
|
479a5e4da9 | ||
|
|
4829454877 | ||
|
|
28664f6387 | ||
|
|
1a08a23a9c | ||
|
|
16f344ff1b | ||
|
|
693ce0e2e8 | ||
|
|
e6f533ea65 | ||
|
|
fcc21d63b0 | ||
|
|
afc0ffcb67 | ||
|
|
9ffb9840e1 | ||
|
|
4766ccf1b6 | ||
|
|
16b75c80a3 | ||
|
|
880f063046 | ||
|
|
723c11b886 | ||
|
|
5e074b1cf7 | ||
|
|
71d2fee36e | ||
|
|
7dc701464f | ||
|
|
fd876ef90f | ||
|
|
0597852178 | ||
|
|
81b1333091 | ||
|
|
7baebd79a6 | ||
|
|
951d0e30ae | ||
|
|
711e172769 | ||
|
|
faa60f0ea1 | ||
|
|
99d81c4329 | ||
|
|
17904326f3 | ||
|
|
5ee74f39d8 | ||
|
|
72f898ed60 | ||
|
|
157eab5bac | ||
|
|
e1b0d0a2ad | ||
|
|
2c050ba031 | ||
|
|
41518b16b4 | ||
|
|
72a16dc95f | ||
|
|
3404c64f55 | ||
|
|
b9015422f8 | ||
|
|
a7441b968d | ||
|
|
2d44be31f7 | ||
|
|
c2cf25bb2b | ||
|
|
7c4c53dcb0 | ||
|
|
aceb3b17c8 | ||
|
|
adfcfdb1de | ||
|
|
da329723bc | ||
|
|
63eb53fa06 | ||
|
|
d32c824515 | ||
|
|
e9ee8c481c | ||
|
|
6d78e52605 | ||
|
|
90136a5562 | ||
|
|
1592767c8c | ||
|
|
afa6ce2113 | ||
|
|
50e6e414ee | ||
|
|
ba9b8edcdc | ||
|
|
d7a927475c | ||
|
|
afe210343f | ||
|
|
4e293daf62 | ||
|
|
f9d7fdc516 | ||
|
|
6a04f3955c | ||
|
|
dce3b1780c | ||
|
|
f47f3e9db6 | ||
|
|
4ac74acaf7 | ||
|
|
cf7588c288 | ||
|
|
ec7bdf4000 | ||
|
|
51cd97f782 | ||
|
|
a16f2f096d | ||
|
|
4e92f14551 | ||
|
|
8f6cc9ff44 | ||
|
|
f885df5c67 | ||
|
|
0ccb110e36 | ||
|
|
2c238dca9b | ||
|
|
3e0aa53fca | ||
|
|
12b4e44296 | ||
|
|
9a2cf4aefe | ||
|
|
0e2a116e0a | ||
|
|
7bf30eb54a | ||
|
|
8dda8c8ff3 | ||
|
|
988fdb22be | ||
|
|
1dd2c8fb4d | ||
|
|
2b39384b28 | ||
|
|
28d1275023 | ||
|
|
979181fc3b | ||
|
|
b374b88ad5 | ||
|
|
6643c19a20 | ||
|
|
7460874c81 | ||
|
|
13dd7511f6 | ||
|
|
f153d251c8 | ||
|
|
3442ace981 | ||
|
|
547cebf5a9 | ||
|
|
7a24d22bc6 | ||
|
|
8f5728afe4 | ||
|
|
41b5ac2c61 | ||
|
|
694ca3bf25 | ||
|
|
674d314b55 | ||
|
|
08a35cc5d1 | ||
|
|
176e5db4d9 | ||
|
|
2535a9ebf9 | ||
|
|
8ff99ee925 | ||
|
|
abc8218487 | ||
|
|
e4765750c3 | ||
|
|
02cddf556b | ||
|
|
e1e6d3c72d | ||
|
|
b401a16002 | ||
|
|
523b0ce295 | ||
|
|
abb06726f0 | ||
|
|
d9190e9418 | ||
|
|
9fe3761eca | ||
|
|
51824afba6 | ||
|
|
160a49ad5f | ||
|
|
9d4907d77e | ||
|
|
53d30e0503 | ||
|
|
9609a2fd5d | ||
|
|
66cf0efcb9 | ||
|
|
0129b9df31 | ||
|
|
a7a5521be1 | ||
|
|
f7f7211ca2 | ||
|
|
8c921a6ed5 | ||
|
|
f14ab870f7 | ||
|
|
26e81ffdb6 | ||
|
|
598a88203e | ||
|
|
914d02e754 | ||
|
|
5cf30f391b | ||
|
|
8de00cad76 | ||
|
|
e4c58f2f4e | ||
|
|
063dba30b6 | ||
|
|
01fa9287a1 | ||
|
|
008067fccb | ||
|
|
091301e945 | ||
|
|
145f4f3265 | ||
|
|
36d4e8c948 | ||
|
|
63de847a57 | ||
|
|
b3e1b7c902 | ||
|
|
2dbc235631 | ||
|
|
4d68b48367 | ||
|
|
65e63e2b2d | ||
|
|
58071e1de1 | ||
|
|
5009ef479f | ||
|
|
e5e9357649 | ||
|
|
a577474772 | ||
|
|
e960a09153 | ||
|
|
13d50f59c3 | ||
|
|
ed4c8475e9 | ||
|
|
2338bf4e15 | ||
|
|
267aa7ee63 | ||
|
|
3df7dbc769 | ||
|
|
5f12ab7e85 | ||
|
|
ac0adc5f95 | ||
|
|
208b702065 | ||
|
|
7e61fbfbac | ||
|
|
0591549727 | ||
|
|
a0833580f8 | ||
|
|
c622b60215 | ||
|
|
c228cf9072 | ||
|
|
4c68dd4904 | ||
|
|
32fde582fc | ||
|
|
2bf2509df7 | ||
|
|
07042189c8 | ||
|
|
d251c94638 | ||
|
|
b4119087fb | ||
|
|
e3959e80fb | ||
|
|
0495a2344c | ||
|
|
c781ec5a4f | ||
|
|
a58566735f | ||
|
|
b37e5c25c3 | ||
|
|
63a937a45b | ||
|
|
c920471c0c | ||
|
|
c4863ee09b | ||
|
|
c0b85c454e | ||
|
|
763a49a4d3 | ||
|
|
0e1540873c | ||
|
|
39dcc636ca | ||
|
|
342778c56e | ||
|
|
fd524891aa | ||
|
|
48cb60020c | ||
|
|
d502c592f7 | ||
|
|
e4377436a7 | ||
|
|
eb44c1271e | ||
|
|
20953c2dfc | ||
|
|
4c96a71124 | ||
|
|
295afdc874 | ||
|
|
a66280ce7b | ||
|
|
ce1ea1c5c5 | ||
|
|
16b58866fc | ||
|
|
b4821b9169 | ||
|
|
3686df0f40 |
18
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
18
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
name: 问题报告/Bug report
|
||||||
|
about: 报告可能的程序错误/Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
---
|
||||||
|
|
||||||
|
## 问题描述/Describe the bug
|
||||||
|
清晰完整的描述问题是什么以及如何发生的。/A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
## 复现方式(可选)/To Reproduce (Optional)
|
||||||
|
|
||||||
|
## 截图(可选)/Screenshots (Optional)**
|
||||||
|
如果有必要,提供报错时的有关截图。/If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
## 附件(可选)/Attachments (Optional)
|
||||||
|
请将会**出现问题的文件**以及**日志文件**打包成一个 ZIP 后作为附件贴在 issue 内,日志文件位于程序目录下的 `logs` 文件夹内。/Please compress the problematic files and the log files into a single ZIP archive and attach it to this issue. The log files are located in the `logs` folder under the program directory.
|
||||||
80
.github/workflows/dotnet-desktop.yml
vendored
80
.github/workflows/dotnet-desktop.yml
vendored
@@ -1,58 +1,94 @@
|
|||||||
name: Build and Release WinForms
|
name: Build & Release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
pull_request:
|
||||||
tags:
|
branches:
|
||||||
- 'v*.*.*'
|
- main
|
||||||
|
types:
|
||||||
|
- closed
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-release:
|
build-release:
|
||||||
|
if: ${{ github.event.pull_request.merged == true }}
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
env:
|
env:
|
||||||
PROJECT_NAME: SpineViewer
|
PROJECT_NAME: SpineViewer
|
||||||
VERSION: ${{ github.ref_name }}
|
PROJ_CLI_NAME: SpineViewerCLI
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-tags: true
|
||||||
|
|
||||||
- name: Setup .NET SDK
|
- name: Setup .NET SDK
|
||||||
uses: actions/setup-dotnet@v3
|
uses: actions/setup-dotnet@v3
|
||||||
with:
|
with:
|
||||||
dotnet-version: '8.0.x'
|
dotnet-version: "8.0.x"
|
||||||
|
|
||||||
|
- name: Extract version from csproj
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
[xml]$proj = Get-Content "$env:PROJECT_NAME\$env:PROJECT_NAME.csproj"
|
||||||
|
$VERSION_NUM = $proj.Project.PropertyGroup.Version
|
||||||
|
$VERSION_TAG = "v$VERSION_NUM".Trim()
|
||||||
|
"VERSION=$VERSION_TAG" >> $env:GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Check Version Tag
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
if (-not $env:VERSION) {
|
||||||
|
Write-Error "Version tag not found in csproj file."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
Write-Host "Version tag found: $env:VERSION"
|
||||||
|
|
||||||
|
- name: Tag merge commit
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
git tag $env:VERSION
|
||||||
|
git push --tags
|
||||||
|
|
||||||
- name: Publish FrameworkDependent version
|
- name: Publish FrameworkDependent version
|
||||||
|
shell: pwsh
|
||||||
run: |
|
run: |
|
||||||
dotnet publish ${{ env.PROJECT_NAME }}/${{ env.PROJECT_NAME }}.csproj -c Release -r win-x64 --sc false -p:PublishSingleFile=true -o publish/${{ env.PROJECT_NAME }}-${{ env.VERSION }}
|
dotnet publish "$env:PROJECT_NAME\$env:PROJECT_NAME.csproj" -c Release -r win-x64 --sc false -o "publish\$env:PROJECT_NAME-$env:VERSION"
|
||||||
|
dotnet publish "$env:PROJ_CLI_NAME\$env:PROJ_CLI_NAME.csproj" -c Release -r win-x64 --sc false -o "publish\$env:PROJECT_NAME-$env:VERSION"
|
||||||
|
|
||||||
- name: Publish SelfContained version
|
- name: Publish SelfContained version
|
||||||
|
shell: pwsh
|
||||||
run: |
|
run: |
|
||||||
dotnet publish ${{ env.PROJECT_NAME }}/${{ env.PROJECT_NAME }}.csproj -c Release -r win-x64 --sc true -p:PublishSingleFile=true -o publish/${{ env.PROJECT_NAME }}-${{ env.VERSION }}-SelfContained
|
dotnet publish "$env:PROJECT_NAME\$env:PROJECT_NAME.csproj" -c Release -r win-x64 --sc true -o "publish\$env:PROJECT_NAME-$env:VERSION-SelfContained"
|
||||||
|
dotnet publish "$env:PROJ_CLI_NAME\$env:PROJ_CLI_NAME.csproj" -c Release -r win-x64 --sc true -o "publish\$env:PROJECT_NAME-$env:VERSION-SelfContained"
|
||||||
|
|
||||||
- name: Create release directory
|
- name: Create release directory
|
||||||
run: mkdir release
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
New-Item -ItemType Directory -Path release -Force | Out-Null
|
||||||
|
|
||||||
- name: Compress FrameworkDependent version
|
- name: Compress FrameworkDependent version
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: |
|
run: |
|
||||||
Compress-Archive -Path "publish/${env:PROJECT_NAME}-${env:VERSION}" -DestinationPath "release/${env:PROJECT_NAME}-${env:VERSION}.zip" -Force
|
Compress-Archive -Path "publish\$env:PROJECT_NAME-$env:VERSION\*" -DestinationPath "release\$env:PROJECT_NAME-$env:VERSION.zip" -Force
|
||||||
|
|
||||||
- name: Compress SelfContained version
|
- name: Compress SelfContained version
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: |
|
run: |
|
||||||
Compress-Archive -Path "publish/${env:PROJECT_NAME}-${env:VERSION}-SelfContained" -DestinationPath "release/${env:PROJECT_NAME}-${env:VERSION}-SelfContained.zip" -Force
|
Compress-Archive -Path "publish\$env:PROJECT_NAME-$env:VERSION-SelfContained\*" -DestinationPath "release\$env:PROJECT_NAME-$env:VERSION-SelfContained.zip" -Force
|
||||||
|
|
||||||
- name: Create GitHub Release
|
- name: Create GitHub Release
|
||||||
id: create_release
|
id: create_release
|
||||||
uses: actions/create-release@v1
|
uses: actions/create-release@v1
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ github.ref_name }}
|
tag_name: ${{ env.VERSION }}
|
||||||
release_name: Release ${{ github.ref_name }}
|
release_name: Release ${{ env.VERSION }}
|
||||||
body: 'Automated release build ${{ github.ref_name }}'
|
|
||||||
draft: false
|
draft: false
|
||||||
prerelease: false
|
prerelease: false
|
||||||
|
|
||||||
- name: Upload FrameworkDependent zip
|
- name: Upload FrameworkDependent zip
|
||||||
uses: actions/upload-release-asset@v1
|
uses: actions/upload-release-asset@v1
|
||||||
env:
|
env:
|
||||||
@@ -62,7 +98,7 @@ jobs:
|
|||||||
asset_path: release/${{ env.PROJECT_NAME }}-${{ env.VERSION }}.zip
|
asset_path: release/${{ env.PROJECT_NAME }}-${{ env.VERSION }}.zip
|
||||||
asset_name: ${{ env.PROJECT_NAME }}-${{ env.VERSION }}.zip
|
asset_name: ${{ env.PROJECT_NAME }}-${{ env.VERSION }}.zip
|
||||||
asset_content_type: application/zip
|
asset_content_type: application/zip
|
||||||
|
|
||||||
- name: Upload SelfContained zip
|
- name: Upload SelfContained zip
|
||||||
uses: actions/upload-release-asset@v1
|
uses: actions/upload-release-asset@v1
|
||||||
env:
|
env:
|
||||||
|
|||||||
350
CHANGELOG.md
Normal file
350
CHANGELOG.md
Normal file
@@ -0,0 +1,350 @@
|
|||||||
|
# CHANGELOG
|
||||||
|
|
||||||
|
## v0.16.4
|
||||||
|
|
||||||
|
- 增加 apng 导出格式
|
||||||
|
- 增加颜色拾取器面板
|
||||||
|
- 增加程序皮肤(主题颜色)首选项
|
||||||
|
- 优化部分使用体验
|
||||||
|
|
||||||
|
## v0.16.3
|
||||||
|
|
||||||
|
- 修复加载工作区时的顺序错误
|
||||||
|
- 调整部分调试渲染的逻辑
|
||||||
|
- 完善命中检测逻辑
|
||||||
|
|
||||||
|
## v0.16.2
|
||||||
|
|
||||||
|
- 修复批量添加时的添加顺序错误
|
||||||
|
- 增加精确命中检测和插槽输出功能
|
||||||
|
- 部分代码重构
|
||||||
|
|
||||||
|
## v0.16.1
|
||||||
|
|
||||||
|
- 修复 3.4 版本存在的附件残留问题
|
||||||
|
|
||||||
|
## v0.16.0
|
||||||
|
|
||||||
|
- 增加最小化至托盘图标功能
|
||||||
|
- 调整部分参数项的顺序
|
||||||
|
- 增加开机自启和自启文件设置
|
||||||
|
- 切换桌面投影时自动设置预览分辨率为主屏幕分辨率
|
||||||
|
- 修复 3.4 版本下可能存在的附件残留问题
|
||||||
|
|
||||||
|
## v0.15.19
|
||||||
|
|
||||||
|
- 模型重载后选中最后一个重载模型
|
||||||
|
- 修复 3.4 版本可能的奇数顶点数组导致的越界崩溃问题
|
||||||
|
- 移除参数自动记录中的背景图片路径
|
||||||
|
- 增加测试性桌面投影功能
|
||||||
|
|
||||||
|
## v0.15.18
|
||||||
|
|
||||||
|
- 完善窗口日志颜色标记
|
||||||
|
- 修复预览图背景颜色为透明
|
||||||
|
- 修复面板高度首次还原错误
|
||||||
|
- 增加托盘图标
|
||||||
|
- 增加可选预览背景画面和填充模式
|
||||||
|
- 增强支持的纹理格式(例如 webp)
|
||||||
|
|
||||||
|
## v0.15.17
|
||||||
|
|
||||||
|
- 修改图标配色
|
||||||
|
|
||||||
|
## v0.15.16
|
||||||
|
|
||||||
|
- 修改模型添加顺序, 每次向顶层添加
|
||||||
|
- 添加模型后自动选中最近添加的模型S
|
||||||
|
- 点击预览画面或者选中项发生变化时转移焦点至列表
|
||||||
|
- 增加移除全部菜单项
|
||||||
|
- 增加单例模式和命令行文件参数
|
||||||
|
- 增加文件关联设置
|
||||||
|
|
||||||
|
## v0.15.15
|
||||||
|
|
||||||
|
- 增加报错信息
|
||||||
|
- 导入后自动选中最后一项
|
||||||
|
|
||||||
|
## v0.15.14
|
||||||
|
|
||||||
|
- 将预览画面的首选项移动至上一次状态参数中
|
||||||
|
- 增加预览画面像素的自动保存和恢复
|
||||||
|
- 增加日志启动时的版本号输出
|
||||||
|
|
||||||
|
## v0.15.13
|
||||||
|
|
||||||
|
- 增加程序布局自动存储和还原
|
||||||
|
- 增加部分预览画面首选项
|
||||||
|
|
||||||
|
## v0.15.12
|
||||||
|
|
||||||
|
- 增加单个模型和单个轨道的时间因子
|
||||||
|
- 增加单个轨道的 Alpha 混合参数
|
||||||
|
- 调整轨道清除命令至右键菜单
|
||||||
|
- 设置默认标签页为模型
|
||||||
|
- 完善导入时的报错信息
|
||||||
|
|
||||||
|
## v0.15.11
|
||||||
|
|
||||||
|
- 修复自定义导出中参数构造错误
|
||||||
|
- 增加 mov 格式及参数说明
|
||||||
|
|
||||||
|
## v0.15.10
|
||||||
|
|
||||||
|
- 增加插槽可见性参数, 允许任何情况下对插槽启用和禁用对插槽的渲染
|
||||||
|
|
||||||
|
## v0.15.9
|
||||||
|
|
||||||
|
- 添加 V34 和 V35 版本支持
|
||||||
|
|
||||||
|
## v0.15.8
|
||||||
|
|
||||||
|
- 修复渲染纹理过程中可能的 null 错误
|
||||||
|
|
||||||
|
## v0.15.7
|
||||||
|
|
||||||
|
- 合并社区 CLI 功能项目
|
||||||
|
|
||||||
|
## v0.15.6
|
||||||
|
|
||||||
|
- 修复导出单个的时长错误
|
||||||
|
- 修改默认导出背景色为不透明黑色
|
||||||
|
|
||||||
|
## v0.15.5
|
||||||
|
|
||||||
|
- 修复自定义导出时的画面错误
|
||||||
|
- 设置 mp4 像素格式为 yuv420p 避免 windows 默认播放器无法打开
|
||||||
|
- 增加预览画面和导出时的速度参数设置
|
||||||
|
- 修复一些提示文本错误
|
||||||
|
- 导出时自动将分辨率向下调整为 2 的倍数, 避免 yuv420p 格式出错
|
||||||
|
|
||||||
|
## v0.15.4
|
||||||
|
|
||||||
|
- 修复导出时可能的卡死问题
|
||||||
|
- 增加 webp 格式无损压缩参数
|
||||||
|
|
||||||
|
## v0.15.3
|
||||||
|
|
||||||
|
- 增加 skel.bytes 后缀识别
|
||||||
|
|
||||||
|
## v0.15.2
|
||||||
|
|
||||||
|
- 修复首选项文件读取为空时的提示信息
|
||||||
|
- 工作区参数增加浏览路径
|
||||||
|
|
||||||
|
## v0.15.1
|
||||||
|
|
||||||
|
- 新版本正式发布
|
||||||
|
|
||||||
|
## v0.15.0
|
||||||
|
|
||||||
|
### 项目分支变更
|
||||||
|
|
||||||
|
自 v0.15.0 开始, 该项目将全面更换至 WPF 框架, Winforms 版本将不再进行功能更新, 只进行 bug 修复.
|
||||||
|
|
||||||
|
整个项目将具有下列分支:
|
||||||
|
|
||||||
|
- `dev/wf`: Winforms 版本开发分支, 继承 v0.15.0 之前的内容.
|
||||||
|
- `dev/wpf`: WPF 版本开发分支, v0.15.0 之后的内容.
|
||||||
|
- `release/wf`: `dev/wf` 的发布分支, 用于保留旧版发布功能.
|
||||||
|
- `main`: 最新的稳定发布分支, 也就是现在的 WPF 版本发布分支.
|
||||||
|
|
||||||
|
所有的本地开发和 pr 操作均在 `dev` 子分支下进行, 确认无误后再合并到对应的发布分支进行发布.
|
||||||
|
|
||||||
|
### 项目结构变更
|
||||||
|
|
||||||
|
粗略的将一些功能模块划分为独立的库项目:
|
||||||
|
|
||||||
|
- `SpineViewer`: 项目主体, UI 和程序逻辑
|
||||||
|
- `Spine`: 对不同版本 Spine 运行时的封装库, 提供所有必需操作的统一接口
|
||||||
|
- `SFMLRenderer`: 一个 WPF 控件, 支持渲染 SFML 内容
|
||||||
|
- `SpineRuntimes/*`: 官方不同版本的运行时库, 部分版本在官方基础上有修改和扩展
|
||||||
|
- `NLog.Windows.Wpf`: NLog 在 WPF 上的扩展库 (尚未完工)
|
||||||
|
|
||||||
|
每个项目的具体内容见各自的 README 文档.
|
||||||
|
|
||||||
|
### 功能变更
|
||||||
|
|
||||||
|
目前 v0.15.0 仅为 pre-release, 功能尚未完全迁移, 有以下功能变化和预期计划:
|
||||||
|
|
||||||
|
- 完善了全屏查看功能. 快捷键 F11 可快速切换全屏/窗口模式, 并且支持全屏模式下, 鼠标移动至边缘唤出操作面板.
|
||||||
|
- 增加了浏览面板. 支持打开文件夹进行浏览, 可以对指定文件夹下所有模型生成预览图进行查看.
|
||||||
|
- 支持复制指定模型的参数, 并且可以一键应用到多个模型上, 无法应用的项会忽略.
|
||||||
|
- 导出功能进行了精简. 分为 4 种类型的导出, 且减少了参数项, 仅保留常用参数.
|
||||||
|
- 导出方式变化. 导出方式变为直接对选中项然后右键菜单进行导出, 不再受 "显示" 和 "仅渲染选中" 参数影响.
|
||||||
|
- 版本转换功能将暂时不在新版本中提供, 旧版本中已有的功能仍然可用.
|
||||||
|
- 未来将增加动态桌面功能.
|
||||||
|
|
||||||
|
## v0.12.13
|
||||||
|
|
||||||
|
- 导出文件名增加额外的随机字符串
|
||||||
|
|
||||||
|
## v0.12.12
|
||||||
|
|
||||||
|
- 修复 2.1 版本遗漏的 SkinnedMeshAttachment 附件渲染
|
||||||
|
|
||||||
|
## v0.12.11
|
||||||
|
|
||||||
|
- 修复可能的闪退错误
|
||||||
|
|
||||||
|
## v0.12.10
|
||||||
|
|
||||||
|
- 增加纹理全局加载选项
|
||||||
|
|
||||||
|
## v0.12.9
|
||||||
|
|
||||||
|
- 修复由于未调用 UpdateCache 导致的约束 bug
|
||||||
|
|
||||||
|
## v0.12.8
|
||||||
|
|
||||||
|
- 增加英语界面文本
|
||||||
|
- 增加 4.2 版本格式转换
|
||||||
|
- 修改格式转换中一些问题和编码范式
|
||||||
|
|
||||||
|
## v0.12.7
|
||||||
|
|
||||||
|
- 修复一些问题
|
||||||
|
|
||||||
|
## v0.12.6
|
||||||
|
|
||||||
|
- 增加全屏预览
|
||||||
|
- 增加桌面投影 (实验性功能)
|
||||||
|
- 增加预览画面背景色设置
|
||||||
|
- 增加分辨率和颜色预设列表
|
||||||
|
- 皮肤面板显示 default
|
||||||
|
|
||||||
|
## v0.12.5
|
||||||
|
|
||||||
|
- 增加插槽属性面板
|
||||||
|
- 修改皮肤属性面板设置方式为True/False
|
||||||
|
|
||||||
|
## v0.12.4
|
||||||
|
|
||||||
|
- 增加导出自动分辨率参数
|
||||||
|
- 增加导出边缘和填充参数
|
||||||
|
- 增加导出内容溢出参数
|
||||||
|
- 支持3.7及以下版本多皮肤功能
|
||||||
|
- 增加3.8版本的骨骼文件二进制和文本格式互转
|
||||||
|
- 增加格式转换输出文件夹参数
|
||||||
|
- 修改打开对话框的默认文件后缀筛选为所有类型
|
||||||
|
|
||||||
|
## v0.12.3
|
||||||
|
|
||||||
|
- 增加按住 ctrl 缩放选中模型
|
||||||
|
- 增加对骨骼/网格/剪裁的调试渲染
|
||||||
|
- 换回以前的上下参数面板布局
|
||||||
|
- 修改窗口缩放模式为 Font -> Dpi
|
||||||
|
- 修复部分问题
|
||||||
|
|
||||||
|
## v0.12.2
|
||||||
|
|
||||||
|
- 模型参数分标签显示
|
||||||
|
- 皮肤/动画列表使用右键菜单进行增删
|
||||||
|
- 标题栏显示版本号
|
||||||
|
- 增加 webp 和 avif 动图格式
|
||||||
|
- 增加导出参数缓存
|
||||||
|
- 动图默认帧率修改为 24 帧
|
||||||
|
- 增加保留最后一帧参数
|
||||||
|
|
||||||
|
## v0.12.1
|
||||||
|
|
||||||
|
- 优化使用体验, 提供初始皮肤/动画空位
|
||||||
|
- 修复预览画面分辨率调整时父容器尺寸获取错误
|
||||||
|
|
||||||
|
## v0.12.0
|
||||||
|
|
||||||
|
- 支持皮肤列表 (仅 3.8.x 及以上支持)
|
||||||
|
- 支持多轨道动画
|
||||||
|
- 动画和皮肤列表多选时改为取并集
|
||||||
|
- 修复导出时没有正确处理预乘像素的问题
|
||||||
|
|
||||||
|
## v0.11.5
|
||||||
|
|
||||||
|
- 导出格式全面支持
|
||||||
|
- 修复预览图不显示的问题
|
||||||
|
- 优化列表卡顿问题
|
||||||
|
- 模型列表增加数量显示
|
||||||
|
|
||||||
|
## v0.11.4
|
||||||
|
|
||||||
|
- 增加 MP4 导出格式
|
||||||
|
- 增加导出背景颜色参数
|
||||||
|
- 增加日志输出 FFMpeg 参数字符串
|
||||||
|
- 增加导出时任务栏图标执行动效
|
||||||
|
- 修复预览面板移动模型时物理效果不同步的问题
|
||||||
|
- 优化部分使用体验
|
||||||
|
|
||||||
|
## v0.11.3
|
||||||
|
|
||||||
|
- 增加模型隐藏设置属性
|
||||||
|
- 加宽面板分割条 (4 -> 8 像素)
|
||||||
|
- 优化属性面板分组显示
|
||||||
|
- 增加调试纹理
|
||||||
|
|
||||||
|
## v0.11.2
|
||||||
|
|
||||||
|
- 增加皮肤切换
|
||||||
|
- 优化模型缩放实现
|
||||||
|
- 修复部分情况纹理加载异常
|
||||||
|
|
||||||
|
## v0.11.1
|
||||||
|
|
||||||
|
- 增加 GIF 导出格式
|
||||||
|
- 增加逐个导出时可选自动时长
|
||||||
|
- 优化使用体验
|
||||||
|
|
||||||
|
## v0.11.0
|
||||||
|
|
||||||
|
- 完成导出系统, 支持完整的单帧和帧序列导出功能
|
||||||
|
- 预览画面增加快进功能
|
||||||
|
|
||||||
|
## v0.10.9
|
||||||
|
|
||||||
|
- 预览图导出增加名称后缀参数
|
||||||
|
|
||||||
|
## v0.10.8
|
||||||
|
|
||||||
|
- 完善预览图导出
|
||||||
|
- 优化骨骼文件选择
|
||||||
|
|
||||||
|
## v0.10.7
|
||||||
|
|
||||||
|
- 增加仅导出选中
|
||||||
|
- 增加模型调试属性
|
||||||
|
|
||||||
|
## v0.10.6
|
||||||
|
|
||||||
|
- 增加文件夹检测
|
||||||
|
- 增加从剪贴板添加(可复制本地文件/文件夹直接打开)
|
||||||
|
- 修复预览图导致的批量添加可能卡死
|
||||||
|
|
||||||
|
## v0.10.5
|
||||||
|
|
||||||
|
- 修复一些问题
|
||||||
|
|
||||||
|
## v0.10.4
|
||||||
|
|
||||||
|
- 修复一些问题
|
||||||
|
|
||||||
|
## v0.10.3
|
||||||
|
|
||||||
|
- 增加自动版本检测
|
||||||
|
- 增加文件拖放打开
|
||||||
|
|
||||||
|
## v0.10.2
|
||||||
|
|
||||||
|
- 增加列表右键菜单快捷键
|
||||||
|
- 增加预览缩略图复制
|
||||||
|
- 增加列表视图切换
|
||||||
|
|
||||||
|
## v0.10.1
|
||||||
|
|
||||||
|
- 增加列表预览图
|
||||||
|
- 增加列表预览图导出
|
||||||
|
|
||||||
|
## v0.10.0
|
||||||
|
|
||||||
|
- 增加了画面和列表的选择联动,并删除了预览画面显示包围盒选项
|
||||||
|
- 增加了骨骼文件格式转换功能,目前仅支持部分版本的不完整功能
|
||||||
|
- 优化了部分使用体验
|
||||||
|
|
||||||
41
CONTRIBUTING.md
Normal file
41
CONTRIBUTING.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# CONTRIBUTING
|
||||||
|
|
||||||
|
## 仓库分支
|
||||||
|
|
||||||
|
仓库目前包含 4 个分支:
|
||||||
|
|
||||||
|
- `main`: 默认分支, 也是项目最新版的发布用分支
|
||||||
|
- `dev/wpf`: WPF 版本开发分支
|
||||||
|
- `release/wf`: Winforms 旧版本发布分支 (已弃用, 仅进行 bug 修复)
|
||||||
|
- `dev/wf`: Winforms 旧版本开发分支 (已弃用, 仅进行 bug 修复)
|
||||||
|
|
||||||
|
仓库的每个发布分支都有对应的开发分支 `dev/*`, **在进行贡献和推送时请在开发分支上进行**, 待开发分支上审核完毕进行必要的确认 (例如版本号的更新) 后, 再从开发分支向对应的发布分支发起 pr, 合并后将会通过 Actions 进行自动生成和发布.
|
||||||
|
|
||||||
|
## 仓库结构
|
||||||
|
|
||||||
|
仓库目前包含两个可执行文件项目, 分别是:
|
||||||
|
|
||||||
|
- `SpineViewer.csproj`
|
||||||
|
- `SpineViewerCLI.csproj`
|
||||||
|
|
||||||
|
前者为仓库主要项目, 提供一个预览操作 Spine 模型文件的 UI 界面, 后者基于社区贡献进行开发, 提供一些便捷的 CLI 功能, 从而可以对模型文件进行一些批量操作.
|
||||||
|
|
||||||
|
除此之外其余项目均为一些基础功能库, 为以上两个项目提供必要的功能支持. 原则上 UI 项目和 CLI 项目二者独立互不引用, 仅引用相同的基础功能库, 以保证整个仓库的层次结构清晰便于维护.
|
||||||
|
|
||||||
|
## 如何贡献
|
||||||
|
|
||||||
|
对于一些小改动, 例如:
|
||||||
|
|
||||||
|
- 某些文件内的 bug 修复 (例如一些逻辑上的错误)
|
||||||
|
- 已有功能的扩展性增强 (例如在已有代码逻辑结构上扩充某些功能字段)
|
||||||
|
- 其他可能的对**已有功能**的修复改进
|
||||||
|
|
||||||
|
可以直接 fork 修改后向开发分支发起 pr, 经 review 无问题后可直接合并.
|
||||||
|
|
||||||
|
对于较大的改动, 例如:
|
||||||
|
|
||||||
|
- 新增某些代码文件 (例如需要添加一些全新的类)
|
||||||
|
- 添加一些全新的逻辑或者功能代码 (例如在自行车上加装发动机)
|
||||||
|
- 其他可能影响项目代码逻辑结构的改动
|
||||||
|
|
||||||
|
这些改动请先提 Issue, 进行必要性讨论, 以及确认新功能的引入方式, 请不要直接将这些可能的破坏性改动发起 pr.
|
||||||
18
NLog.Windows.Wpf/NLog.Windows.Wpf.csproj
Normal file
18
NLog.Windows.Wpf/NLog.Windows.Wpf.csproj
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<Platforms>x64</Platforms>
|
||||||
|
<TargetFramework>net8.0-windows</TargetFramework>
|
||||||
|
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
|
||||||
|
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||||
|
<Version>0.16.0</Version>
|
||||||
|
<UseWPF>true</UseWPF>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="NLog" Version="5.4.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
53
NLog.Windows.Wpf/RichTextBoxRowColoringRule.cs
Normal file
53
NLog.Windows.Wpf/RichTextBoxRowColoringRule.cs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
using NLog;
|
||||||
|
using NLog.Conditions;
|
||||||
|
using NLog.Config;
|
||||||
|
using NLog.Layouts;
|
||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
namespace NLog.Windows.Wpf
|
||||||
|
{
|
||||||
|
[NLogConfigurationItem]
|
||||||
|
public class RichTextBoxRowColoringRule
|
||||||
|
{
|
||||||
|
public static RichTextBoxRowColoringRule Default { get; private set; }
|
||||||
|
|
||||||
|
[RequiredParameter]
|
||||||
|
public ConditionExpression Condition { get; set; }
|
||||||
|
|
||||||
|
public Layout FontColor { get; set; }
|
||||||
|
public Layout BackgroundColor { get; set; }
|
||||||
|
|
||||||
|
public FontStyle FontStyle { get; set; }
|
||||||
|
public FontWeight FontWeight { get; set; }
|
||||||
|
|
||||||
|
static RichTextBoxRowColoringRule()
|
||||||
|
{
|
||||||
|
RichTextBoxRowColoringRule.Default = new RichTextBoxRowColoringRule();
|
||||||
|
}
|
||||||
|
|
||||||
|
public RichTextBoxRowColoringRule() : this(null, "Empty", "Empty", FontStyles.Normal, FontWeights.Normal) { }
|
||||||
|
|
||||||
|
public RichTextBoxRowColoringRule(string condition, string fontColor, string backColor)
|
||||||
|
{
|
||||||
|
this.Condition = (ConditionExpression)condition;
|
||||||
|
this.FontColor = Layout.FromString(fontColor);
|
||||||
|
this.BackgroundColor = Layout.FromString(backColor);
|
||||||
|
this.FontStyle = FontStyles.Normal;
|
||||||
|
this.FontWeight = FontWeights.Normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RichTextBoxRowColoringRule(string condition, string fontColor, string backColor, FontStyle fontStyle, FontWeight fontWeight)
|
||||||
|
{
|
||||||
|
this.Condition = (ConditionExpression)condition;
|
||||||
|
this.FontColor = Layout.FromString(fontColor);
|
||||||
|
this.BackgroundColor = Layout.FromString(backColor);
|
||||||
|
this.FontStyle = fontStyle;
|
||||||
|
this.FontWeight = fontWeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CheckCondition(LogEventInfo logEvent)
|
||||||
|
{
|
||||||
|
return true.Equals(this.Condition.Evaluate(logEvent));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
281
NLog.Windows.Wpf/RichTextBoxTarget.cs
Normal file
281
NLog.Windows.Wpf/RichTextBoxTarget.cs
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
using NLog;
|
||||||
|
using NLog.Common;
|
||||||
|
using NLog.Config;
|
||||||
|
using NLog.Targets;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Documents;
|
||||||
|
using System.Windows.Media;
|
||||||
|
|
||||||
|
namespace NLog.Windows.Wpf
|
||||||
|
{
|
||||||
|
[Target("RichTextBox")]
|
||||||
|
public sealed class RichTextBoxTarget : TargetWithLayout
|
||||||
|
{
|
||||||
|
public static ReadOnlyCollection<RichTextBoxRowColoringRule> DefaultRowColoringRules { get; } = CreateDefaultColoringRules();
|
||||||
|
|
||||||
|
private static ReadOnlyCollection<RichTextBoxRowColoringRule> CreateDefaultColoringRules()
|
||||||
|
{
|
||||||
|
return new List<RichTextBoxRowColoringRule>()
|
||||||
|
{
|
||||||
|
new RichTextBoxRowColoringRule("level == LogLevel.Fatal", "White", "Red", FontStyles.Normal, FontWeights.Bold),
|
||||||
|
new RichTextBoxRowColoringRule("level == LogLevel.Error", "Red", "Empty", FontStyles.Italic, FontWeights.Bold),
|
||||||
|
new RichTextBoxRowColoringRule("level == LogLevel.Warn", "Orange", "Empty"),
|
||||||
|
new RichTextBoxRowColoringRule("level == LogLevel.Info", "Black", "Empty"),
|
||||||
|
new RichTextBoxRowColoringRule("level == LogLevel.Debug", "Gray", "Empty"),
|
||||||
|
new RichTextBoxRowColoringRule("level == LogLevel.Trace", "DarkGray", "Empty", FontStyles.Italic, FontWeights.Normal),
|
||||||
|
}.AsReadOnly();
|
||||||
|
}
|
||||||
|
|
||||||
|
public RichTextBoxTarget() { }
|
||||||
|
|
||||||
|
public string ControlName { get; set; }
|
||||||
|
|
||||||
|
public string WindowName { get; set; }
|
||||||
|
|
||||||
|
public bool UseDefaultRowColoringRules { get; set; }
|
||||||
|
|
||||||
|
public bool AutoScroll { get; set; }
|
||||||
|
|
||||||
|
public int MaxLines { get; set; }
|
||||||
|
|
||||||
|
[ArrayParameter(typeof(RichTextBoxRowColoringRule), "row-coloring")]
|
||||||
|
public IList<RichTextBoxRowColoringRule> RowColoringRules { get; } = new List<RichTextBoxRowColoringRule>();
|
||||||
|
|
||||||
|
[ArrayParameter(typeof(RichTextBoxWordColoringRule), "word-coloring")]
|
||||||
|
public IList<RichTextBoxWordColoringRule> WordColoringRules { get; } = new List<RichTextBoxWordColoringRule>();
|
||||||
|
|
||||||
|
[NLogConfigurationIgnoreProperty]
|
||||||
|
public Window TargetWindow { get; set; }
|
||||||
|
|
||||||
|
[NLogConfigurationIgnoreProperty]
|
||||||
|
public RichTextBox TargetRichTextBox { get; set; }
|
||||||
|
|
||||||
|
protected override void InitializeTarget()
|
||||||
|
{
|
||||||
|
base.InitializeTarget();
|
||||||
|
if (TargetRichTextBox != null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (WindowName == null)
|
||||||
|
{
|
||||||
|
HandleError("WindowName should be specified for {0}.{1}", GetType().Name, Name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(ControlName))
|
||||||
|
{
|
||||||
|
HandleError("Rich text box control name must be specified for {0}.{1}", GetType().Name, Name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetWindow = Application.Current.Windows.OfType<Window>().FirstOrDefault(w => w.Name == WindowName);
|
||||||
|
if (targetWindow == null)
|
||||||
|
{
|
||||||
|
InternalLogger.Info("{0}: WindowName '{1}' not found", this, WindowName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetControl = targetWindow.FindName(ControlName) as RichTextBox;
|
||||||
|
if (targetControl == null)
|
||||||
|
{
|
||||||
|
InternalLogger.Info("{0}: WIndowName '{1}' does not contain ControlName '{2}'", this, WindowName, ControlName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AttachToControl(targetWindow, targetControl);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void HandleError(string message, params object[] args)
|
||||||
|
{
|
||||||
|
if (LogManager.ThrowExceptions)
|
||||||
|
{
|
||||||
|
throw new NLogConfigurationException(string.Format(message, args));
|
||||||
|
}
|
||||||
|
InternalLogger.Error(message, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AttachToControl(Window window, RichTextBox textboxControl)
|
||||||
|
{
|
||||||
|
InternalLogger.Info("{0}: Attaching target to textbox {1}.{2}", this, window.Name, textboxControl.Name);
|
||||||
|
DetachFromControl();
|
||||||
|
TargetWindow = window;
|
||||||
|
TargetRichTextBox = textboxControl;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DetachFromControl()
|
||||||
|
{
|
||||||
|
TargetWindow = null;
|
||||||
|
TargetRichTextBox = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void CloseTarget()
|
||||||
|
{
|
||||||
|
DetachFromControl();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Write(LogEventInfo logEvent)
|
||||||
|
{
|
||||||
|
RichTextBox textbox = TargetRichTextBox;
|
||||||
|
if (textbox == null || textbox.Dispatcher.HasShutdownStarted || textbox.Dispatcher.HasShutdownFinished)
|
||||||
|
{
|
||||||
|
//no last logged textbox
|
||||||
|
InternalLogger.Trace("{0}: Attached Textbox is {1}, skipping logging", this, textbox == null ? "null" : "disposed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string logMessage = RenderLogEvent(Layout, logEvent);
|
||||||
|
RichTextBoxRowColoringRule matchingRule = FindMatchingRule(logEvent);
|
||||||
|
_ = DoSendMessageToTextbox(logMessage, matchingRule, logEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool DoSendMessageToTextbox(string logMessage, RichTextBoxRowColoringRule rule, LogEventInfo logEvent)
|
||||||
|
{
|
||||||
|
RichTextBox textbox = TargetRichTextBox;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (textbox != null && !textbox.Dispatcher.HasShutdownStarted && !textbox.Dispatcher.HasShutdownFinished)
|
||||||
|
{
|
||||||
|
if (!textbox.Dispatcher.CheckAccess())
|
||||||
|
{
|
||||||
|
textbox.Dispatcher.BeginInvoke(() => SendTheMessageToRichTextBox(textbox, logMessage, rule, logEvent));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SendTheMessageToRichTextBox(textbox, logMessage, rule, logEvent);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
InternalLogger.Warn(ex, "{0}: Failed to append RichTextBox", this);
|
||||||
|
|
||||||
|
if (LogManager.ThrowExceptions)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private RichTextBoxRowColoringRule FindMatchingRule(LogEventInfo logEvent)
|
||||||
|
{
|
||||||
|
//custom rules first
|
||||||
|
if (RowColoringRules.Count > 0)
|
||||||
|
{
|
||||||
|
foreach (RichTextBoxRowColoringRule coloringRule in RowColoringRules)
|
||||||
|
{
|
||||||
|
if (coloringRule.CheckCondition(logEvent))
|
||||||
|
{
|
||||||
|
return coloringRule;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (UseDefaultRowColoringRules && DefaultRowColoringRules != null)
|
||||||
|
{
|
||||||
|
foreach (RichTextBoxRowColoringRule coloringRule in DefaultRowColoringRules)
|
||||||
|
{
|
||||||
|
if (coloringRule.CheckCondition(logEvent))
|
||||||
|
{
|
||||||
|
return coloringRule;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return RichTextBoxRowColoringRule.Default;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SendTheMessageToRichTextBox(RichTextBox textBox, string logMessage, RichTextBoxRowColoringRule rule, LogEventInfo logEvent)
|
||||||
|
{
|
||||||
|
if (textBox == null) return;
|
||||||
|
|
||||||
|
var document = textBox.Document;
|
||||||
|
|
||||||
|
// 插入文本(带换行)
|
||||||
|
var tr = new TextRange(document.ContentEnd, document.ContentEnd)
|
||||||
|
{
|
||||||
|
Text = logMessage + Environment.NewLine
|
||||||
|
};
|
||||||
|
|
||||||
|
// 设置行级样式
|
||||||
|
var fgColor = rule.FontColor?.Render(logEvent);
|
||||||
|
var bgColor = rule.BackgroundColor?.Render(logEvent);
|
||||||
|
|
||||||
|
tr.ApplyPropertyValue(TextElement.ForegroundProperty,
|
||||||
|
string.IsNullOrEmpty(fgColor) || fgColor == "Empty"
|
||||||
|
? textBox.Foreground
|
||||||
|
: new SolidColorBrush((Color)ColorConverter.ConvertFromString(fgColor)));
|
||||||
|
|
||||||
|
tr.ApplyPropertyValue(TextElement.BackgroundProperty,
|
||||||
|
string.IsNullOrEmpty(bgColor) || bgColor == "Empty"
|
||||||
|
? Brushes.Transparent
|
||||||
|
: new SolidColorBrush((Color)ColorConverter.ConvertFromString(bgColor)));
|
||||||
|
|
||||||
|
tr.ApplyPropertyValue(TextElement.FontStyleProperty, rule.FontStyle);
|
||||||
|
tr.ApplyPropertyValue(TextElement.FontWeightProperty, rule.FontWeight);
|
||||||
|
|
||||||
|
// Word coloring(在刚插入的范围内做匹配)
|
||||||
|
if (WordColoringRules.Count > 0)
|
||||||
|
{
|
||||||
|
foreach (var wordRule in WordColoringRules)
|
||||||
|
{
|
||||||
|
var pattern = wordRule.Regex?.Render(logEvent) ?? string.Empty;
|
||||||
|
var text = wordRule.Text?.Render(logEvent) ?? string.Empty;
|
||||||
|
var wholeWords = wordRule.WholeWords.RenderValue(logEvent);
|
||||||
|
var ignoreCase = wordRule.IgnoreCase.RenderValue(logEvent);
|
||||||
|
|
||||||
|
var regex = wordRule.ResolveRegEx(pattern, text, wholeWords, ignoreCase);
|
||||||
|
var matches = regex.Matches(tr.Text);
|
||||||
|
|
||||||
|
foreach (Match match in matches)
|
||||||
|
{
|
||||||
|
// 匹配到的部分范围
|
||||||
|
var start = tr.Start.GetPositionAtOffset(match.Index, LogicalDirection.Forward);
|
||||||
|
var endPos = tr.Start.GetPositionAtOffset(match.Index + match.Length, LogicalDirection.Backward);
|
||||||
|
if (start == null || endPos == null) continue;
|
||||||
|
|
||||||
|
var wordRange = new TextRange(start, endPos);
|
||||||
|
|
||||||
|
var wordFg = wordRule.FontColor?.Render(logEvent);
|
||||||
|
var wordBg = wordRule.BackgroundColor?.Render(logEvent);
|
||||||
|
|
||||||
|
wordRange.ApplyPropertyValue(TextElement.ForegroundProperty,
|
||||||
|
string.IsNullOrEmpty(wordFg) || wordFg == "Empty"
|
||||||
|
? tr.GetPropertyValue(TextElement.ForegroundProperty)
|
||||||
|
: new SolidColorBrush((Color)ColorConverter.ConvertFromString(wordFg)));
|
||||||
|
|
||||||
|
wordRange.ApplyPropertyValue(TextElement.BackgroundProperty,
|
||||||
|
string.IsNullOrEmpty(wordBg) || wordBg == "Empty"
|
||||||
|
? tr.GetPropertyValue(TextElement.BackgroundProperty)
|
||||||
|
: new SolidColorBrush((Color)ColorConverter.ConvertFromString(wordBg)));
|
||||||
|
|
||||||
|
wordRange.ApplyPropertyValue(TextElement.FontStyleProperty, wordRule.FontStyle);
|
||||||
|
wordRange.ApplyPropertyValue(TextElement.FontWeightProperty, wordRule.FontWeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 限制最大行数
|
||||||
|
if (MaxLines > 0)
|
||||||
|
{
|
||||||
|
while (document.Blocks.Count > MaxLines)
|
||||||
|
{
|
||||||
|
document.Blocks.Remove(document.Blocks.FirstBlock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动滚动到最后
|
||||||
|
if (AutoScroll)
|
||||||
|
{
|
||||||
|
textBox.ScrollToEnd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
59
NLog.Windows.Wpf/RichTextBoxWordColoringRule.cs
Normal file
59
NLog.Windows.Wpf/RichTextBoxWordColoringRule.cs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
using NLog.Config;
|
||||||
|
using NLog.Layouts;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
namespace NLog.Windows.Wpf
|
||||||
|
{
|
||||||
|
[NLogConfigurationItem]
|
||||||
|
public class RichTextBoxWordColoringRule
|
||||||
|
{
|
||||||
|
public Layout Regex { get; set; }
|
||||||
|
public Layout Text { get; set; }
|
||||||
|
public Layout<bool> WholeWords { get; set; }
|
||||||
|
public Layout<bool> IgnoreCase { get; set; }
|
||||||
|
|
||||||
|
public Layout FontColor { get; set; }
|
||||||
|
public Layout BackgroundColor { get; set; }
|
||||||
|
|
||||||
|
public FontStyle FontStyle { get; set; }
|
||||||
|
public FontWeight FontWeight { get; set; }
|
||||||
|
|
||||||
|
internal Regex ResolveRegEx(string pattern, string text, bool wholeWords, bool ignoreCase)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(pattern) && text != null)
|
||||||
|
{
|
||||||
|
pattern = System.Text.RegularExpressions.Regex.Escape(text);
|
||||||
|
if (wholeWords)
|
||||||
|
pattern = "\b" + pattern + "\b";
|
||||||
|
}
|
||||||
|
|
||||||
|
RegexOptions options = RegexOptions.None;
|
||||||
|
if (ignoreCase)
|
||||||
|
options |= RegexOptions.IgnoreCase;
|
||||||
|
|
||||||
|
return new Regex(pattern, options); // RegEx-Cache
|
||||||
|
}
|
||||||
|
|
||||||
|
public RichTextBoxWordColoringRule() : this(null, "Empty", "Empty", FontStyles.Normal, FontWeights.Normal) { }
|
||||||
|
|
||||||
|
public RichTextBoxWordColoringRule(string text, string fontColor, string backgroundColor)
|
||||||
|
{
|
||||||
|
this.Text = text;
|
||||||
|
this.FontColor = Layout.FromString(fontColor);
|
||||||
|
this.BackgroundColor = Layout.FromString(backgroundColor);
|
||||||
|
this.FontStyle = FontStyles.Normal;
|
||||||
|
this.FontWeight = FontWeights.Normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RichTextBoxWordColoringRule(string text, string textColor, string backgroundColor, FontStyle fontStyle, FontWeight fontWeight)
|
||||||
|
{
|
||||||
|
this.Text = text;
|
||||||
|
this.FontColor = Layout.FromString(textColor);
|
||||||
|
this.BackgroundColor = Layout.FromString(backgroundColor);
|
||||||
|
this.FontStyle = fontStyle;
|
||||||
|
this.FontWeight = fontWeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
151
README.en.md
Normal file
151
README.en.md
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
# [SpineViewer](https://github.com/ww-rm/SpineViewer)
|
||||||
|
|
||||||
|
[](https://github.com/ww-rm/SpineViewer/actions/workflows/dotnet-desktop.yml)
|
||||||
|
[](https://github.com/ww-rm/SpineViewer/releases)
|
||||||
|
[](https://github.com/ww-rm/SpineViewer/releases)
|
||||||
|
|
||||||
|
[中文](README.md) | [English](README.en.md)
|
||||||
|
|
||||||
|
A simple and user-friendly Spine file viewer and exporter with multi-language support (Chinese/English/Japanese).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
https://github.com/user-attachments/assets/37b6b730-088a-4352-827a-c338127a16f0
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Multiple versions of Spine files
|
||||||
|
- Batch file opening via drag-and-drop or copy-paste
|
||||||
|
- Batch preview
|
||||||
|
- List-based multi-skeleton viewing and render order management
|
||||||
|
- Multi-selection in lists for batch skeleton parameter settings
|
||||||
|
- Multi-track animation settings
|
||||||
|
- Skin and custom slot attachment settings
|
||||||
|
- Custom slot visibility
|
||||||
|
- Debug rendering
|
||||||
|
- Playback speed adjustment for view/model/track timelines
|
||||||
|
- Track alpha blending parameter settings
|
||||||
|
- Fullscreen preview
|
||||||
|
- Export to single frame, image sequence, animated GIF, or video file
|
||||||
|
- Automatic resolution batch export
|
||||||
|
- Custom export with FFmpeg
|
||||||
|
- Program parameter saving
|
||||||
|
- File extension association
|
||||||
|
- Texture images in formats other than PNG
|
||||||
|
- Launch at startup with persistent dynamic wallpaper
|
||||||
|
- ......
|
||||||
|
|
||||||
|
### Supported Spine Versions
|
||||||
|
|
||||||
|
| Version | View & Export |
|
||||||
|
| :-----: | :------------------: |
|
||||||
|
| `2.1.x` | :white\_check\_mark: |
|
||||||
|
| `3.4.x` | :white\_check\_mark: |
|
||||||
|
| `3.5.x` | :white\_check\_mark: |
|
||||||
|
| `3.6.x` | :white\_check\_mark: |
|
||||||
|
| `3.7.x` | :white\_check\_mark: |
|
||||||
|
| `3.8.x` | :white\_check\_mark: |
|
||||||
|
| `4.0.x` | :white\_check\_mark: |
|
||||||
|
| `4.1.x` | :white\_check\_mark: |
|
||||||
|
| `4.2.x` | :white\_check\_mark: |
|
||||||
|
| `4.3.x` | |
|
||||||
|
|
||||||
|
More versions under development \:rocket: \:rocket: \:rocket:
|
||||||
|
|
||||||
|
### Supported Export Formats
|
||||||
|
|
||||||
|
| Format | Use Case |
|
||||||
|
| -------------- | ----------------------------------------------------------------------------- |
|
||||||
|
| Single Frame | Generate high-resolution images of models; manually adjust the desired frame. |
|
||||||
|
| Frame Sequence | Supports PNG format with transparency and lossless compression. |
|
||||||
|
| GIF/Video | Export preview animations or common video formats. |
|
||||||
|
| Custom Export | Supports arbitrary FFmpeg parameters for custom, complex export needs. |
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Download the compressed package from the [Release](https://github.com/ww-rm/SpineViewer/releases) page.
|
||||||
|
|
||||||
|
The software requires the [.NET Desktop Runtime 8.0.x](https://dotnet.microsoft.com/download/dotnet/8.0) to run.
|
||||||
|
|
||||||
|
Alternatively, download the package with the `SelfContained` suffix for standalone execution.
|
||||||
|
|
||||||
|
For exporting GIF/MP4 and other animation/video formats, FFmpeg must be installed and added to the system environment variables. Visit the [FFmpeg Windows download page](https://ffmpeg.org/download.html#build-windows) or download the latest version directly: [ffmpeg-release-full.7z](https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-full.7z).
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### How to Change the Display Language
|
||||||
|
|
||||||
|
In the menu, go to "File" -> "Preferences..." -> "Language," select your desired language, and confirm the change.
|
||||||
|
|
||||||
|
### Basic Overview
|
||||||
|
|
||||||
|
The program is organized into a left-right layout:
|
||||||
|
|
||||||
|
- **Left Panel:** Functionality panel.
|
||||||
|
- **Right Panel:** Preview display.
|
||||||
|
|
||||||
|
The left panel includes three sub-panels:
|
||||||
|
|
||||||
|
- **Browse:** Preview the content of a specified folder without importing files into the program. This panel allows generating `.webp` previews for models or importing selected models.
|
||||||
|
- **Model:** Lists imported models for rendering. Parameters and rendering order can be adjusted here, along with other model-related functionalities.
|
||||||
|
- **Display:** Adjust parameters for the right-side preview display.
|
||||||
|
|
||||||
|
Hover your mouse over buttons, labels, or input fields to see help text for most UI elements.
|
||||||
|
|
||||||
|
### Skeleton Import
|
||||||
|
|
||||||
|
Drag-and-drop or paste skeleton files/directories into the Model panel.
|
||||||
|
|
||||||
|
Alternatively, use the right-click menu in the Browse panel to import selected items.
|
||||||
|
|
||||||
|
### Content Adjustment
|
||||||
|
|
||||||
|
The Model panel supports right-click menus, some shortcuts, and batch adjustments of model parameters through multi-selection.
|
||||||
|
|
||||||
|
For preview display adjustments:
|
||||||
|
|
||||||
|
- **Left-click:** Select and drag models. Hold `Ctrl` for multi-selection, synchronized with the left-side list.
|
||||||
|
- **Right-click:** Drag the entire display.
|
||||||
|
- **Scroll wheel:** Zoom in/out. Hold `Ctrl` to scale selected models.
|
||||||
|
- **Render selected-only mode:** In this mode, the preview only shows selected models, and selection status can only be changed via the left-side list.
|
||||||
|
|
||||||
|
The buttons below the preview display allow time adjustments, serving as a simple playback control.
|
||||||
|
|
||||||
|
### Content Export
|
||||||
|
|
||||||
|
Export follows the **WYSIWYG (What You See Is What You Get)** principle, meaning the preview display reflects the exported output.
|
||||||
|
|
||||||
|
Use the right-click menu in the Model panel to export selected items.
|
||||||
|
|
||||||
|
Key export parameters include:
|
||||||
|
|
||||||
|
- **Output folder:** Optional. When not specified, output is saved to the respective model folder; otherwise, all output is saved to the provided folder.
|
||||||
|
- **Export single:** By default, each model is exported independently. Selecting "Export single" renders all selected models in a single frame, producing a unified output.
|
||||||
|
- **Auto resolution:** Ignores the preview resolution and viewport parameters, exporting output at the actual size of the content. For animations/videos, the output matches the size required for full visibility.
|
||||||
|
|
||||||
|
### Dynamic Wallpaper
|
||||||
|
|
||||||
|
Dynamic wallpaper is implemented through desktop projection, allowing the content of the current preview to be projected onto the desktop in real time.
|
||||||
|
|
||||||
|
You can enable or disable desktop projection from the program preferences or the right-click menu of the tray icon. After adjusting the model and display parameters, you can save the current configuration as a workspace file for convenient restoration later.
|
||||||
|
|
||||||
|
If you want the wallpaper to stay active after startup, you can enable auto-start in the preferences and specify which workspace file should be loaded when the program launches.
|
||||||
|
|
||||||
|
### More Information
|
||||||
|
|
||||||
|
For detailed usage and documentation, see the [Wiki](https://github.com/ww-rm/SpineViewer/wiki). For usage questions or bug reports, submit an [Issue](https://github.com/ww-rm/SpineViewer/issues).
|
||||||
|
|
||||||
|
## Acknowledgements
|
||||||
|
|
||||||
|
- [spine-runtimes](https://github.com/EsotericSoftware/spine-runtimes)
|
||||||
|
- [SFML.Net](https://github.com/SFML/SFML.Net)
|
||||||
|
- [FFMpegCore](https://github.com/rosenbjerg/FFMpegCore)
|
||||||
|
- [HandyControl](https://github.com/HandyOrg/HandyControl)
|
||||||
|
- [NLog](https://github.com/NLog/NLog)
|
||||||
|
- [SkiaSharp](https://github.com/mono/SkiaSharp)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*If you find this project helpful, please give it a \:star: and share it with others! :)*
|
||||||
|
|
||||||
|
[](https://starchart.cc/ww-rm/SpineViewer)
|
||||||
151
README.md
151
README.md
@@ -1,65 +1,150 @@
|
|||||||
# SpineViewer
|
# [SpineViewer](https://github.com/ww-rm/SpineViewer)
|
||||||
|
|
||||||
|
[](https://github.com/ww-rm/SpineViewer/actions/workflows/dotnet-desktop.yml)
|
||||||
|
[](https://github.com/ww-rm/SpineViewer/releases)
|
||||||
|
[](https://github.com/ww-rm/SpineViewer/releases)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
[中文](README.md) | [English](README.en.md)
|
[中文](README.md) | [English](README.en.md)
|
||||||
|
|
||||||
一个简单好用的 Spine 文件查看&导出程序.
|
Spine 文件查看&导出程序, 同时也是支持 Spine 的动态壁纸程序.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
---
|
https://github.com/user-attachments/assets/37b6b730-088a-4352-827a-c338127a16f0
|
||||||
|
|
||||||
|
## 功能
|
||||||
|
|
||||||
|
- 支持多版本 spine 文件
|
||||||
|
- 支持拖拽/复制粘贴批量打开文件
|
||||||
|
- 支持批量预览
|
||||||
|
- 支持列表式多骨骼查看和渲染层级管理
|
||||||
|
- 支持列表多选批量设置骨骼参数
|
||||||
|
- 支持多轨道动画设置
|
||||||
|
- 支持皮肤/自定义插槽附件设置
|
||||||
|
- 支持自定义插槽可见性
|
||||||
|
- 支持调试渲染
|
||||||
|
- 支持画面/模型/轨道时间倍速设置
|
||||||
|
- 支持设置轨道 Alpha 混合参数
|
||||||
|
- 支持全屏预览
|
||||||
|
- 支持单帧/动图/视频文件导出
|
||||||
|
- 支持自动分辨率批量导出
|
||||||
|
- 支持 FFmpeg 自定义导出
|
||||||
|
- 支持程序参数保存
|
||||||
|
- 支持文件后缀关联
|
||||||
|
- 支持非 png 格式的纹理图片格式
|
||||||
|
- 支持开机自启常驻动态壁纸
|
||||||
|
- ......
|
||||||
|
|
||||||
|
### Spine 版本支持
|
||||||
|
|
||||||
|
| 版本 | 查看&导出 |
|
||||||
|
| :---: | :---: |
|
||||||
|
| `2.1.x` | :white_check_mark: |
|
||||||
|
| `3.4.x` | :white_check_mark: |
|
||||||
|
| `3.5.x` | :white_check_mark: |
|
||||||
|
| `3.6.x` | :white_check_mark: |
|
||||||
|
| `3.7.x` | :white_check_mark: |
|
||||||
|
| `3.8.x` | :white_check_mark: |
|
||||||
|
| `4.0.x` | :white_check_mark: |
|
||||||
|
| `4.1.x` | :white_check_mark: |
|
||||||
|
| `4.2.x` | :white_check_mark: |
|
||||||
|
| `4.3.x` | |
|
||||||
|
|
||||||
|
更多版本正在施工 :rocket: :rocket: :rocket:
|
||||||
|
|
||||||
|
### 导出格式支持
|
||||||
|
|
||||||
|
| 导出格式 | 适用场景 |
|
||||||
|
| --- | --- |
|
||||||
|
| 单帧画面 | 支持生成高清模型画面图像, 可手动调节需要的一帧. |
|
||||||
|
| 帧序列 | 支持 PNG 格式帧序列, 可保留透明通道且无损压缩. |
|
||||||
|
| 动图/视频 | 可以生成预览动图或者常见格式视频. |
|
||||||
|
| 自定义导出 | 除上述预设方案, 支持提供任意 FFmpeg 参数进行导出, 满足自定义复杂需求. |
|
||||||
|
|
||||||
## 安装
|
## 安装
|
||||||
|
|
||||||
前往 [Release](https://github.com/ww-rm/SpineViewer/releases) 界面下载压缩包.
|
前往 [Release](https://github.com/ww-rm/SpineViewer/releases) 界面下载压缩包.
|
||||||
|
|
||||||
`SelfContained` 可独立运行, `FrameworkDependent` 需要安装依赖框架 [.NET 桌面运行时 8.0.x](https://dotnet.microsoft.com/zh-cn/download/dotnet/8.0).
|
软件需要安装依赖框架 [.NET 桌面运行时 8.0.x](https://dotnet.microsoft.com/zh-cn/download/dotnet/8.0).
|
||||||
|
|
||||||
## 功能
|
也可以下载带有 `SelfContained` 后缀的压缩包, 可以独立运行.
|
||||||
|
|
||||||
- 支持不同版本 Spine 查看
|
导出 GIF/MP4 等动图/视频格式需要在本地安装 ffmpeg 命令行, 并且添加至环境变量, [点击前往 FFmpeg-Windows 下载页面](https://ffmpeg.org/download.html#build-windows), 也可以点这个下载最新版本 [ffmpeg-release-full.7z](https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-full.7z).
|
||||||
- [x] `v3.6.x`
|
|
||||||
- [x] `v3.7.x`
|
|
||||||
- [x] `v3.8.x`
|
|
||||||
- [x] `v4.0.x`
|
|
||||||
- [x] `v4.1.x`
|
|
||||||
- [x] `v4.2.x`
|
|
||||||
- [ ] `v4.3.x`
|
|
||||||
- 支持多骨骼文件动画预览
|
|
||||||
- 支持每个骨骼独立参数设置
|
|
||||||
- 支持动画PNG帧序列导出
|
|
||||||
- 支持缩放旋转等导出画面设置
|
|
||||||
- Coming soon...
|
|
||||||
|
|
||||||
## 使用方法
|
## 使用方法
|
||||||
|
|
||||||
|
### 如何修改显示语言
|
||||||
|
|
||||||
|
窗口菜单的 "文件" -> "首选项..." -> "语言", 选择你需要的语言并确认修改.
|
||||||
|
|
||||||
|
### 基本介绍
|
||||||
|
|
||||||
|
程序大致是左右布局, 左侧是功能面板, 右侧是画面.
|
||||||
|
|
||||||
|
左侧有三个子面板, 分别是:
|
||||||
|
|
||||||
|
- **浏览**. 该面板用于预览指定文件夹的内容, 并没有真正导入文件到程序. 在该面板可以为模型生成 webp 格式的预览图, 或者导入选中的模型.
|
||||||
|
- **模型**. 该面板记录导入并进行渲染的模型列表, 可以在这个面板设置与模型渲染相关的参数和渲染顺序, 以及一些与模型有关的功能.
|
||||||
|
- **画面**. 该面板用于设置右侧预览画面的参数.
|
||||||
|
|
||||||
|
绝大部分按钮或者标签或者输入框都可以通过鼠标指针悬停来获取帮助文本.
|
||||||
|
|
||||||
### 骨骼导入
|
### 骨骼导入
|
||||||
|
|
||||||
**文件**菜单可以选择**打开**或者**批量打开**进行骨骼文件导入.
|
可以直接拖放/粘贴需要导入的骨骼文件/目录到模型面板.
|
||||||
|
|
||||||
### 骨骼调整
|
或者在浏览面板内右键菜单导入选中项.
|
||||||
|
|
||||||
在**模型列表**中选择一项或多项, 将会在**模型参数**面板显示可供调节的参数.
|
### 内容调整
|
||||||
|
|
||||||
**模型列表**右键菜单可以对列表项进行增删调整, 也可以使用鼠标左键拖动调整顺序.
|
模型面板支持右键菜单以及部分快捷键, 并且可以多选进行模型参数的批量调整.
|
||||||
|
|
||||||
### 画面调整
|
预览画面除了使用面板进行参数设置外, 支持部分鼠标动作:
|
||||||
|
|
||||||
**预览画面**支持的鼠标操作:
|
- 左键可以选择和拖拽模型, 按下 `Ctrl` 键可以实现多选, 与左侧列表选择是联动的.
|
||||||
|
- 右键对整体画面进行拖动.
|
||||||
|
- 滚轮进行画面缩放, 按住 `Ctrl` 可以对选中的模型进行批量缩放.
|
||||||
|
- 仅渲染选中模式, 在该模式下, 预览画面仅包含被选中的模型, 并且只能通过左侧列表改变选中状态.
|
||||||
|
|
||||||
- 左键可以对骨骼进行拖动
|
预览画面下方按钮支持对画面时间进行调整, 可以当作一个简易的播放器.
|
||||||
- 右键对画面进行拖动
|
|
||||||
- 滚轮进行画面缩放
|
|
||||||
|
|
||||||
除此之外, 也可以通过**画面参数**面板调节导出和预览时的画面参数.
|
### 内容导出
|
||||||
|
|
||||||
在**功能**菜单中, 可以重置同步所有骨骼动画时间.
|
导出遵循 "所见即所得" 原则, 即实时预览的画面就是你导出的画面.
|
||||||
|
|
||||||
### 动画导出
|
在模型面板里, 右键菜单可以对选中项进行导出操作.
|
||||||
|
|
||||||
**文件**菜单中选择**导出**可以将目前加载的所有骨骼动画按照预览时的画面进行PNG帧序列导出.
|
导出有以下几个关键参数:
|
||||||
|
|
||||||
可以在每个骨骼的**模型参数**中查看动画完整时长.
|
- 输出文件夹. 这个参数某些时候可选, 当不提供时, 则将输出产物输出到每个模型各自的模型文件夹, 否则输出产物全部输出到提供的输出文件夹.
|
||||||
|
- 导出单个. 默认是每个模型独立导出, 即对模型列表进行批量操作, 如果选择仅导出单个, 那么被导出的所有模型将在同一个画面上被渲染, 输出产物只有一份.
|
||||||
|
- 自动分辨率. 该模式会忽略预览画面的分辨率和视区参数, 导出产物的分辨率与被导出内容的实际大小一致, 如果是动图或者视频则会与完整显示动画的必需大小一致.
|
||||||
|
|
||||||
|
### 动态壁纸
|
||||||
|
|
||||||
|
动态壁纸通过桌面投影实现, 可以将当前预览画面上的内容实时投影至桌面.
|
||||||
|
|
||||||
|
在程序首选项或者托盘图标右键菜单中可以进行桌面投影的启用与否, 模型和画面参数调整完成后, 可以将当前参数保存为工作区文件, 方便之后恢复该配置.
|
||||||
|
|
||||||
|
如果希望开机自启常驻壁纸, 也可以在首选项中启用开机自启, 并且设置启动后需要加载的工作区文件.
|
||||||
|
|
||||||
|
### 更多
|
||||||
|
|
||||||
|
更为详细的使用方法和说明见 [Wiki](https://github.com/ww-rm/SpineViewer/wiki), 有使用上的问题或者 BUG 可以提个 [Issue](https://github.com/ww-rm/SpineViewer/issues).
|
||||||
|
|
||||||
|
## Acknowledgements
|
||||||
|
|
||||||
|
- [spine-runtimes](https://github.com/EsotericSoftware/spine-runtimes)
|
||||||
|
- [SFML.Net](https://github.com/SFML/SFML.Net)
|
||||||
|
- [FFMpegCore](https://github.com/rosenbjerg/FFMpegCore)
|
||||||
|
- [HandyControl](https://github.com/HandyOrg/HandyControl)
|
||||||
|
- [NLog](https://github.com/NLog/NLog)
|
||||||
|
- [SkiaSharp](https://github.com/mono/SkiaSharp)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*如果你觉得这个项目不错请给个 :star:, 并分享给更多人知道! :)*
|
*如果你觉得这个项目不错请给个 :star:, 并分享给更多人知道! :)*
|
||||||
|
|
||||||
|
[](https://starchart.cc/ww-rm/SpineViewer)
|
||||||
|
|||||||
138
SFMLRenderer/ISFMLRenderer.cs
Normal file
138
SFMLRenderer/ISFMLRenderer.cs
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
using SFML.Graphics;
|
||||||
|
using SFML.System;
|
||||||
|
using SFML.Window;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace SFMLRenderer
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 定义了 SFML 渲染器的基本功能和事件, 基本上是对 <see cref="RenderWindow"/> 的抽象
|
||||||
|
/// <para>实现示例可以见 <see cref="SFMLRenderPanel"/></para>
|
||||||
|
/// </summary>
|
||||||
|
public interface ISFMLRenderer
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 发生在资源首次创建完成后, 该事件发生之后渲染器才是可用的, 操作才会生效
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler? RendererCreated;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 发生在资源即将不可用之前, 该事件发生之后对渲染器的操作将被忽略
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler? RendererDisposing;
|
||||||
|
|
||||||
|
public event EventHandler<MouseMoveEventArgs>? CanvasMouseMove;
|
||||||
|
public event EventHandler<MouseButtonEventArgs>? CanvasMouseButtonPressed;
|
||||||
|
public event EventHandler<MouseButtonEventArgs>? CanvasMouseButtonReleased;
|
||||||
|
public event EventHandler<MouseWheelScrollEventArgs>? CanvasMouseWheelScrolled;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 分辨率, 影响画面的相对比例
|
||||||
|
/// </summary>
|
||||||
|
public Vector2u Resolution { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 快捷设置视区中心点
|
||||||
|
/// </summary>
|
||||||
|
public Vector2f Center { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 快捷设置视区缩放
|
||||||
|
/// </summary>
|
||||||
|
public float Zoom { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 快捷设置视区旋转
|
||||||
|
/// </summary>
|
||||||
|
public float Rotation { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 快捷设置视区水平翻转
|
||||||
|
/// </summary>
|
||||||
|
public bool FlipX { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 快捷设置视区垂直翻转
|
||||||
|
/// </summary>
|
||||||
|
public bool FlipY { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 最大帧率, 影响 Draw 的最大调用频率, <see cref="RenderWindow.SetFramerateLimit(uint)"/>
|
||||||
|
/// </summary>
|
||||||
|
public uint MaxFps { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 垂直同步, <see cref="RenderWindow.SetVerticalSyncEnabled(bool)"/>
|
||||||
|
/// </summary>
|
||||||
|
public bool VerticalSync { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <inheritdoc cref="RenderWindow.SetActive(bool)"/>
|
||||||
|
/// </summary>
|
||||||
|
public bool SetActive(bool active);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <inheritdoc cref="RenderWindow.GetView"/>
|
||||||
|
/// </summary>
|
||||||
|
public View GetView();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <inheritdoc cref="RenderWindow.SetView(View)"/>
|
||||||
|
/// </summary>
|
||||||
|
public void SetView(View view);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <inheritdoc cref="RenderWindow.MapPixelToCoords(Vector2i)"/>
|
||||||
|
/// </summary>
|
||||||
|
public Vector2f MapPixelToCoords(Vector2i point);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <inheritdoc cref="RenderWindow.MapCoordsToPixel(Vector2f)"/>
|
||||||
|
/// </summary>
|
||||||
|
public Vector2i MapCoordsToPixel(Vector2f point);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <inheritdoc cref="RenderWindow.Clear()"/>
|
||||||
|
/// </summary>
|
||||||
|
public void Clear();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <inheritdoc cref="RenderWindow.Clear(Color)"/>
|
||||||
|
/// </summary>
|
||||||
|
public void Clear(Color color);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <inheritdoc cref="RenderWindow.Draw(Drawable)"/>
|
||||||
|
/// </summary>
|
||||||
|
public void Draw(Drawable drawable);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <inheritdoc cref="RenderWindow.Draw(Drawable, RenderStates)"/>
|
||||||
|
/// </summary>
|
||||||
|
public void Draw(Drawable drawable, RenderStates states);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <inheritdoc cref="RenderWindow.Draw(Vertex[], PrimitiveType)"/>
|
||||||
|
/// </summary>
|
||||||
|
public void Draw(Vertex[] vertices, PrimitiveType type);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <inheritdoc cref="RenderWindow.Draw(Vertex[], PrimitiveType, RenderStates)"/>
|
||||||
|
/// </summary>
|
||||||
|
public void Draw(Vertex[] vertices, PrimitiveType type, RenderStates states);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <inheritdoc cref="RenderWindow.Draw(Vertex[], uint, uint, PrimitiveType)"/>
|
||||||
|
/// </summary>
|
||||||
|
public void Draw(Vertex[] vertices, uint start, uint count, PrimitiveType type);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <inheritdoc cref="RenderWindow.Display"/>
|
||||||
|
/// </summary>
|
||||||
|
public void Display();
|
||||||
|
}
|
||||||
|
}
|
||||||
21
SFMLRenderer/README.md
Normal file
21
SFMLRenderer/README.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# SFMLRenderer
|
||||||
|
|
||||||
|
这个库封装了一个用于 WPF 的 SFML 渲染控件.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
|
||||||
|
namespace SFMLRenderer {
|
||||||
|
class ISFMLRenderer {
|
||||||
|
<<Interface>>
|
||||||
|
}
|
||||||
|
|
||||||
|
class SFMLHwndHost
|
||||||
|
|
||||||
|
class SFMLRenderPanel
|
||||||
|
}
|
||||||
|
|
||||||
|
ISFMLRenderer <|.. SFMLRenderPanel
|
||||||
|
SFMLHwndHost <.. SFMLRenderPanel
|
||||||
|
|
||||||
|
```
|
||||||
73
SFMLRenderer/SFMLHwndHost.cs
Normal file
73
SFMLRenderer/SFMLHwndHost.cs
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Interop;
|
||||||
|
using System.Windows.Media;
|
||||||
|
|
||||||
|
namespace SFMLRenderer
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 原生窗口控件, 不应直接使用该类, 而是使用 <see cref="SFMLRenderPanel"/> 或者二次封装
|
||||||
|
/// </summary>
|
||||||
|
public class SFMLHwndHost : HwndHost
|
||||||
|
{
|
||||||
|
private HwndSource? _hwndSource;
|
||||||
|
private SFML.Graphics.RenderWindow? _renderWindow;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 内部的 SFML 窗口对象
|
||||||
|
/// </summary>
|
||||||
|
public SFML.Graphics.RenderWindow? RenderWindow => _renderWindow;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 窗口建立事件
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler? RenderWindowBuilded;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 窗口销毁事件
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler? RenderWindowDestroying;
|
||||||
|
|
||||||
|
protected override HandleRef BuildWindowCore(HandleRef hwndParent)
|
||||||
|
{
|
||||||
|
var ps = new HwndSourceParameters(GetType().Name, (int)Width, (int)Height)
|
||||||
|
{
|
||||||
|
ParentWindow = hwndParent.Handle,
|
||||||
|
WindowStyle = 0x40000000 | 0x10000000, // WS_CHILD | WS_VISIBLE
|
||||||
|
HwndSourceHook = HwndMessageHook
|
||||||
|
};
|
||||||
|
_hwndSource = new HwndSource(ps);
|
||||||
|
_renderWindow = new(_hwndSource.Handle);
|
||||||
|
_renderWindow.SetActive(false);
|
||||||
|
|
||||||
|
RenderWindowBuilded?.Invoke(this, EventArgs.Empty);
|
||||||
|
return new HandleRef(this, _hwndSource.Handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void DestroyWindowCore(HandleRef hwnd)
|
||||||
|
{
|
||||||
|
RenderWindowDestroying?.Invoke(this, EventArgs.Empty);
|
||||||
|
|
||||||
|
_renderWindow?.Close();
|
||||||
|
var rw = _renderWindow;
|
||||||
|
_renderWindow = null;
|
||||||
|
rw?.Dispose();
|
||||||
|
var hs = _hwndSource;
|
||||||
|
_hwndSource = null;
|
||||||
|
hs?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private IntPtr HwndMessageHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
|
||||||
|
{
|
||||||
|
_renderWindow?.DispatchEvents();
|
||||||
|
return IntPtr.Zero;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
SFMLRenderer/SFMLRenderPanel.xaml
Normal file
14
SFMLRenderer/SFMLRenderPanel.xaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<UserControl x:Class="SFMLRenderer.SFMLRenderPanel"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:local="clr-namespace:SFMLRenderer"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignHeight="450" d:DesignWidth="800">
|
||||||
|
<local:SFMLHwndHost x:Name="_hwndHost"
|
||||||
|
Width="100"
|
||||||
|
Height="100"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
</UserControl>
|
||||||
253
SFMLRenderer/SFMLRenderPanel.xaml.cs
Normal file
253
SFMLRenderer/SFMLRenderPanel.xaml.cs
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
using SFML.Graphics;
|
||||||
|
using SFML.System;
|
||||||
|
using SFML.Window;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace SFMLRenderer
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// SFMLRenderPanel.xaml 的交互逻辑
|
||||||
|
/// </summary>
|
||||||
|
public partial class SFMLRenderPanel : System.Windows.Controls.UserControl, ISFMLRenderer
|
||||||
|
{
|
||||||
|
private RenderWindow? RenderWindow => _hwndHost.RenderWindow;
|
||||||
|
|
||||||
|
public SFMLRenderPanel()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public event EventHandler? RendererCreated
|
||||||
|
{
|
||||||
|
add => _hwndHost.RenderWindowBuilded += value;
|
||||||
|
remove => _hwndHost.RenderWindowBuilded -= value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public event EventHandler? RendererDisposing
|
||||||
|
{
|
||||||
|
add => _hwndHost.RenderWindowDestroying += value;
|
||||||
|
remove => _hwndHost.RenderWindowDestroying -= value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public event EventHandler<MouseMoveEventArgs>? CanvasMouseMove
|
||||||
|
{
|
||||||
|
add { if (RenderWindow is RenderWindow w) w.MouseMoved += value; }
|
||||||
|
remove { if (RenderWindow is RenderWindow w) w.MouseMoved -= value; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public event EventHandler<MouseButtonEventArgs>? CanvasMouseButtonPressed
|
||||||
|
{
|
||||||
|
add { if (RenderWindow is RenderWindow w) w.MouseButtonPressed += value; }
|
||||||
|
remove { if (RenderWindow is RenderWindow w) w.MouseButtonPressed -= value; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public event EventHandler<MouseButtonEventArgs>? CanvasMouseButtonReleased
|
||||||
|
{
|
||||||
|
add { if (RenderWindow is RenderWindow w) w.MouseButtonReleased += value; }
|
||||||
|
remove { if (RenderWindow is RenderWindow w) w.MouseButtonReleased -= value; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public event EventHandler<MouseWheelScrollEventArgs>? CanvasMouseWheelScrolled
|
||||||
|
{
|
||||||
|
add { if (RenderWindow is RenderWindow w) w.MouseWheelScrolled += value; }
|
||||||
|
remove { if (RenderWindow is RenderWindow w) w.MouseWheelScrolled -= value; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vector2u Resolution
|
||||||
|
{
|
||||||
|
get => _resolution;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (RenderWindow is null) return;
|
||||||
|
if (value == _resolution) return;
|
||||||
|
if (value.X <= 0 || value.Y <= 0) return;
|
||||||
|
|
||||||
|
var zoom = Zoom;
|
||||||
|
|
||||||
|
float parentW = (float)ActualWidth;
|
||||||
|
float parentH = (float)ActualHeight;
|
||||||
|
float renderW = value.X;
|
||||||
|
float renderH = value.Y;
|
||||||
|
float scale = Math.Min(parentW / renderW, parentH / renderH); // 两方向取较小值, 保证 parent 覆盖 render
|
||||||
|
renderW *= scale;
|
||||||
|
renderH *= scale;
|
||||||
|
|
||||||
|
_hwndHost.Width = renderW;
|
||||||
|
_hwndHost.Height = renderH;
|
||||||
|
|
||||||
|
_resolution = value;
|
||||||
|
|
||||||
|
// 设置完 resolution 后还原缩放比例
|
||||||
|
Zoom = zoom;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private Vector2u _resolution = new(100, 100);
|
||||||
|
|
||||||
|
public Vector2f Center
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (RenderWindow is null) return default;
|
||||||
|
using var view = RenderWindow.GetView();
|
||||||
|
return view.Center;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (RenderWindow is null) return;
|
||||||
|
using var view = RenderWindow.GetView();
|
||||||
|
view.Center = value;
|
||||||
|
RenderWindow.SetView(view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public float Zoom
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (RenderWindow is null) return 1;
|
||||||
|
using var view = RenderWindow.GetView();
|
||||||
|
return Math.Abs(_resolution.X / view.Size.X); // XXX: 仅使用宽度进行缩放计算
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
value = Math.Abs(value);
|
||||||
|
if (RenderWindow is null || value <= 0) return;
|
||||||
|
using var view = RenderWindow.GetView();
|
||||||
|
var signX = Math.Sign(view.Size.X);
|
||||||
|
var signY = Math.Sign(view.Size.Y);
|
||||||
|
view.Size = new(_resolution.X / value * signX, _resolution.Y / value * signY);
|
||||||
|
RenderWindow.SetView(view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public float Rotation
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (RenderWindow is null) return default;
|
||||||
|
using var view = RenderWindow.GetView();
|
||||||
|
return view.Rotation;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (RenderWindow is null) return;
|
||||||
|
using var view = RenderWindow.GetView();
|
||||||
|
view.Rotation = value;
|
||||||
|
RenderWindow.SetView(view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool FlipX
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (RenderWindow is null) return false;
|
||||||
|
using var view = RenderWindow.GetView();
|
||||||
|
return view.Size.X < 0;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (RenderWindow is null) return;
|
||||||
|
|
||||||
|
using var view = RenderWindow.GetView();
|
||||||
|
var size = view.Size;
|
||||||
|
if (size.X > 0 && value || size.X < 0 && !value)
|
||||||
|
size.X *= -1;
|
||||||
|
view.Size = size;
|
||||||
|
RenderWindow.SetView(view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool FlipY
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (RenderWindow is null) return false;
|
||||||
|
using var view = RenderWindow.GetView();
|
||||||
|
return view.Size.Y < 0;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (RenderWindow is null) return;
|
||||||
|
|
||||||
|
using var view = RenderWindow.GetView();
|
||||||
|
var size = view.Size;
|
||||||
|
if (size.Y > 0 && value || size.Y < 0 && !value)
|
||||||
|
size.Y *= -1;
|
||||||
|
view.Size = size;
|
||||||
|
RenderWindow.SetView(view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public uint MaxFps
|
||||||
|
{
|
||||||
|
get => _maxFps;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (RenderWindow is null) return;
|
||||||
|
RenderWindow.SetFramerateLimit(value);
|
||||||
|
_maxFps = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private uint _maxFps = 0;
|
||||||
|
|
||||||
|
public bool VerticalSync
|
||||||
|
{
|
||||||
|
get => _verticalSync;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (RenderWindow is null) return;
|
||||||
|
RenderWindow.SetVerticalSyncEnabled(value);
|
||||||
|
_verticalSync = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private bool _verticalSync = false;
|
||||||
|
|
||||||
|
public void Clear() => RenderWindow?.Clear();
|
||||||
|
|
||||||
|
public void Clear(Color color) => RenderWindow?.Clear(color);
|
||||||
|
|
||||||
|
public void Display() => RenderWindow?.Display();
|
||||||
|
|
||||||
|
public void Draw(Drawable drawable) => RenderWindow?.Draw(drawable);
|
||||||
|
|
||||||
|
public void Draw(Drawable drawable, RenderStates states) => RenderWindow?.Draw(drawable, states);
|
||||||
|
|
||||||
|
public void Draw(Vertex[] vertices, PrimitiveType type) => RenderWindow?.Draw(vertices, type);
|
||||||
|
|
||||||
|
public void Draw(Vertex[] vertices, PrimitiveType type, RenderStates states) => RenderWindow?.Draw(vertices, type, states);
|
||||||
|
|
||||||
|
public void Draw(Vertex[] vertices, uint start, uint count, PrimitiveType type) => RenderWindow?.Draw(vertices, start, count, type);
|
||||||
|
|
||||||
|
public View GetView() => RenderWindow?.GetView() ?? new();
|
||||||
|
|
||||||
|
public Vector2i MapCoordsToPixel(Vector2f point) => RenderWindow?.MapCoordsToPixel(point) ?? default;
|
||||||
|
|
||||||
|
public Vector2f MapPixelToCoords(Vector2i point) => RenderWindow?.MapPixelToCoords(point) ?? default;
|
||||||
|
|
||||||
|
public bool SetActive(bool active) => RenderWindow?.SetActive(active) ?? false;
|
||||||
|
|
||||||
|
public void SetView(View view) => RenderWindow?.SetView(view);
|
||||||
|
|
||||||
|
protected override void OnRenderSizeChanged(System.Windows.SizeChangedInfo sizeInfo)
|
||||||
|
{
|
||||||
|
base.OnRenderSizeChanged(sizeInfo);
|
||||||
|
|
||||||
|
if (RenderWindow is null) return;
|
||||||
|
float parentW = (float)sizeInfo.NewSize.Width;
|
||||||
|
float parentH = (float)sizeInfo.NewSize.Height;
|
||||||
|
float renderW = (float)_hwndHost.ActualWidth;
|
||||||
|
float renderH = (float)_hwndHost.ActualHeight;
|
||||||
|
float scale = Math.Min(parentW / renderW, parentH / renderH); // 两方向取较小值, 保证 parent 覆盖 render
|
||||||
|
renderW *= scale;
|
||||||
|
renderH *= scale;
|
||||||
|
|
||||||
|
_hwndHost.Width = renderW;
|
||||||
|
_hwndHost.Height = renderH;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
171
SFMLRenderer/SFMLRenderWindow.cs
Normal file
171
SFMLRenderer/SFMLRenderWindow.cs
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
using SFML.Graphics;
|
||||||
|
using SFML.System;
|
||||||
|
using SFML.Window;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Threading;
|
||||||
|
|
||||||
|
namespace SFMLRenderer
|
||||||
|
{
|
||||||
|
public class SFMLRenderWindow : RenderWindow, ISFMLRenderer
|
||||||
|
{
|
||||||
|
private readonly DispatcherTimer _timer = new() { Interval = TimeSpan.FromMilliseconds(10) };
|
||||||
|
|
||||||
|
public SFMLRenderWindow(VideoMode mode, string title, Styles style) : base(mode, title, style)
|
||||||
|
{
|
||||||
|
SetActive(false);
|
||||||
|
_timer.Tick += (s, e) => DispatchEvents();
|
||||||
|
_timer.Start();
|
||||||
|
RendererCreated?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
public event EventHandler? RendererCreated;
|
||||||
|
|
||||||
|
public event EventHandler? RendererDisposing
|
||||||
|
{
|
||||||
|
add => throw new NotImplementedException();
|
||||||
|
remove => throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public event EventHandler<MouseMoveEventArgs>? CanvasMouseMove
|
||||||
|
{
|
||||||
|
add { MouseMoved += value; }
|
||||||
|
remove { MouseMoved -= value; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public event EventHandler<MouseButtonEventArgs>? CanvasMouseButtonPressed
|
||||||
|
{
|
||||||
|
add { MouseButtonPressed += value; }
|
||||||
|
remove { MouseButtonPressed -= value; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public event EventHandler<MouseButtonEventArgs>? CanvasMouseButtonReleased
|
||||||
|
{
|
||||||
|
add { MouseButtonReleased += value; }
|
||||||
|
remove { MouseButtonReleased -= value; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public event EventHandler<MouseWheelScrollEventArgs>? CanvasMouseWheelScrolled
|
||||||
|
{
|
||||||
|
add { MouseWheelScrolled += value; }
|
||||||
|
remove { MouseWheelScrolled -= value; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vector2u Resolution
|
||||||
|
{
|
||||||
|
get => Size;
|
||||||
|
set => Size = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vector2f Center
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
using var view = GetView();
|
||||||
|
return view.Center;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
using var view = GetView();
|
||||||
|
view.Center = value;
|
||||||
|
SetView(view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public float Zoom
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
using var view = GetView();
|
||||||
|
return Math.Abs(Size.X / view.Size.X); // XXX: 仅使用宽度进行缩放计算
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
value = Math.Abs(value);
|
||||||
|
if (value <= 0) return;
|
||||||
|
using var view = GetView();
|
||||||
|
var signX = Math.Sign(view.Size.X);
|
||||||
|
var signY = Math.Sign(view.Size.Y);
|
||||||
|
var resolution = Size;
|
||||||
|
view.Size = new(resolution.X / value * signX, resolution.Y / value * signY);
|
||||||
|
SetView(view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public float Rotation
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
using var view = GetView();
|
||||||
|
return view.Rotation;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
using var view = GetView();
|
||||||
|
view.Rotation = value;
|
||||||
|
SetView(view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool FlipX
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
using var view = GetView();
|
||||||
|
return view.Size.X < 0;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
using var view = GetView();
|
||||||
|
var size = view.Size;
|
||||||
|
if (size.X > 0 && value || size.X < 0 && !value)
|
||||||
|
size.X *= -1;
|
||||||
|
view.Size = size;
|
||||||
|
SetView(view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool FlipY
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
using var view = GetView();
|
||||||
|
return view.Size.Y < 0;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
using var view = GetView();
|
||||||
|
var size = view.Size;
|
||||||
|
if (size.Y > 0 && value || size.Y < 0 && !value)
|
||||||
|
size.Y *= -1;
|
||||||
|
view.Size = size;
|
||||||
|
SetView(view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public uint MaxFps
|
||||||
|
{
|
||||||
|
get => _maxFps;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
SetFramerateLimit(value);
|
||||||
|
_maxFps = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private uint _maxFps = 0;
|
||||||
|
|
||||||
|
public bool VerticalSync
|
||||||
|
{
|
||||||
|
get => _verticalSync;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
SetVerticalSyncEnabled(value);
|
||||||
|
_verticalSync = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private bool _verticalSync = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
22
SFMLRenderer/SFMLRenderer.csproj
Normal file
22
SFMLRenderer/SFMLRenderer.csproj
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<Platforms>x64</Platforms>
|
||||||
|
<TargetFramework>net8.0-windows</TargetFramework>
|
||||||
|
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
|
||||||
|
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||||
|
<Version>0.16.0</Version>
|
||||||
|
<UseWPF>true</UseWPF>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<NoWarn>$(NoWarn);NETSDK1206</NoWarn>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="SFML.Net" Version="2.6.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
207
Spine/Exporters/BaseExporter.cs
Normal file
207
Spine/Exporters/BaseExporter.cs
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
using NLog;
|
||||||
|
using SFML.Graphics;
|
||||||
|
using SFML.System;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Spine.Exporters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 导出类基类, 提供基本的帧渲染功能
|
||||||
|
/// </summary>
|
||||||
|
public abstract class BaseExporter : IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 日志器
|
||||||
|
/// </summary>
|
||||||
|
protected static readonly Logger _logger = LogManager.GetCurrentClassLogger();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用于渲染的画布
|
||||||
|
/// </summary>
|
||||||
|
protected RenderTexture _renderTexture;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化导出器
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="width">画布宽像素值</param>
|
||||||
|
/// <param name="height">画布高像素值</param>
|
||||||
|
public BaseExporter(uint width , uint height)
|
||||||
|
{
|
||||||
|
// XXX: 强制变成 2 的倍数, 防止像是 yuv420p 这种像素格式报错
|
||||||
|
width = width >> 1 << 1;
|
||||||
|
height = height >> 1 << 1;
|
||||||
|
if (width <= 0 || height <= 0)
|
||||||
|
throw new ArgumentException($"Invalid resolution: {width}, {height}");
|
||||||
|
_renderTexture = new(width, height);
|
||||||
|
_renderTexture.SetActive(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化导出器
|
||||||
|
/// </summary>
|
||||||
|
public BaseExporter(Vector2u resolution)
|
||||||
|
{
|
||||||
|
// XXX: 强制变成 2 的倍数, 防止像是 yuv420p 这种像素格式报错
|
||||||
|
resolution.X = resolution.X >> 1 << 1;
|
||||||
|
resolution.Y = resolution.Y >> 1 << 1;
|
||||||
|
if (resolution.X <= 0 || resolution.Y <= 0)
|
||||||
|
throw new ArgumentException($"Invalid resolution: {resolution}");
|
||||||
|
_renderTexture = new(resolution.X, resolution.Y);
|
||||||
|
_renderTexture.SetActive(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 可选的进度回调函数
|
||||||
|
/// <list type="number">
|
||||||
|
/// <item><c>total</c>: 任务总量</item>
|
||||||
|
/// <item><c>done</c>: 已完成量</item>
|
||||||
|
/// <item><c>progressText</c>: 需要设置的进度提示文本</item>
|
||||||
|
/// </list>
|
||||||
|
/// </summary>
|
||||||
|
public Action<float, float, string>? ProgressReporter { get => _progressReporter; set => _progressReporter = value; }
|
||||||
|
protected Action<float, float, string>? _progressReporter;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 背景颜色
|
||||||
|
/// </summary>
|
||||||
|
public Color BackgroundColor
|
||||||
|
{
|
||||||
|
get => _backgroundColor;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_backgroundColor = value;
|
||||||
|
var bcPma = value;
|
||||||
|
var a = bcPma.A / 255f;
|
||||||
|
bcPma.R = (byte)(bcPma.R * a);
|
||||||
|
bcPma.G = (byte)(bcPma.G * a);
|
||||||
|
bcPma.B = (byte)(bcPma.B * a);
|
||||||
|
_backgroundColorPma = bcPma;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
protected Color _backgroundColor = Color.Black;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 预乘后的背景颜色
|
||||||
|
/// </summary>
|
||||||
|
protected Color _backgroundColorPma = Color.Black;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 画面分辨率
|
||||||
|
/// <inheritdoc cref="RenderTexture.Size"/>
|
||||||
|
/// </summary>
|
||||||
|
public Vector2u Resolution
|
||||||
|
{
|
||||||
|
get => _renderTexture.Size;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
// XXX: 强制变成 2 的倍数, 防止像是 yuv420p 这种像素格式报错
|
||||||
|
value.X = value.X >> 1 << 1;
|
||||||
|
value.Y = value.Y >> 1 << 1;
|
||||||
|
if (value.X <= 0 || value.Y <= 0)
|
||||||
|
{
|
||||||
|
_logger.Warn("Omit invalid exporter resolution: {0}", value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_renderTexture.Size != value)
|
||||||
|
{
|
||||||
|
using var old = _renderTexture;
|
||||||
|
using var view = old.GetView();
|
||||||
|
var renderTexture = new RenderTexture(value.X, value.Y);
|
||||||
|
renderTexture.SetActive(false);
|
||||||
|
renderTexture.SetView(view);
|
||||||
|
_renderTexture = renderTexture;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <inheritdoc cref="View.Viewport"/>
|
||||||
|
/// </summary>
|
||||||
|
public FloatRect Viewport
|
||||||
|
{
|
||||||
|
get { using var view = _renderTexture.GetView(); return view.Viewport; }
|
||||||
|
set { using var view = _renderTexture.GetView(); view.Viewport = value; _renderTexture.SetView(view); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <inheritdoc cref="View.Center"/>
|
||||||
|
/// </summary>
|
||||||
|
public Vector2f Center
|
||||||
|
{
|
||||||
|
get { using var view = _renderTexture.GetView(); return view.Center; }
|
||||||
|
set { using var view = _renderTexture.GetView(); view.Center = value; _renderTexture.SetView(view); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <inheritdoc cref="View.Size"/>
|
||||||
|
/// </summary>
|
||||||
|
public Vector2f Size
|
||||||
|
{
|
||||||
|
get { using var view = _renderTexture.GetView(); return view.Size; }
|
||||||
|
set { using var view = _renderTexture.GetView(); view.Size = value; _renderTexture.SetView(view); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <inheritdoc cref="View.Rotation"/>
|
||||||
|
/// </summary>
|
||||||
|
public float Rotation
|
||||||
|
{
|
||||||
|
get { using var view = _renderTexture.GetView(); return view.Rotation; }
|
||||||
|
set { using var view = _renderTexture.GetView(); view.Rotation = value; _renderTexture.SetView(view); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取的一帧, 结果是预乘的
|
||||||
|
/// </summary>
|
||||||
|
protected virtual SFMLImageVideoFrame GetFrame(SpineObject[] spines)
|
||||||
|
{
|
||||||
|
_renderTexture.SetActive(true);
|
||||||
|
_renderTexture.Clear(_backgroundColorPma);
|
||||||
|
foreach (var sp in spines.Reverse()) _renderTexture.Draw(sp);
|
||||||
|
_renderTexture.Display();
|
||||||
|
_renderTexture.SetActive(false);
|
||||||
|
return new(_renderTexture.Texture.CopyToImage());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 导出给定的模型, 从前往后对应从上往下的渲染顺序
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="output">输出路径, 一般而言都是文件路径, 少数情况指定的是文件夹</param>
|
||||||
|
/// <param name="spines">要导出的模型, 从前往后对应从上往下的渲染顺序</param>
|
||||||
|
public abstract void Export(string output, params SpineObject[] spines);
|
||||||
|
|
||||||
|
#region IDisposable 接口实现
|
||||||
|
|
||||||
|
private bool _disposed = false;
|
||||||
|
|
||||||
|
protected virtual void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (_disposed) return;
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
_renderTexture.Dispose();
|
||||||
|
}
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
~BaseExporter()
|
||||||
|
{
|
||||||
|
Dispose(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Dispose(true);
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
100
Spine/Exporters/CustomFFmpegExporter.cs
Normal file
100
Spine/Exporters/CustomFFmpegExporter.cs
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
using FFMpegCore;
|
||||||
|
using FFMpegCore.Pipes;
|
||||||
|
using SFML.Graphics;
|
||||||
|
using SFML.System;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Spine.Exporters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 自定义参数的 FFmpeg 导出类
|
||||||
|
/// </summary>
|
||||||
|
public class CustomFFmpegExporter : VideoExporter
|
||||||
|
{
|
||||||
|
public CustomFFmpegExporter(uint width = 100, uint height = 100) : base(width, height) { }
|
||||||
|
public CustomFFmpegExporter(Vector2u resolution) : base(resolution) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <c>-f</c>
|
||||||
|
/// </summary>
|
||||||
|
public string? Format { get => _format; set => _format = value; }
|
||||||
|
private string? _format;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <c>-c:v</c>
|
||||||
|
/// </summary>
|
||||||
|
public string? Codec { get => _codec; set => _codec = value; }
|
||||||
|
private string? _codec;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <c>-pix_fmt</c>
|
||||||
|
/// </summary>
|
||||||
|
public string? PixelFormat { get => _pixelFormat; set => _pixelFormat = value; }
|
||||||
|
private string? _pixelFormat;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <c>-b:v</c>
|
||||||
|
/// </summary>
|
||||||
|
public string? Bitrate { get => _bitrate; set => _bitrate = value; }
|
||||||
|
private string? _bitrate;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <c>-vf</c>
|
||||||
|
/// </summary>
|
||||||
|
public string? Filter { get => _filter; set => _filter = value; }
|
||||||
|
private string? _filter;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 其他自定义参数
|
||||||
|
/// </summary>
|
||||||
|
public string? CustomArgs { get => _customArgs; set => _customArgs = value; }
|
||||||
|
private string? _customArgs;
|
||||||
|
|
||||||
|
private void SetOutputOptions(FFMpegArgumentOptions options)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(_format)) options.ForceFormat(_format);
|
||||||
|
if (!string.IsNullOrEmpty(_codec)) options.WithVideoCodec(_codec);
|
||||||
|
if (!string.IsNullOrEmpty(_pixelFormat)) options.ForcePixelFormat(_pixelFormat);
|
||||||
|
if (!string.IsNullOrEmpty(_bitrate)) options.WithCustomArgument($"-b:v {_bitrate}");
|
||||||
|
if (!string.IsNullOrEmpty(_filter)) options.WithCustomArgument($"-vf \"unpremultiply=inplace=1, {_filter}\"");
|
||||||
|
else options.WithCustomArgument("-vf unpremultiply=inplace=1");
|
||||||
|
if (!string.IsNullOrEmpty(_customArgs)) options.WithCustomArgument($"{_customArgs}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取的一帧, 结果是预乘的
|
||||||
|
/// </summary>
|
||||||
|
protected override SFMLImageVideoFrame GetFrame(SpineObject[] spines)
|
||||||
|
{
|
||||||
|
// BUG: 也许和 SFML 多线程或者 FFmpeg 调用有关, 当渲染线程也在运行的时候此处并行渲染会导致和 SFML 有关的内容都卡死
|
||||||
|
// 不知道为什么用 FFmpeg 必须临时创建 RenderTexture, 否则无法正常渲染, 会导致画面帧丢失
|
||||||
|
using var tex = new RenderTexture(_renderTexture.Size.X, _renderTexture.Size.Y);
|
||||||
|
using var view = _renderTexture.GetView();
|
||||||
|
tex.SetView(view);
|
||||||
|
tex.Clear(_backgroundColorPma);
|
||||||
|
foreach (var sp in spines.Reverse()) tex.Draw(sp);
|
||||||
|
tex.Display();
|
||||||
|
return new(tex.Texture.CopyToImage());
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Export(string output, CancellationToken ct, params SpineObject[] spines)
|
||||||
|
{
|
||||||
|
var videoFramesSource = new RawVideoPipeSource(GetFrames(spines, output, ct)) { FrameRate = _fps };
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var ffmpegArgs = FFMpegArguments.FromPipeInput(videoFramesSource).OutputToFile(output, true, SetOutputOptions);
|
||||||
|
_logger.Info("FFmpeg arguments: {0}", ffmpegArgs.Arguments);
|
||||||
|
ffmpegArgs.ProcessSynchronously();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Trace(ex.ToString());
|
||||||
|
_logger.Error("Failed to export {0} {1}, {2}", _format, output, ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
186
Spine/Exporters/FFmpegVideoExporter.cs
Normal file
186
Spine/Exporters/FFmpegVideoExporter.cs
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
using FFMpegCore;
|
||||||
|
using FFMpegCore.Enums;
|
||||||
|
using FFMpegCore.Pipes;
|
||||||
|
using NLog;
|
||||||
|
using SFML.Graphics;
|
||||||
|
using SFML.System;
|
||||||
|
using SkiaSharp;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Spine.Exporters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 基于 FFmpeg 命令行的导出类, 可以导出动图及视频格式
|
||||||
|
/// </summary>
|
||||||
|
public class FFmpegVideoExporter : VideoExporter
|
||||||
|
{
|
||||||
|
public FFmpegVideoExporter(uint width = 100, uint height = 100) : base(width, height) { }
|
||||||
|
public FFmpegVideoExporter(Vector2u resolution) : base(resolution) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// FFmpeg 导出格式
|
||||||
|
/// </summary>
|
||||||
|
public enum VideoFormat
|
||||||
|
{
|
||||||
|
Gif,
|
||||||
|
Webp,
|
||||||
|
Apng,
|
||||||
|
Mp4,
|
||||||
|
Webm,
|
||||||
|
Mkv,
|
||||||
|
Mov,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 视频格式
|
||||||
|
/// </summary>
|
||||||
|
public VideoFormat Format { get => _format; set => _format = value; }
|
||||||
|
private VideoFormat _format = VideoFormat.Mp4;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// [Gif/Webp/Apng] 动图是否循环
|
||||||
|
/// </summary>
|
||||||
|
public bool Loop { get => _loop; set => _loop = value; }
|
||||||
|
private bool _loop = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// [Webp] 质量
|
||||||
|
/// </summary>
|
||||||
|
public int Quality { get => _quality; set => _quality = Math.Clamp(value, 0, 100); }
|
||||||
|
private int _quality = 75;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// [Webp] 无损压缩
|
||||||
|
/// </summary>
|
||||||
|
public bool Lossless { get => _lossless; set => _lossless = value; }
|
||||||
|
private bool _lossless = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// [Apng] 预测器算法, 取值范围 0-5, 分别对应 none, sub, up, avg, paeth, mixed
|
||||||
|
/// </summary>
|
||||||
|
public int ApngPred { get => _apngPred; set => _apngPred = Math.Clamp(value, 0, 5); }
|
||||||
|
private int _apngPred = 5;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// [Mp4/Webm/Mkv] CRF
|
||||||
|
/// </summary>
|
||||||
|
public int Crf { get => _crf; set => _crf = Math.Clamp(value, 0, 63); }
|
||||||
|
private int _crf = 23;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// [Mov] prores_ks 编码器的配置等级, -1 是自动, 越高质量越好, 只有 4 及以上才有透明通道
|
||||||
|
/// </summary>
|
||||||
|
public int Profile { get => _profile; set => _profile = Math.Clamp(value, -1, 5); }
|
||||||
|
private int _profile = 5;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取的一帧, 结果是预乘的
|
||||||
|
/// </summary>
|
||||||
|
protected override SFMLImageVideoFrame GetFrame(SpineObject[] spines)
|
||||||
|
{
|
||||||
|
// BUG: 也许和 SFML 多线程或者 FFmpeg 调用有关, 当渲染线程也在运行的时候此处并行渲染会导致和 SFML 有关的内容都卡死
|
||||||
|
// 不知道为什么用 FFmpeg 必须临时创建 RenderTexture, 否则无法正常渲染, 会导致画面帧丢失
|
||||||
|
using var tex = new RenderTexture(_renderTexture.Size.X, _renderTexture.Size.Y);
|
||||||
|
using var view = _renderTexture.GetView();
|
||||||
|
tex.SetView(view);
|
||||||
|
tex.Clear(_backgroundColorPma);
|
||||||
|
foreach (var sp in spines.Reverse()) tex.Draw(sp);
|
||||||
|
tex.Display();
|
||||||
|
return new(tex.Texture.CopyToImage());
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Export(string output, CancellationToken ct, params SpineObject[] spines)
|
||||||
|
{
|
||||||
|
var videoFramesSource = new RawVideoPipeSource(GetFrames(spines, output, ct)) { FrameRate = _fps };
|
||||||
|
Action<FFMpegArgumentOptions> setOutputOptions = _format switch
|
||||||
|
{
|
||||||
|
VideoFormat.Gif => SetGifOptions,
|
||||||
|
VideoFormat.Webp => SetWebpOptions,
|
||||||
|
VideoFormat.Apng => SetApngOptions,
|
||||||
|
VideoFormat.Mp4 => SetMp4Options,
|
||||||
|
VideoFormat.Webm => SetWebmOptions,
|
||||||
|
VideoFormat.Mkv => SetMkvOptions,
|
||||||
|
VideoFormat.Mov => SetMovOptions,
|
||||||
|
_ => throw new NotImplementedException(),
|
||||||
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var ffmpegArgs = FFMpegArguments.FromPipeInput(videoFramesSource).OutputToFile(output, true, setOutputOptions);
|
||||||
|
|
||||||
|
_logger.Info("FFmpeg arguments: {0}", ffmpegArgs.Arguments);
|
||||||
|
ffmpegArgs.ProcessSynchronously();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Trace(ex.ToString());
|
||||||
|
_logger.Error("Failed to export {0} {1}, {2}", _format, output, ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetGifOptions(FFMpegArgumentOptions options)
|
||||||
|
{
|
||||||
|
// Gif 固定使用 256 调色板和 128 透明度阈值
|
||||||
|
var v = "split [s0][s1]";
|
||||||
|
var s0 = "[s0] palettegen=max_colors=256 [p]";
|
||||||
|
var s1 = "[s1][p] paletteuse=alpha_threshold=128";
|
||||||
|
var customArgs = $"-vf \"unpremultiply=inplace=1, {v};{s0};{s1}\" -loop {(_loop ? 0 : -1)}";
|
||||||
|
options.ForceFormat("gif")
|
||||||
|
.WithCustomArgument(customArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetWebpOptions(FFMpegArgumentOptions options)
|
||||||
|
{
|
||||||
|
var customArgs = $"-vf unpremultiply=inplace=1 -quality {_quality} -loop {(_loop ? 0 : 1)} -lossless {(_lossless ? 1 : 0)}";
|
||||||
|
options.ForceFormat("webp").WithVideoCodec("libwebp_anim").ForcePixelFormat("yuva420p")
|
||||||
|
.WithCustomArgument(customArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetApngOptions(FFMpegArgumentOptions options)
|
||||||
|
{
|
||||||
|
var customArgs = $"-vf unpremultiply=inplace=1 -plays {(_loop ? 0 : 1)} -pred {_apngPred}";
|
||||||
|
options.ForceFormat("apng").WithVideoCodec("apng").ForcePixelFormat("rgba")
|
||||||
|
.WithCustomArgument(customArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetMp4Options(FFMpegArgumentOptions options)
|
||||||
|
{
|
||||||
|
// XXX: windows 默认播放器在播放 MP4 格式时对于 libx264 编码器只支持 yuv420p 的像素格式
|
||||||
|
// 但是如果是 libx265 则没有该限制
|
||||||
|
var customArgs = "-vf unpremultiply=inplace=1";
|
||||||
|
options.ForceFormat("mp4").WithVideoCodec("libx264").ForcePixelFormat("yuv420p")
|
||||||
|
.WithFastStart()
|
||||||
|
.WithConstantRateFactor(_crf)
|
||||||
|
.WithCustomArgument(customArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetWebmOptions(FFMpegArgumentOptions options)
|
||||||
|
{
|
||||||
|
var customArgs = "-vf unpremultiply=inplace=1";
|
||||||
|
options.ForceFormat("webm").WithVideoCodec("libvpx-vp9").ForcePixelFormat("yuva420p")
|
||||||
|
.WithConstantRateFactor(_crf)
|
||||||
|
.WithCustomArgument(customArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetMkvOptions(FFMpegArgumentOptions options)
|
||||||
|
{
|
||||||
|
var customArgs = "-vf unpremultiply=inplace=1";
|
||||||
|
options.ForceFormat("matroska").WithVideoCodec("libx265").ForcePixelFormat("yuv444p")
|
||||||
|
.WithConstantRateFactor(_crf)
|
||||||
|
.WithCustomArgument(customArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetMovOptions(FFMpegArgumentOptions options)
|
||||||
|
{
|
||||||
|
var customArgs = "-vf unpremultiply=inplace=1";
|
||||||
|
options.ForceFormat("mov").WithVideoCodec("prores_ks").ForcePixelFormat("yuva444p10le")
|
||||||
|
.WithFastStart()
|
||||||
|
.WithCustomArgument($"-profile {_profile}")
|
||||||
|
.WithCustomArgument(customArgs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
Spine/Exporters/FrameExporter.cs
Normal file
37
Spine/Exporters/FrameExporter.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
using SFML.System;
|
||||||
|
using SkiaSharp;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Frozen;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Spine.Exporters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 单帧画面导出类
|
||||||
|
/// </summary>
|
||||||
|
public class FrameExporter : BaseExporter
|
||||||
|
{
|
||||||
|
public FrameExporter(uint width = 100, uint height = 100) : base(width, height) { }
|
||||||
|
public FrameExporter(Vector2u resolution) : base(resolution) { }
|
||||||
|
|
||||||
|
public SKEncodedImageFormat Format { get => _format; set => _format = value; }
|
||||||
|
protected SKEncodedImageFormat _format = SKEncodedImageFormat.Png;
|
||||||
|
|
||||||
|
public int Quality { get => _quality; set => _quality = Math.Clamp(value, 0, 100); }
|
||||||
|
protected int _quality = 80;
|
||||||
|
|
||||||
|
public override void Export(string output, params SpineObject[] spines)
|
||||||
|
{
|
||||||
|
using var frame = GetFrame(spines);
|
||||||
|
var info = new SKImageInfo(frame.Width, frame.Height, SKColorType.Rgba8888, SKAlphaType.Premul);
|
||||||
|
using var skImage = SKImage.FromPixelCopy(info, frame.Image.Pixels);
|
||||||
|
using var data = skImage.Encode(_format, _quality);
|
||||||
|
using var stream = File.OpenWrite(output);
|
||||||
|
data.SaveTo(stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
61
Spine/Exporters/FrameSequenceExporter.cs
Normal file
61
Spine/Exporters/FrameSequenceExporter.cs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
using NLog;
|
||||||
|
using SFML.System;
|
||||||
|
using SkiaSharp;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Spine.Exporters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 帧序列导出器, 导出 png 帧序列
|
||||||
|
/// </summary>
|
||||||
|
public class FrameSequenceExporter : VideoExporter
|
||||||
|
{
|
||||||
|
public FrameSequenceExporter(uint width = 100, uint height = 100) : base(width, height) { }
|
||||||
|
public FrameSequenceExporter(Vector2u resolution) : base(resolution) { }
|
||||||
|
|
||||||
|
public override void Export(string output, CancellationToken ct, params SpineObject[] spines)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(output);
|
||||||
|
|
||||||
|
int frameCount = GetFrameCount();
|
||||||
|
int frameIdx = 0;
|
||||||
|
|
||||||
|
_progressReporter?.Invoke(frameCount, 0, $"[{frameIdx}/{frameCount}] {output}");
|
||||||
|
foreach (var frame in GetFrames(spines))
|
||||||
|
{
|
||||||
|
if (ct.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
_logger.Info("Export cancelled");
|
||||||
|
frame.Dispose();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var savePath = Path.Combine(output, $"frame_{_fps}_{frameIdx:d6}.png");
|
||||||
|
var info = new SKImageInfo(frame.Width, frame.Height, SKColorType.Rgba8888, SKAlphaType.Premul);
|
||||||
|
|
||||||
|
_progressReporter?.Invoke(frameCount, frameIdx, $"[{frameIdx + 1}/{frameCount}] {savePath}");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var skImage = SKImage.FromPixelCopy(info, frame.Image.Pixels);
|
||||||
|
using var data = skImage.Encode(SKEncodedImageFormat.Png, 100);
|
||||||
|
using var stream = File.OpenWrite(savePath);
|
||||||
|
data.SaveTo(stream);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Trace(ex.ToString());
|
||||||
|
_logger.Error("Failed to save frame {0}, {1}", savePath, ex.Message);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
frame.Dispose();
|
||||||
|
}
|
||||||
|
frameIdx++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
52
Spine/Exporters/SFMLImageVideoFrame.cs
Normal file
52
Spine/Exporters/SFMLImageVideoFrame.cs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
using FFMpegCore.Pipes;
|
||||||
|
using SFML.Graphics;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Spine.Exporters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// <see cref="SFML.Graphics.Image"/> 帧对象包装类, 将接管给定对象生命周期
|
||||||
|
/// </summary>
|
||||||
|
public class SFMLImageVideoFrame(Image image) : IVideoFrame, IDisposable
|
||||||
|
{
|
||||||
|
private readonly Image _image = image;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 接管的 <see cref="SFML.Graphics.Image"/> 内部对象
|
||||||
|
/// </summary>
|
||||||
|
public Image Image => _image;
|
||||||
|
|
||||||
|
public int Width => (int)_image.Size.X;
|
||||||
|
public int Height => (int)_image.Size.Y;
|
||||||
|
public string Format => "rgba";
|
||||||
|
public void Serialize(Stream pipe) => pipe.Write(_image.Pixels);
|
||||||
|
public async Task SerializeAsync(Stream pipe, CancellationToken token) => await pipe.WriteAsync(_image.Pixels, token);
|
||||||
|
|
||||||
|
#region IDisposable 接口实现
|
||||||
|
|
||||||
|
private bool _disposed = false;
|
||||||
|
|
||||||
|
protected virtual void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (!_disposed)
|
||||||
|
{
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
_image.Dispose();
|
||||||
|
}
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Dispose(true);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
157
Spine/Exporters/VideoExporter.cs
Normal file
157
Spine/Exporters/VideoExporter.cs
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
using NLog;
|
||||||
|
using SFML.System;
|
||||||
|
using SkiaSharp;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Frozen;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Spine.Exporters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 多帧画面导出基类, 可以获取连续的帧序列
|
||||||
|
/// </summary>
|
||||||
|
public abstract class VideoExporter : BaseExporter
|
||||||
|
{
|
||||||
|
public VideoExporter(uint width, uint height) : base(width, height) { }
|
||||||
|
public VideoExporter(Vector2u resolution) : base(resolution) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 导出时长
|
||||||
|
/// </summary>
|
||||||
|
public float Duration
|
||||||
|
{
|
||||||
|
get => _duration;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value < 0)
|
||||||
|
{
|
||||||
|
_logger.Warn("Omit invalid duration: {0}", value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_duration = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
protected float _duration = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 帧率
|
||||||
|
/// </summary>
|
||||||
|
public float Fps
|
||||||
|
{
|
||||||
|
get => _fps;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value <= 0)
|
||||||
|
{
|
||||||
|
_logger.Warn("Omit invalid fps: {0}", value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_fps = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
protected float _fps = 24;
|
||||||
|
|
||||||
|
public float Speed
|
||||||
|
{
|
||||||
|
get => _speed;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_speed <= 0)
|
||||||
|
{
|
||||||
|
_logger.Warn("Omit invalid speed: {0}", value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_speed = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
protected float _speed = 1f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否保留最后一帧
|
||||||
|
/// </summary>
|
||||||
|
public bool KeepLast { get => _keepLast; set => _keepLast = value; }
|
||||||
|
protected bool _keepLast = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取总帧数
|
||||||
|
/// </summary>
|
||||||
|
public int GetFrameCount()
|
||||||
|
{
|
||||||
|
var delta = 1f / _fps;
|
||||||
|
var total = (int)(_duration * _fps); // 完整帧的数量
|
||||||
|
|
||||||
|
var deltaFinal = _duration - delta * total; // 最后一帧时长
|
||||||
|
var final = _keepLast && deltaFinal > 1e-3 ? 1 : 0;
|
||||||
|
|
||||||
|
var frameCount = 1 + total + final; // 所有帧的数量 = 起始帧 + 完整帧 + 最后一帧
|
||||||
|
return frameCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 生成帧序列
|
||||||
|
/// </summary>
|
||||||
|
protected IEnumerable<SFMLImageVideoFrame> GetFrames(SpineObject[] spines)
|
||||||
|
{
|
||||||
|
float delta = 1f / _fps;
|
||||||
|
int total = (int)(_duration * _fps); // 完整帧的数量
|
||||||
|
bool hasFinal = _keepLast && (_duration - delta * total) > 1e-3;
|
||||||
|
|
||||||
|
// 导出首帧
|
||||||
|
var firstFrame = GetFrame(spines);
|
||||||
|
yield return firstFrame;
|
||||||
|
|
||||||
|
// 导出完整帧
|
||||||
|
for (int i = 0; i < total; i++)
|
||||||
|
{
|
||||||
|
foreach (var spine in spines) spine.Update(delta * _speed);
|
||||||
|
yield return GetFrame(spines);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出最后一帧
|
||||||
|
if (hasFinal)
|
||||||
|
{
|
||||||
|
// XXX: 此处还是按照完整的一帧时长进行更新, 也许可以只更新准确的最后一帧时长
|
||||||
|
foreach (var spine in spines) spine.Update(delta * _speed);
|
||||||
|
yield return GetFrame(spines);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 生成帧序列, 支持中途取消和进度输出
|
||||||
|
/// </summary>
|
||||||
|
protected IEnumerable<SFMLImageVideoFrame> GetFrames(SpineObject[] spines, string output, CancellationToken ct)
|
||||||
|
{
|
||||||
|
int frameCount = GetFrameCount();
|
||||||
|
int frameIdx = 0;
|
||||||
|
|
||||||
|
_progressReporter?.Invoke(frameCount, 0, $"[{frameIdx}/{frameCount}] {output}");
|
||||||
|
foreach (var frame in GetFrames(spines))
|
||||||
|
{
|
||||||
|
if (ct.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
_logger.Info("Export cancelled");
|
||||||
|
frame.Dispose();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
_progressReporter?.Invoke(frameCount, frameIdx, $"[{frameIdx + 1}/{frameCount}] {output}");
|
||||||
|
yield return frame;
|
||||||
|
frameIdx++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed override void Export(string output, params SpineObject[] spines) => Export(output, default, spines);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 导出给定的模型, 从前往后对应从上往下的渲染顺序
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="output">输出路径, 一般而言都是文件路径, 少数情况指定的是文件夹</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <param name="spines">要导出的模型, 从前往后对应从上往下的渲染顺序</param>
|
||||||
|
public abstract void Export(string output, CancellationToken ct, params SpineObject[] spines);
|
||||||
|
}
|
||||||
|
}
|
||||||
406
Spine/Implementations/TextureLoader.cs
Normal file
406
Spine/Implementations/TextureLoader.cs
Normal file
@@ -0,0 +1,406 @@
|
|||||||
|
using NLog;
|
||||||
|
using SFML.Graphics;
|
||||||
|
using SkiaSharp;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Spine.Implementations
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 实现不同版本的 TextureLoader
|
||||||
|
/// </summary>
|
||||||
|
public class TextureLoader :
|
||||||
|
SpineRuntime21.TextureLoader,
|
||||||
|
SpineRuntime34.TextureLoader,
|
||||||
|
SpineRuntime35.TextureLoader,
|
||||||
|
SpineRuntime36.TextureLoader,
|
||||||
|
SpineRuntime37.TextureLoader,
|
||||||
|
SpineRuntime38.TextureLoader,
|
||||||
|
SpineRuntime40.TextureLoader,
|
||||||
|
SpineRuntime41.TextureLoader,
|
||||||
|
SpineRuntime42.TextureLoader
|
||||||
|
{
|
||||||
|
private static readonly Logger _logger = LogManager.GetCurrentClassLogger();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 默认的全局纹理加载器
|
||||||
|
/// </summary>
|
||||||
|
public static TextureLoader DefaultLoader { get; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 在读取纹理时强制进行通道预乘操作
|
||||||
|
/// </summary>
|
||||||
|
public bool ForcePremul { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 强制使用 Nearest
|
||||||
|
/// </summary>
|
||||||
|
public bool ForceNearest { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 强制启用 Mipmap
|
||||||
|
/// </summary>
|
||||||
|
public bool ForceMipmap { get; set; }
|
||||||
|
|
||||||
|
private Texture ReadTexture(string path)
|
||||||
|
{
|
||||||
|
if (!File.Exists(path))
|
||||||
|
{
|
||||||
|
_logger.Error($"Texture file not found, {path}");
|
||||||
|
throw new FileNotFoundException("Texture file not found", path);
|
||||||
|
}
|
||||||
|
|
||||||
|
using var codec = SKCodec.Create(path, out var result);
|
||||||
|
if (codec is null || result != SKCodecResult.Success)
|
||||||
|
{
|
||||||
|
_logger.Error($"Failed to create codec '{path}', {result}");
|
||||||
|
throw new InvalidOperationException($"Failed to create codec '{path}', {result}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var width = codec.Info.Width;
|
||||||
|
var height = codec.Info.Height;
|
||||||
|
|
||||||
|
// 判断是否需要强制预乘
|
||||||
|
var alphaType = ForcePremul ? SKAlphaType.Premul : SKAlphaType.Unpremul;
|
||||||
|
var info = new SKImageInfo(width, height, SKColorType.Rgba8888, alphaType);
|
||||||
|
|
||||||
|
result = codec.GetPixels(info, out var pixels);
|
||||||
|
if (result != SKCodecResult.Success)
|
||||||
|
{
|
||||||
|
_logger.Error($"Failed to decode image '{path}', {result}");
|
||||||
|
throw new InvalidOperationException($"Failed to decode image '{path}', {result}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Texture tex = new((uint)width, (uint)height);
|
||||||
|
tex.Update(pixels);
|
||||||
|
return tex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void Load(SpineRuntime21.AtlasPage page, string path)
|
||||||
|
{
|
||||||
|
var texture = ReadTexture(path);
|
||||||
|
|
||||||
|
if (page.magFilter == SpineRuntime21.TextureFilter.Linear)
|
||||||
|
{
|
||||||
|
texture.Smooth = true;
|
||||||
|
}
|
||||||
|
if (page.uWrap == SpineRuntime21.TextureWrap.Repeat && page.vWrap == SpineRuntime21.TextureWrap.Repeat)
|
||||||
|
{
|
||||||
|
texture.Repeated = true;
|
||||||
|
}
|
||||||
|
switch (page.minFilter)
|
||||||
|
{
|
||||||
|
case SpineRuntime21.TextureFilter.Linear:
|
||||||
|
texture.Smooth = true;
|
||||||
|
break;
|
||||||
|
case SpineRuntime21.TextureFilter.MipMap:
|
||||||
|
case SpineRuntime21.TextureFilter.MipMapNearestNearest:
|
||||||
|
texture.GenerateMipmap();
|
||||||
|
break;
|
||||||
|
case SpineRuntime21.TextureFilter.MipMapLinearNearest:
|
||||||
|
case SpineRuntime21.TextureFilter.MipMapNearestLinear:
|
||||||
|
case SpineRuntime21.TextureFilter.MipMapLinearLinear:
|
||||||
|
texture.Smooth = true;
|
||||||
|
texture.GenerateMipmap();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ForceNearest) texture.Smooth = false;
|
||||||
|
if (ForceMipmap) texture.GenerateMipmap();
|
||||||
|
|
||||||
|
page.rendererObject = texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void Load(SpineRuntime34.AtlasPage page, string path)
|
||||||
|
{
|
||||||
|
var texture = ReadTexture(path);
|
||||||
|
|
||||||
|
if (page.magFilter == SpineRuntime34.TextureFilter.Linear)
|
||||||
|
{
|
||||||
|
texture.Smooth = true;
|
||||||
|
}
|
||||||
|
if (page.uWrap == SpineRuntime34.TextureWrap.Repeat && page.vWrap == SpineRuntime34.TextureWrap.Repeat)
|
||||||
|
{
|
||||||
|
texture.Repeated = true;
|
||||||
|
}
|
||||||
|
switch (page.minFilter)
|
||||||
|
{
|
||||||
|
case SpineRuntime34.TextureFilter.Linear:
|
||||||
|
texture.Smooth = true;
|
||||||
|
break;
|
||||||
|
case SpineRuntime34.TextureFilter.MipMap:
|
||||||
|
case SpineRuntime34.TextureFilter.MipMapNearestNearest:
|
||||||
|
texture.GenerateMipmap();
|
||||||
|
break;
|
||||||
|
case SpineRuntime34.TextureFilter.MipMapLinearNearest:
|
||||||
|
case SpineRuntime34.TextureFilter.MipMapNearestLinear:
|
||||||
|
case SpineRuntime34.TextureFilter.MipMapLinearLinear:
|
||||||
|
texture.Smooth = true;
|
||||||
|
texture.GenerateMipmap();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ForceNearest) texture.Smooth = false;
|
||||||
|
if (ForceMipmap) texture.GenerateMipmap();
|
||||||
|
|
||||||
|
page.rendererObject = texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void Load(SpineRuntime35.AtlasPage page, string path)
|
||||||
|
{
|
||||||
|
var texture = ReadTexture(path);
|
||||||
|
|
||||||
|
if (page.magFilter == SpineRuntime35.TextureFilter.Linear)
|
||||||
|
{
|
||||||
|
texture.Smooth = true;
|
||||||
|
}
|
||||||
|
if (page.uWrap == SpineRuntime35.TextureWrap.Repeat && page.vWrap == SpineRuntime35.TextureWrap.Repeat)
|
||||||
|
{
|
||||||
|
texture.Repeated = true;
|
||||||
|
}
|
||||||
|
switch (page.minFilter)
|
||||||
|
{
|
||||||
|
case SpineRuntime35.TextureFilter.Linear:
|
||||||
|
texture.Smooth = true;
|
||||||
|
break;
|
||||||
|
case SpineRuntime35.TextureFilter.MipMap:
|
||||||
|
case SpineRuntime35.TextureFilter.MipMapNearestNearest:
|
||||||
|
texture.GenerateMipmap();
|
||||||
|
break;
|
||||||
|
case SpineRuntime35.TextureFilter.MipMapLinearNearest:
|
||||||
|
case SpineRuntime35.TextureFilter.MipMapNearestLinear:
|
||||||
|
case SpineRuntime35.TextureFilter.MipMapLinearLinear:
|
||||||
|
texture.Smooth = true;
|
||||||
|
texture.GenerateMipmap();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ForceNearest) texture.Smooth = false;
|
||||||
|
if (ForceMipmap) texture.GenerateMipmap();
|
||||||
|
|
||||||
|
page.rendererObject = texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void Load(SpineRuntime36.AtlasPage page, string path)
|
||||||
|
{
|
||||||
|
var texture = ReadTexture(path);
|
||||||
|
|
||||||
|
if (page.magFilter == SpineRuntime36.TextureFilter.Linear)
|
||||||
|
{
|
||||||
|
texture.Smooth = true;
|
||||||
|
}
|
||||||
|
if (page.uWrap == SpineRuntime36.TextureWrap.Repeat && page.vWrap == SpineRuntime36.TextureWrap.Repeat)
|
||||||
|
{
|
||||||
|
texture.Repeated = true;
|
||||||
|
}
|
||||||
|
switch (page.minFilter)
|
||||||
|
{
|
||||||
|
case SpineRuntime36.TextureFilter.Linear:
|
||||||
|
texture.Smooth = true;
|
||||||
|
break;
|
||||||
|
case SpineRuntime36.TextureFilter.MipMap:
|
||||||
|
case SpineRuntime36.TextureFilter.MipMapNearestNearest:
|
||||||
|
texture.GenerateMipmap();
|
||||||
|
break;
|
||||||
|
case SpineRuntime36.TextureFilter.MipMapLinearNearest:
|
||||||
|
case SpineRuntime36.TextureFilter.MipMapNearestLinear:
|
||||||
|
case SpineRuntime36.TextureFilter.MipMapLinearLinear:
|
||||||
|
texture.Smooth = true;
|
||||||
|
texture.GenerateMipmap();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ForceNearest) texture.Smooth = false;
|
||||||
|
if (ForceMipmap) texture.GenerateMipmap();
|
||||||
|
|
||||||
|
page.rendererObject = texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void Load(SpineRuntime37.AtlasPage page, string path)
|
||||||
|
{
|
||||||
|
var texture = ReadTexture(path);
|
||||||
|
|
||||||
|
if (page.magFilter == SpineRuntime37.TextureFilter.Linear)
|
||||||
|
{
|
||||||
|
texture.Smooth = true;
|
||||||
|
}
|
||||||
|
if (page.uWrap == SpineRuntime37.TextureWrap.Repeat && page.vWrap == SpineRuntime37.TextureWrap.Repeat)
|
||||||
|
{
|
||||||
|
texture.Repeated = true;
|
||||||
|
}
|
||||||
|
switch (page.minFilter)
|
||||||
|
{
|
||||||
|
case SpineRuntime37.TextureFilter.Linear:
|
||||||
|
texture.Smooth = true;
|
||||||
|
break;
|
||||||
|
case SpineRuntime37.TextureFilter.MipMap:
|
||||||
|
case SpineRuntime37.TextureFilter.MipMapNearestNearest:
|
||||||
|
texture.GenerateMipmap();
|
||||||
|
break;
|
||||||
|
case SpineRuntime37.TextureFilter.MipMapLinearNearest:
|
||||||
|
case SpineRuntime37.TextureFilter.MipMapNearestLinear:
|
||||||
|
case SpineRuntime37.TextureFilter.MipMapLinearLinear:
|
||||||
|
texture.Smooth = true;
|
||||||
|
texture.GenerateMipmap();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ForceNearest) texture.Smooth = false;
|
||||||
|
if (ForceMipmap) texture.GenerateMipmap();
|
||||||
|
|
||||||
|
page.rendererObject = texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void Load(SpineRuntime38.AtlasPage page, string path)
|
||||||
|
{
|
||||||
|
var texture = ReadTexture(path);
|
||||||
|
|
||||||
|
if (page.magFilter == SpineRuntime38.TextureFilter.Linear)
|
||||||
|
{
|
||||||
|
texture.Smooth = true;
|
||||||
|
}
|
||||||
|
if (page.uWrap == SpineRuntime38.TextureWrap.Repeat && page.vWrap == SpineRuntime38.TextureWrap.Repeat)
|
||||||
|
{
|
||||||
|
texture.Repeated = true;
|
||||||
|
}
|
||||||
|
switch (page.minFilter)
|
||||||
|
{
|
||||||
|
case SpineRuntime38.TextureFilter.Linear:
|
||||||
|
texture.Smooth = true;
|
||||||
|
break;
|
||||||
|
case SpineRuntime38.TextureFilter.MipMap:
|
||||||
|
case SpineRuntime38.TextureFilter.MipMapNearestNearest:
|
||||||
|
texture.GenerateMipmap();
|
||||||
|
break;
|
||||||
|
case SpineRuntime38.TextureFilter.MipMapLinearNearest:
|
||||||
|
case SpineRuntime38.TextureFilter.MipMapNearestLinear:
|
||||||
|
case SpineRuntime38.TextureFilter.MipMapLinearLinear:
|
||||||
|
texture.Smooth = true;
|
||||||
|
texture.GenerateMipmap();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ForceNearest) texture.Smooth = false;
|
||||||
|
if (ForceMipmap) texture.GenerateMipmap();
|
||||||
|
|
||||||
|
page.rendererObject = texture;
|
||||||
|
|
||||||
|
// 似乎是不需要设置的, 因为存在某些 png 和 atlas 大小不同的情况, 一般是有一些缩放, 如果设置了反而渲染异常
|
||||||
|
// page.width = (int)texture.Size.X;
|
||||||
|
// page.height = (int)texture.Size.Y;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void Load(SpineRuntime40.AtlasPage page, string path)
|
||||||
|
{
|
||||||
|
var texture = ReadTexture(path);
|
||||||
|
|
||||||
|
if (page.magFilter == SpineRuntime40.TextureFilter.Linear)
|
||||||
|
{
|
||||||
|
texture.Smooth = true;
|
||||||
|
}
|
||||||
|
if (page.uWrap == SpineRuntime40.TextureWrap.Repeat && page.vWrap == SpineRuntime40.TextureWrap.Repeat)
|
||||||
|
{
|
||||||
|
texture.Repeated = true;
|
||||||
|
}
|
||||||
|
switch (page.minFilter)
|
||||||
|
{
|
||||||
|
case SpineRuntime40.TextureFilter.Linear:
|
||||||
|
texture.Smooth = true;
|
||||||
|
break;
|
||||||
|
case SpineRuntime40.TextureFilter.MipMap:
|
||||||
|
case SpineRuntime40.TextureFilter.MipMapNearestNearest:
|
||||||
|
texture.GenerateMipmap();
|
||||||
|
break;
|
||||||
|
case SpineRuntime40.TextureFilter.MipMapLinearNearest:
|
||||||
|
case SpineRuntime40.TextureFilter.MipMapNearestLinear:
|
||||||
|
case SpineRuntime40.TextureFilter.MipMapLinearLinear:
|
||||||
|
texture.Smooth = true;
|
||||||
|
texture.GenerateMipmap();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ForceNearest) texture.Smooth = false;
|
||||||
|
if (ForceMipmap) texture.GenerateMipmap();
|
||||||
|
|
||||||
|
page.rendererObject = texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void Load(SpineRuntime41.AtlasPage page, string path)
|
||||||
|
{
|
||||||
|
var texture = ReadTexture(path);
|
||||||
|
|
||||||
|
if (page.magFilter == SpineRuntime41.TextureFilter.Linear)
|
||||||
|
{
|
||||||
|
texture.Smooth = true;
|
||||||
|
}
|
||||||
|
if (page.uWrap == SpineRuntime41.TextureWrap.Repeat && page.vWrap == SpineRuntime41.TextureWrap.Repeat)
|
||||||
|
{
|
||||||
|
texture.Repeated = true;
|
||||||
|
}
|
||||||
|
switch (page.minFilter)
|
||||||
|
{
|
||||||
|
case SpineRuntime41.TextureFilter.Linear:
|
||||||
|
texture.Smooth = true;
|
||||||
|
break;
|
||||||
|
case SpineRuntime41.TextureFilter.MipMap:
|
||||||
|
case SpineRuntime41.TextureFilter.MipMapNearestNearest:
|
||||||
|
texture.GenerateMipmap();
|
||||||
|
break;
|
||||||
|
case SpineRuntime41.TextureFilter.MipMapLinearNearest:
|
||||||
|
case SpineRuntime41.TextureFilter.MipMapNearestLinear:
|
||||||
|
case SpineRuntime41.TextureFilter.MipMapLinearLinear:
|
||||||
|
texture.Smooth = true;
|
||||||
|
texture.GenerateMipmap();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ForceNearest) texture.Smooth = false;
|
||||||
|
if (ForceMipmap) texture.GenerateMipmap();
|
||||||
|
|
||||||
|
page.rendererObject = texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void Load(SpineRuntime42.AtlasPage page, string path)
|
||||||
|
{
|
||||||
|
var texture = ReadTexture(path);
|
||||||
|
|
||||||
|
if (page.magFilter == SpineRuntime42.TextureFilter.Linear)
|
||||||
|
{
|
||||||
|
texture.Smooth = true;
|
||||||
|
}
|
||||||
|
if (page.uWrap == SpineRuntime42.TextureWrap.Repeat && page.vWrap == SpineRuntime42.TextureWrap.Repeat)
|
||||||
|
{
|
||||||
|
texture.Repeated = true;
|
||||||
|
}
|
||||||
|
switch (page.minFilter)
|
||||||
|
{
|
||||||
|
case SpineRuntime42.TextureFilter.Linear:
|
||||||
|
texture.Smooth = true;
|
||||||
|
break;
|
||||||
|
case SpineRuntime42.TextureFilter.MipMap:
|
||||||
|
case SpineRuntime42.TextureFilter.MipMapNearestNearest:
|
||||||
|
texture.GenerateMipmap();
|
||||||
|
break;
|
||||||
|
case SpineRuntime42.TextureFilter.MipMapLinearNearest:
|
||||||
|
case SpineRuntime42.TextureFilter.MipMapNearestLinear:
|
||||||
|
case SpineRuntime42.TextureFilter.MipMapLinearLinear:
|
||||||
|
texture.Smooth = true;
|
||||||
|
texture.GenerateMipmap();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ForceNearest) texture.Smooth = false;
|
||||||
|
if (ForceMipmap) texture.GenerateMipmap();
|
||||||
|
|
||||||
|
page.rendererObject = texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void Unload(object texture)
|
||||||
|
{
|
||||||
|
((Texture)texture).Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
Spine/Implementations/V21/Animation21.cs
Normal file
23
Spine/Implementations/V21/Animation21.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
using Spine.Interfaces;
|
||||||
|
using SpineRuntime21;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V21
|
||||||
|
{
|
||||||
|
internal sealed class Animation21(Animation innerObject) : IAnimation
|
||||||
|
{
|
||||||
|
private readonly Animation _o = innerObject;
|
||||||
|
|
||||||
|
public Animation InnerObject => _o;
|
||||||
|
|
||||||
|
public string Name => _o.Name;
|
||||||
|
|
||||||
|
public float Duration => _o.Duration;
|
||||||
|
|
||||||
|
public override string ToString() => _o.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
179
Spine/Implementations/V21/AnimationState21.cs
Normal file
179
Spine/Implementations/V21/AnimationState21.cs
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using SpineRuntime21;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V21
|
||||||
|
{
|
||||||
|
internal sealed class AnimationState21(AnimationState innerObject, SpineObjectData21 data) : IAnimationState
|
||||||
|
{
|
||||||
|
private readonly AnimationState _o = innerObject;
|
||||||
|
private readonly SpineObjectData21 _data = data;
|
||||||
|
|
||||||
|
private readonly Dictionary<TrackEntry, TrackEntry21> _trackEntryPool = [];
|
||||||
|
|
||||||
|
private readonly Dictionary<IAnimationState.TrackEntryDelegate, AnimationState.TrackEntryDelegate> _eventMapping = [];
|
||||||
|
private readonly Dictionary<IAnimationState.TrackEntryDelegate, int> _eventCount = [];
|
||||||
|
|
||||||
|
public AnimationState InnerObject => _o;
|
||||||
|
|
||||||
|
#pragma warning disable CS0067
|
||||||
|
|
||||||
|
// NOTE: 2.1 没有这两个事件
|
||||||
|
public event IAnimationState.TrackEntryDelegate? Interrupt;
|
||||||
|
public event IAnimationState.TrackEntryDelegate? Dispose;
|
||||||
|
|
||||||
|
#pragma warning restore CS0067
|
||||||
|
|
||||||
|
public event IAnimationState.TrackEntryDelegate? Start
|
||||||
|
{
|
||||||
|
add
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (!_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_eventMapping[value] = f = (t) => value(GetTrackEntry(t));
|
||||||
|
_eventCount[value] = 0;
|
||||||
|
}
|
||||||
|
_o.Start += f;
|
||||||
|
_eventCount[value]++;
|
||||||
|
}
|
||||||
|
remove
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_o.Start -= f;
|
||||||
|
_eventCount[value]--;
|
||||||
|
if (_eventCount[value] <= 0)
|
||||||
|
{
|
||||||
|
_eventMapping.Remove(value);
|
||||||
|
_eventCount.Remove(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public event IAnimationState.TrackEntryDelegate? End
|
||||||
|
{
|
||||||
|
add
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (!_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_eventMapping[value] = f = (t) => value(GetTrackEntry(t));
|
||||||
|
_eventCount[value] = 0;
|
||||||
|
}
|
||||||
|
_o.End += f;
|
||||||
|
_eventCount[value]++;
|
||||||
|
}
|
||||||
|
remove
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_o.End -= f;
|
||||||
|
_eventCount[value]--;
|
||||||
|
if (_eventCount[value] <= 0)
|
||||||
|
{
|
||||||
|
_eventMapping.Remove(value);
|
||||||
|
_eventCount.Remove(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public event IAnimationState.TrackEntryDelegate? Complete
|
||||||
|
{
|
||||||
|
add
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (!_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_eventMapping[value] = f = (t) => value(GetTrackEntry(t));
|
||||||
|
_eventCount[value] = 0;
|
||||||
|
}
|
||||||
|
_o.Complete += f;
|
||||||
|
_eventCount[value]++;
|
||||||
|
}
|
||||||
|
remove
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_o.Complete -= f;
|
||||||
|
_eventCount[value]--;
|
||||||
|
if (_eventCount[value] <= 0)
|
||||||
|
{
|
||||||
|
_eventMapping.Remove(value);
|
||||||
|
_eventCount.Remove(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public float TimeScale { get => _o.TimeScale; set => _o.TimeScale = value; }
|
||||||
|
|
||||||
|
public void Update(float delta) => _o.Update(delta);
|
||||||
|
|
||||||
|
public void Apply(ISkeleton skeleton)
|
||||||
|
{
|
||||||
|
if (skeleton is Skeleton21 skel)
|
||||||
|
{
|
||||||
|
_o.Apply(skel.InnerObject);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Received {skeleton.GetType().Name}", nameof(skeleton));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取 <see cref="ITrackEntry"/> 对象, 不存在则创建
|
||||||
|
/// </summary>
|
||||||
|
public ITrackEntry GetTrackEntry(TrackEntry trackEntry)
|
||||||
|
{
|
||||||
|
if (!_trackEntryPool.TryGetValue(trackEntry, out var tr))
|
||||||
|
_trackEntryPool[trackEntry] = tr = new(trackEntry, this, _data);
|
||||||
|
return tr;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<ITrackEntry?> IterTracks() => _o.Tracks.Select(t => t is null ? null : GetTrackEntry(t));
|
||||||
|
|
||||||
|
public ITrackEntry? GetCurrent(int index) { var t = _o.GetCurrent(index); return t is null ? null : GetTrackEntry(t); }
|
||||||
|
|
||||||
|
public void ClearTrack(int index) => _o.ClearTrack(index);
|
||||||
|
|
||||||
|
public void ClearTracks() => _o.ClearTracks();
|
||||||
|
|
||||||
|
public ITrackEntry SetAnimation(int trackIndex, string animationName, bool loop)
|
||||||
|
=> GetTrackEntry(_o.SetAnimation(trackIndex, animationName, loop));
|
||||||
|
|
||||||
|
public ITrackEntry SetAnimation(int trackIndex, IAnimation animation, bool loop)
|
||||||
|
{
|
||||||
|
if (animation is Animation21 anime)
|
||||||
|
return GetTrackEntry(_o.SetAnimation(trackIndex, anime.InnerObject, loop));
|
||||||
|
throw new ArgumentException($"Received {animation.GetType().Name}", nameof(animation));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ITrackEntry SetEmptyAnimation(int trackIndex, float mixDuration) => GetTrackEntry(_o.SetEmptyAnimation(trackIndex, mixDuration));
|
||||||
|
|
||||||
|
public void SetEmptyAnimations(float mixDuration) => _o.SetEmptyAnimations(mixDuration);
|
||||||
|
|
||||||
|
public ITrackEntry AddAnimation(int trackIndex, string animationName, bool loop, float delay)
|
||||||
|
=> GetTrackEntry(_o.AddAnimation(trackIndex, animationName, loop, delay));
|
||||||
|
|
||||||
|
public ITrackEntry AddAnimation(int trackIndex, IAnimation animation, bool loop, float delay)
|
||||||
|
{
|
||||||
|
if (animation is Animation21 anime)
|
||||||
|
return GetTrackEntry(_o.AddAnimation(trackIndex, anime.InnerObject, loop, delay));
|
||||||
|
throw new ArgumentException($"Received {animation.GetType().Name}", nameof(animation));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ITrackEntry AddEmptyAnimation(int trackIndex, float mixDuration, float delay)
|
||||||
|
=> GetTrackEntry(_o.AddEmptyAnimation(trackIndex, mixDuration, delay));
|
||||||
|
|
||||||
|
public override string ToString() => _o.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
24
Spine/Implementations/V21/Attachments/Attachment21.cs
Normal file
24
Spine/Implementations/V21/Attachments/Attachment21.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using Spine.Interfaces.Attachments;
|
||||||
|
using SpineRuntime21;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V21.Attachments
|
||||||
|
{
|
||||||
|
internal abstract class Attachment21(Attachment innerObject) : IAttachment
|
||||||
|
{
|
||||||
|
private readonly Attachment _o = innerObject;
|
||||||
|
|
||||||
|
public virtual Attachment InnerObject => _o;
|
||||||
|
|
||||||
|
public string Name => _o.Name;
|
||||||
|
|
||||||
|
public abstract int ComputeWorldVertices(ISlot slot, ref float[] worldVertices);
|
||||||
|
|
||||||
|
public override string ToString() => _o.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Implementations.V21;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using Spine.Interfaces.Attachments;
|
||||||
|
using SpineRuntime21;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V21.Attachments
|
||||||
|
{
|
||||||
|
internal sealed class BoundingBoxAttachment21(BoundingBoxAttachment innerObject) :
|
||||||
|
Attachment21(innerObject),
|
||||||
|
IBoundingBoxAttachment
|
||||||
|
{
|
||||||
|
private readonly BoundingBoxAttachment _o = innerObject;
|
||||||
|
|
||||||
|
public override BoundingBoxAttachment InnerObject => _o;
|
||||||
|
|
||||||
|
public override int ComputeWorldVertices(ISlot slot, ref float[] worldVertices)
|
||||||
|
{
|
||||||
|
if (slot is Slot21 st)
|
||||||
|
{
|
||||||
|
var length = _o.Vertices.Length;
|
||||||
|
if (worldVertices.Length < length) worldVertices = new float[length];
|
||||||
|
_o.ComputeWorldVertices(st.InnerObject.Bone, worldVertices);
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot21)}, but received {slot.GetType().Name}", nameof(slot));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
46
Spine/Implementations/V21/Attachments/MeshAttachment21.cs
Normal file
46
Spine/Implementations/V21/Attachments/MeshAttachment21.cs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Implementations.V21;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using Spine.Interfaces.Attachments;
|
||||||
|
using SpineRuntime21;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V21.Attachments
|
||||||
|
{
|
||||||
|
internal sealed class MeshAttachment21(MeshAttachment innerObject) :
|
||||||
|
Attachment21(innerObject),
|
||||||
|
IMeshAttachment
|
||||||
|
{
|
||||||
|
private readonly MeshAttachment _o = innerObject;
|
||||||
|
|
||||||
|
public override MeshAttachment InnerObject => _o;
|
||||||
|
|
||||||
|
public override int ComputeWorldVertices(ISlot slot, ref float[] worldVertices)
|
||||||
|
{
|
||||||
|
if (slot is Slot21 st)
|
||||||
|
{
|
||||||
|
var length = _o.Vertices.Length;
|
||||||
|
if (worldVertices.Length < length) worldVertices = new float[length];
|
||||||
|
_o.ComputeWorldVertices(st.InnerObject, worldVertices);
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot21)}, but received {slot.GetType().Name}", nameof(slot));
|
||||||
|
}
|
||||||
|
|
||||||
|
public float R { get => _o.R; set => _o.R = value; }
|
||||||
|
public float G { get => _o.G; set => _o.G = value; }
|
||||||
|
public float B { get => _o.B; set => _o.B = value; }
|
||||||
|
public float A { get => _o.A; set => _o.A = value; }
|
||||||
|
|
||||||
|
public SFML.Graphics.Texture RendererObject => (SFML.Graphics.Texture)((AtlasRegion)_o.RendererObject).page.rendererObject;
|
||||||
|
|
||||||
|
public float[] UVs => _o.UVs;
|
||||||
|
|
||||||
|
public int[] Triangles => _o.Triangles;
|
||||||
|
|
||||||
|
public int HullLength => _o.HullLength;
|
||||||
|
}
|
||||||
|
}
|
||||||
41
Spine/Implementations/V21/Attachments/RegionAttachment21.cs
Normal file
41
Spine/Implementations/V21/Attachments/RegionAttachment21.cs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Implementations.V21;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using Spine.Interfaces.Attachments;
|
||||||
|
using SpineRuntime21;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V21.Attachments
|
||||||
|
{
|
||||||
|
internal sealed class RegionAttachment21(RegionAttachment innerObject) :
|
||||||
|
Attachment21(innerObject),
|
||||||
|
IRegionAttachment
|
||||||
|
{
|
||||||
|
private readonly RegionAttachment _o = innerObject;
|
||||||
|
|
||||||
|
public override RegionAttachment InnerObject => _o;
|
||||||
|
|
||||||
|
public override int ComputeWorldVertices(ISlot slot, ref float[] worldVertices)
|
||||||
|
{
|
||||||
|
if (slot is Slot21 st)
|
||||||
|
{
|
||||||
|
if (worldVertices.Length < 8) worldVertices = new float[8];
|
||||||
|
_o.ComputeWorldVertices(st.InnerObject.Bone, worldVertices);
|
||||||
|
return 8;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot21)}, but received {slot.GetType().Name}", nameof(slot));
|
||||||
|
}
|
||||||
|
|
||||||
|
public float R { get => _o.R; set => _o.R = value; }
|
||||||
|
public float G { get => _o.G; set => _o.G = value; }
|
||||||
|
public float B { get => _o.B; set => _o.B = value; }
|
||||||
|
public float A { get => _o.A; set => _o.A = value; }
|
||||||
|
|
||||||
|
public SFML.Graphics.Texture RendererObject => (SFML.Graphics.Texture)((AtlasRegion)_o.RendererObject).page.rendererObject;
|
||||||
|
|
||||||
|
public float[] UVs => _o.UVs;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Implementations.V21;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using Spine.Interfaces.Attachments;
|
||||||
|
using SpineRuntime21;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V21.Attachments
|
||||||
|
{
|
||||||
|
internal sealed class SkinnedMeshAttachment21(SkinnedMeshAttachment innerObject) :
|
||||||
|
Attachment21(innerObject),
|
||||||
|
ISkinnedMeshAttachment
|
||||||
|
{
|
||||||
|
private readonly SkinnedMeshAttachment _o = innerObject;
|
||||||
|
|
||||||
|
public override SkinnedMeshAttachment InnerObject => _o;
|
||||||
|
|
||||||
|
public override int ComputeWorldVertices(ISlot slot, ref float[] worldVertices)
|
||||||
|
{
|
||||||
|
if (slot is Slot21 st)
|
||||||
|
{
|
||||||
|
var length = _o.UVs.Length;
|
||||||
|
if (worldVertices.Length < length) worldVertices = new float[length];
|
||||||
|
_o.ComputeWorldVertices(st.InnerObject, worldVertices);
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot21)}, but received {slot.GetType().Name}", nameof(slot));
|
||||||
|
}
|
||||||
|
|
||||||
|
public float R { get => _o.R; set => _o.R = value; }
|
||||||
|
public float G { get => _o.G; set => _o.G = value; }
|
||||||
|
public float B { get => _o.B; set => _o.B = value; }
|
||||||
|
public float A { get => _o.A; set => _o.A = value; }
|
||||||
|
|
||||||
|
public SFML.Graphics.Texture RendererObject => (SFML.Graphics.Texture)((AtlasRegion)_o.RendererObject).page.rendererObject;
|
||||||
|
|
||||||
|
public float[] UVs => _o.UVs;
|
||||||
|
|
||||||
|
public int[] Triangles => _o.Triangles;
|
||||||
|
|
||||||
|
public int HullLength => _o.HullLength;
|
||||||
|
}
|
||||||
|
}
|
||||||
33
Spine/Implementations/V21/Bone21.cs
Normal file
33
Spine/Implementations/V21/Bone21.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using SpineRuntime21;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V21
|
||||||
|
{
|
||||||
|
internal sealed class Bone21(Bone innerObject, Bone21? parent = null) : IBone
|
||||||
|
{
|
||||||
|
private readonly Bone _o = innerObject;
|
||||||
|
private readonly Bone21? _parent = parent;
|
||||||
|
|
||||||
|
public Bone InnerObject => _o;
|
||||||
|
|
||||||
|
public string Name => _o.Data.Name;
|
||||||
|
public int Index => _o.Data.Index;
|
||||||
|
|
||||||
|
public IBone? Parent => _parent;
|
||||||
|
public bool Active => true; // NOTE: 3.7 及以下没有 Active 属性, 此处总是返回 true
|
||||||
|
public float Length => _o.Data.Length;
|
||||||
|
public float WorldX => _o.WorldX;
|
||||||
|
public float WorldY => _o.WorldY;
|
||||||
|
public float A => _o.M00;
|
||||||
|
public float B => _o.M01;
|
||||||
|
public float C => _o.M10;
|
||||||
|
public float D => _o.M11;
|
||||||
|
|
||||||
|
public override string ToString() => _o.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
101
Spine/Implementations/V21/Skeleton21.cs
Normal file
101
Spine/Implementations/V21/Skeleton21.cs
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Collections.Frozen;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using SpineRuntime21;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V21
|
||||||
|
{
|
||||||
|
internal sealed class Skeleton21 : ISkeleton
|
||||||
|
{
|
||||||
|
private readonly Skeleton _o;
|
||||||
|
private readonly SpineObjectData21 _data;
|
||||||
|
|
||||||
|
private readonly ImmutableArray<IBone> _bones;
|
||||||
|
private readonly FrozenDictionary<string, IBone> _bonesByName;
|
||||||
|
private readonly ImmutableArray<ISlot> _slots;
|
||||||
|
private readonly FrozenDictionary<string, ISlot> _slotsByName;
|
||||||
|
|
||||||
|
private Skin21? _skin;
|
||||||
|
|
||||||
|
public Skeleton21(Skeleton innerObject, SpineObjectData21 data)
|
||||||
|
{
|
||||||
|
_o = innerObject;
|
||||||
|
_data = data;
|
||||||
|
|
||||||
|
List<Bone21> bones = [];
|
||||||
|
Dictionary<string, IBone> bonesByName = [];
|
||||||
|
foreach (var b in _o.Bones)
|
||||||
|
{
|
||||||
|
var bone = new Bone21(b, b.Parent is null ? null : bones[b.Parent.Data.Index]);
|
||||||
|
bones.Add(bone);
|
||||||
|
bonesByName[bone.Name] = bone;
|
||||||
|
}
|
||||||
|
_bones = bones.Cast<IBone>().ToImmutableArray();
|
||||||
|
_bonesByName = bonesByName.ToFrozenDictionary();
|
||||||
|
|
||||||
|
List<Slot21> slots = [];
|
||||||
|
Dictionary<string, ISlot> slotsByName = [];
|
||||||
|
foreach (var s in _o.Slots)
|
||||||
|
{
|
||||||
|
var slot = new Slot21(s, _data, bones[s.Bone.Data.Index]);
|
||||||
|
slots.Add(slot);
|
||||||
|
slotsByName[slot.Name] = slot;
|
||||||
|
}
|
||||||
|
_slots = slots.Cast<ISlot>().ToImmutableArray();
|
||||||
|
_slotsByName = slotsByName.ToFrozenDictionary();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Skeleton InnerObject => _o;
|
||||||
|
|
||||||
|
public string Name => _o.Data.Name;
|
||||||
|
public float R { get => _o.R; set => _o.R = value; }
|
||||||
|
public float G { get => _o.G; set => _o.G = value; }
|
||||||
|
public float B { get => _o.B; set => _o.B = value; }
|
||||||
|
public float A { get => _o.A; set => _o.A = value; }
|
||||||
|
public float X { get => _o.X; set => _o.X = value; }
|
||||||
|
public float Y { get => _o.Y; set => _o.Y = value; }
|
||||||
|
public float ScaleX { get => _o.ScaleX; set => _o.ScaleX = value; }
|
||||||
|
public float ScaleY { get => _o.ScaleY; set => _o.ScaleY = value; }
|
||||||
|
|
||||||
|
public ImmutableArray<IBone> Bones => _bones;
|
||||||
|
public FrozenDictionary<string, IBone> BonesByName => _bonesByName;
|
||||||
|
public ImmutableArray<ISlot> Slots => _slots;
|
||||||
|
public FrozenDictionary<string, ISlot> SlotsByName => _slotsByName;
|
||||||
|
|
||||||
|
public ISkin? Skin
|
||||||
|
{
|
||||||
|
get => _skin;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value is null)
|
||||||
|
{
|
||||||
|
_o.Skin = null;
|
||||||
|
_skin = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (value is Skin21 sk)
|
||||||
|
{
|
||||||
|
_o.Skin = sk.InnerObject;
|
||||||
|
_skin = sk;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Received {value.GetType().Name}", nameof(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<ISlot> IterDrawOrder() => _o.DrawOrder.Select(s => _slots[s.Data.Index]);
|
||||||
|
public void UpdateCache() => _o.UpdateCache();
|
||||||
|
public void UpdateWorldTransform(ISkeleton.Physics physics) => _o.UpdateWorldTransform();
|
||||||
|
public void SetToSetupPose() => _o.SetToSetupPose();
|
||||||
|
public void SetBonesToSetupPose() => _o.SetBonesToSetupPose();
|
||||||
|
public void SetSlotsToSetupPose() => _o.SetSlotsToSetupPose();
|
||||||
|
public void Update(float delta) => _o.Update(delta);
|
||||||
|
|
||||||
|
public override string ToString() => _o.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
42
Spine/Implementations/V21/SkeletonClipping21.cs
Normal file
42
Spine/Implementations/V21/SkeletonClipping21.cs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
using Spine.Interfaces;
|
||||||
|
using Spine.Interfaces.Attachments;
|
||||||
|
using Spine.Utils;
|
||||||
|
using SpineRuntime21;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V21
|
||||||
|
{
|
||||||
|
internal sealed class SkeletonClipping21 : ISkeletonClipping
|
||||||
|
{
|
||||||
|
public bool IsClipping => false;
|
||||||
|
|
||||||
|
public float[] ClippedVertices { get; private set; } = [];
|
||||||
|
|
||||||
|
public int ClippedVerticesLength { get; private set; } = 0;
|
||||||
|
|
||||||
|
public int[] ClippedTriangles { get; private set; } = [];
|
||||||
|
|
||||||
|
public int ClippedTrianglesLength { get; private set; } = 0;
|
||||||
|
|
||||||
|
public float[] ClippedUVs { get; private set; } = [];
|
||||||
|
|
||||||
|
public void ClipEnd(ISlot slot) { }
|
||||||
|
|
||||||
|
public void ClipEnd() { }
|
||||||
|
|
||||||
|
public void ClipStart(ISlot slot, IClippingAttachment clippingAttachment) { }
|
||||||
|
|
||||||
|
public void ClipTriangles(float[] vertices, int verticesLength, int[] triangles, int trianglesLength, float[] uvs)
|
||||||
|
{
|
||||||
|
ClippedVertices = vertices.ToArray();
|
||||||
|
ClippedVerticesLength = verticesLength;
|
||||||
|
ClippedTriangles = triangles.ToArray();
|
||||||
|
ClippedTrianglesLength = trianglesLength;
|
||||||
|
ClippedUVs = uvs.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
45
Spine/Implementations/V21/Skin21.cs
Normal file
45
Spine/Implementations/V21/Skin21.cs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using SpineRuntime21;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V21
|
||||||
|
{
|
||||||
|
internal sealed class Skin21 : ISkin
|
||||||
|
{
|
||||||
|
private readonly Skin _o;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用指定名字创建空皮肤
|
||||||
|
/// </summary>
|
||||||
|
public Skin21(string name) => _o = new(name);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 包装已有皮肤对象
|
||||||
|
/// </summary>
|
||||||
|
public Skin21(Skin innerObject) => _o = innerObject;
|
||||||
|
|
||||||
|
public Skin InnerObject => _o;
|
||||||
|
|
||||||
|
public string Name => _o.Name;
|
||||||
|
|
||||||
|
public void AddSkin(ISkin skin)
|
||||||
|
{
|
||||||
|
if (skin is Skin21 sk)
|
||||||
|
{
|
||||||
|
// NOTE: 3.7 及以下不支持 AddSkin
|
||||||
|
foreach (var (k, v) in sk._o.Attachments)
|
||||||
|
_o.AddAttachment(k.Key, k.Value, v);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Received {skin.GetType().Name}", nameof(skin));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clear() => _o.Attachments.Clear();
|
||||||
|
|
||||||
|
public override string ToString() => _o.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
74
Spine/Implementations/V21/Slot21.cs
Normal file
74
Spine/Implementations/V21/Slot21.cs
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Frozen;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Utils;
|
||||||
|
using SpineRuntime21;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using Spine.Interfaces.Attachments;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V21
|
||||||
|
{
|
||||||
|
internal sealed class Slot21 : ISlot
|
||||||
|
{
|
||||||
|
private readonly Slot _o;
|
||||||
|
private readonly SpineObjectData21 _data;
|
||||||
|
|
||||||
|
private readonly Bone21 _bone;
|
||||||
|
private readonly SFML.Graphics.BlendMode _blendMode;
|
||||||
|
|
||||||
|
public Slot21(Slot innerObject, SpineObjectData21 data, Bone21 bone)
|
||||||
|
{
|
||||||
|
_o = innerObject;
|
||||||
|
_data = data;
|
||||||
|
|
||||||
|
_bone = bone;
|
||||||
|
_blendMode = _o.Data.AdditiveBlending ? SFMLBlendMode.AdditivePma : SFMLBlendMode.NormalPma; // NOTE: 2.1 没有完整的 BlendMode
|
||||||
|
}
|
||||||
|
|
||||||
|
public Slot InnerObject => _o;
|
||||||
|
|
||||||
|
public string Name => _o.Data.Name;
|
||||||
|
public int Index => _o.Data.Index;
|
||||||
|
public SFML.Graphics.BlendMode Blend => _blendMode;
|
||||||
|
|
||||||
|
public float R { get => _o.R; set => _o.R = value; }
|
||||||
|
public float G { get => _o.G; set => _o.G = value; }
|
||||||
|
public float B { get => _o.B; set => _o.B = value; }
|
||||||
|
public float A { get => _o.A; set => _o.A = value; }
|
||||||
|
public IBone Bone => _bone;
|
||||||
|
|
||||||
|
public IAttachment? Attachment
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_o.Attachment is Attachment att)
|
||||||
|
{
|
||||||
|
return _data.SlotAttachments[Name][att.Name];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value is null)
|
||||||
|
{
|
||||||
|
_o.Attachment = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (value is Attachments.Attachment21 att)
|
||||||
|
{
|
||||||
|
_o.Attachment = att.InnerObject;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Received {value.GetType().Name}", nameof(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Disabled { get; set; }
|
||||||
|
|
||||||
|
public override string ToString() => _o.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
146
Spine/Implementations/V21/SpineObjectData21.cs
Normal file
146
Spine/Implementations/V21/SpineObjectData21.cs
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Frozen;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Utils;
|
||||||
|
using SpineRuntime21;
|
||||||
|
using Spine.Implementations.V21.Attachments;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using Spine.Interfaces.Attachments;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V21
|
||||||
|
{
|
||||||
|
[SpineImplementation(2, 1)]
|
||||||
|
internal sealed class SpineObjectData21 : SpineObjectData
|
||||||
|
{
|
||||||
|
private readonly Atlas _atlas;
|
||||||
|
private readonly SkeletonData _skeletonData;
|
||||||
|
private readonly AnimationStateData _animationStateData;
|
||||||
|
|
||||||
|
private readonly ImmutableArray<ISkin> _skins;
|
||||||
|
private readonly FrozenDictionary<string, ISkin> _skinsByName;
|
||||||
|
private readonly FrozenDictionary<string, FrozenDictionary<string, IAttachment>> _slotAttachments;
|
||||||
|
private readonly ImmutableArray<IAnimation> _animations;
|
||||||
|
private readonly FrozenDictionary<string, IAnimation> _animationsByName;
|
||||||
|
|
||||||
|
public SpineObjectData21(string skelPath, string atlasPath, TextureLoader textureLoader)
|
||||||
|
: base(skelPath, atlasPath, textureLoader)
|
||||||
|
{
|
||||||
|
// 加载 atlas
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_atlas = new Atlas(atlasPath, textureLoader);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Trace(ex.ToString());
|
||||||
|
throw new InvalidDataException($"Failed to load atlas '{atlasPath}'");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Utf8Validator.IsUtf8(skelPath))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Trace(ex.ToString());
|
||||||
|
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Trace(ex.ToString());
|
||||||
|
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_atlas.Dispose();
|
||||||
|
_logger.Trace(ex.ToString());
|
||||||
|
throw new InvalidDataException($"Failed to load skeleton file {skelPath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载动画数据
|
||||||
|
_animationStateData = new AnimationStateData(_skeletonData);
|
||||||
|
|
||||||
|
// 整理皮肤和附件
|
||||||
|
Dictionary<string, Dictionary<string, IAttachment>> slotAttachments = [];
|
||||||
|
List<ISkin> skins = [];
|
||||||
|
Dictionary<string, ISkin> skinsByName = [];
|
||||||
|
foreach (var s in _skeletonData.Skins)
|
||||||
|
{
|
||||||
|
var skin = new Skin21(s);
|
||||||
|
skins.Add(skin);
|
||||||
|
skinsByName[s.Name] = skin;
|
||||||
|
foreach (var (k, att) in s.Attachments)
|
||||||
|
{
|
||||||
|
var slotName = _skeletonData.Slots[k.Key].Name;
|
||||||
|
if (!slotAttachments.TryGetValue(slotName, out var attachments))
|
||||||
|
slotAttachments[slotName] = attachments = [];
|
||||||
|
|
||||||
|
attachments[att.Name] = att switch
|
||||||
|
{
|
||||||
|
RegionAttachment regionAtt => new RegionAttachment21(regionAtt),
|
||||||
|
MeshAttachment meshAtt => new MeshAttachment21(meshAtt),
|
||||||
|
SkinnedMeshAttachment skMeshAtt => new SkinnedMeshAttachment21(skMeshAtt),
|
||||||
|
BoundingBoxAttachment bbAtt => new BoundingBoxAttachment21(bbAtt),
|
||||||
|
_ => throw new InvalidOperationException($"Unrecognized attachment type {att.GetType().FullName}")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_slotAttachments = slotAttachments.ToFrozenDictionary(it => it.Key, it => it.Value.ToFrozenDictionary());
|
||||||
|
_skins = skins.ToImmutableArray();
|
||||||
|
_skinsByName = skinsByName.ToFrozenDictionary();
|
||||||
|
|
||||||
|
// 整理所有动画数据
|
||||||
|
List<IAnimation> animations = [];
|
||||||
|
Dictionary<string, IAnimation> animationsByName = [];
|
||||||
|
foreach (var a in _skeletonData.Animations)
|
||||||
|
{
|
||||||
|
var anime = new Animation21(a);
|
||||||
|
animations.Add(anime);
|
||||||
|
animationsByName[anime.Name] = anime;
|
||||||
|
}
|
||||||
|
_animations = animations.ToImmutableArray();
|
||||||
|
_animationsByName = animationsByName.ToFrozenDictionary();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string SkeletonVersion => _skeletonData.Version;
|
||||||
|
|
||||||
|
public override ImmutableArray<ISkin> Skins => _skins;
|
||||||
|
|
||||||
|
public override FrozenDictionary<string, ISkin> SkinsByName => _skinsByName;
|
||||||
|
|
||||||
|
public override FrozenDictionary<string, FrozenDictionary<string, IAttachment>> SlotAttachments => _slotAttachments;
|
||||||
|
|
||||||
|
public override float DefaultMix { get => _animationStateData.DefaultMix; set => _animationStateData.DefaultMix = value; }
|
||||||
|
|
||||||
|
public override ImmutableArray<IAnimation> Animations => _animations;
|
||||||
|
|
||||||
|
public override FrozenDictionary<string, IAnimation> AnimationsByName => _animationsByName;
|
||||||
|
|
||||||
|
protected override void DisposeAtlas() => _atlas.Dispose();
|
||||||
|
|
||||||
|
public override ISkeleton CreateSkeleton() => new Skeleton21(new(_skeletonData), this);
|
||||||
|
|
||||||
|
public override IAnimationState CreateAnimationState() => new AnimationState21(new(_animationStateData), this);
|
||||||
|
|
||||||
|
public override ISkeletonClipping CreateSkeletonClipping() => new SkeletonClipping21();
|
||||||
|
|
||||||
|
public override ISkin CreateSkin(string name) => new Skin21(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
135
Spine/Implementations/V21/TrackEntry21.cs
Normal file
135
Spine/Implementations/V21/TrackEntry21.cs
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
using Spine.Interfaces;
|
||||||
|
using SpineRuntime21;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V21
|
||||||
|
{
|
||||||
|
internal sealed class TrackEntry21(TrackEntry innerObject, AnimationState21 animationState, SpineObjectData21 data): ITrackEntry
|
||||||
|
{
|
||||||
|
private readonly TrackEntry _o = innerObject;
|
||||||
|
private readonly AnimationState21 _animationState = animationState;
|
||||||
|
private readonly SpineObjectData21 _data = data;
|
||||||
|
|
||||||
|
private readonly Dictionary<IAnimationState.TrackEntryDelegate, AnimationState.TrackEntryDelegate> _eventMapping = [];
|
||||||
|
private readonly Dictionary<IAnimationState.TrackEntryDelegate, int> _eventCount = [];
|
||||||
|
|
||||||
|
public TrackEntry InnerObject => _o;
|
||||||
|
|
||||||
|
#pragma warning disable CS0067
|
||||||
|
|
||||||
|
// 2.1 没有这两个事件
|
||||||
|
public event IAnimationState.TrackEntryDelegate? Interrupt;
|
||||||
|
public event IAnimationState.TrackEntryDelegate? Dispose;
|
||||||
|
|
||||||
|
#pragma warning restore CS0067
|
||||||
|
|
||||||
|
public event IAnimationState.TrackEntryDelegate? Start
|
||||||
|
{
|
||||||
|
add
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (!_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_eventMapping[value] = f = (t) => value(_animationState.GetTrackEntry(t));
|
||||||
|
_eventCount[value] = 0;
|
||||||
|
}
|
||||||
|
_o.Start += f;
|
||||||
|
_eventCount[value]++;
|
||||||
|
}
|
||||||
|
remove
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_o.Start -= f;
|
||||||
|
_eventCount[value]--;
|
||||||
|
if (_eventCount[value] <= 0)
|
||||||
|
{
|
||||||
|
_eventMapping.Remove(value);
|
||||||
|
_eventCount.Remove(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public event IAnimationState.TrackEntryDelegate? End
|
||||||
|
{
|
||||||
|
add
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (!_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_eventMapping[value] = f = (t) => value(_animationState.GetTrackEntry(t));
|
||||||
|
_eventCount[value] = 0;
|
||||||
|
}
|
||||||
|
_o.End += f;
|
||||||
|
_eventCount[value]++;
|
||||||
|
}
|
||||||
|
remove
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_o.End -= f;
|
||||||
|
_eventCount[value]--;
|
||||||
|
if (_eventCount[value] <= 0)
|
||||||
|
{
|
||||||
|
_eventMapping.Remove(value);
|
||||||
|
_eventCount.Remove(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public event IAnimationState.TrackEntryDelegate? Complete
|
||||||
|
{
|
||||||
|
add
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (!_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_eventMapping[value] = f = (t) => value(_animationState.GetTrackEntry(t));
|
||||||
|
_eventCount[value] = 0;
|
||||||
|
}
|
||||||
|
_o.Complete += f;
|
||||||
|
_eventCount[value]++;
|
||||||
|
}
|
||||||
|
remove
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_o.Complete -= f;
|
||||||
|
_eventCount[value]--;
|
||||||
|
if (_eventCount[value] <= 0)
|
||||||
|
{
|
||||||
|
_eventMapping.Remove(value);
|
||||||
|
_eventCount.Remove(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int TrackIndex { get => _o.TrackIndex; }
|
||||||
|
|
||||||
|
public IAnimation Animation { get => _data.AnimationsByName[_o.Animation.Name]; }
|
||||||
|
|
||||||
|
public ITrackEntry? Next { get { var t = _o.Next; return t is null ? null : _animationState.GetTrackEntry(t); } }
|
||||||
|
|
||||||
|
public bool Loop { get => _o.Loop; set => _o.Loop = value; }
|
||||||
|
|
||||||
|
public float TrackTime { get => _o.Time; set => _o.Time = value; }
|
||||||
|
|
||||||
|
public float TimeScale { get => _o.TimeScale; set => _o.TimeScale = value; }
|
||||||
|
|
||||||
|
public float Alpha { get => _o.Mix; set => _o.Mix = value; }
|
||||||
|
|
||||||
|
public float MixDuration { get => _o.MixDuration; set => _o.MixDuration = value; }
|
||||||
|
|
||||||
|
public override string ToString() => _o.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
23
Spine/Implementations/V34/Animation34.cs
Normal file
23
Spine/Implementations/V34/Animation34.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
using Spine.Interfaces;
|
||||||
|
using SpineRuntime34;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V34
|
||||||
|
{
|
||||||
|
internal sealed class Animation34(Animation innerObject) : IAnimation
|
||||||
|
{
|
||||||
|
private readonly Animation _o = innerObject;
|
||||||
|
|
||||||
|
public Animation InnerObject => _o;
|
||||||
|
|
||||||
|
public string Name => _o.Name;
|
||||||
|
|
||||||
|
public float Duration => _o.Duration;
|
||||||
|
|
||||||
|
public override string ToString() => _o.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
180
Spine/Implementations/V34/AnimationState34.cs
Normal file
180
Spine/Implementations/V34/AnimationState34.cs
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using SpineRuntime34;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V34
|
||||||
|
{
|
||||||
|
internal sealed class AnimationState34(AnimationState innerObject, SpineObjectData34 data) : IAnimationState
|
||||||
|
{
|
||||||
|
private readonly AnimationState _o = innerObject;
|
||||||
|
private readonly SpineObjectData34 _data = data;
|
||||||
|
|
||||||
|
private readonly Dictionary<TrackEntry, TrackEntry34> _trackEntryPool = [];
|
||||||
|
|
||||||
|
private readonly Dictionary<IAnimationState.TrackEntryDelegate, AnimationState.TrackEntryDelegate> _eventMapping = [];
|
||||||
|
private readonly Dictionary<IAnimationState.TrackEntryDelegate, int> _eventCount = [];
|
||||||
|
|
||||||
|
public AnimationState InnerObject => _o;
|
||||||
|
|
||||||
|
#pragma warning disable CS0067
|
||||||
|
|
||||||
|
// NOTE: 3.4 以下没有这两个事件
|
||||||
|
public event IAnimationState.TrackEntryDelegate? Interrupt;
|
||||||
|
public event IAnimationState.TrackEntryDelegate? Dispose;
|
||||||
|
|
||||||
|
#pragma warning restore CS0067
|
||||||
|
|
||||||
|
|
||||||
|
public event IAnimationState.TrackEntryDelegate? Start
|
||||||
|
{
|
||||||
|
add
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (!_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_eventMapping[value] = f = (t) => value(GetTrackEntry(t));
|
||||||
|
_eventCount[value] = 0;
|
||||||
|
}
|
||||||
|
_o.Start += f;
|
||||||
|
_eventCount[value]++;
|
||||||
|
}
|
||||||
|
remove
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_o.Start -= f;
|
||||||
|
_eventCount[value]--;
|
||||||
|
if (_eventCount[value] <= 0)
|
||||||
|
{
|
||||||
|
_eventMapping.Remove(value);
|
||||||
|
_eventCount.Remove(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public event IAnimationState.TrackEntryDelegate? End
|
||||||
|
{
|
||||||
|
add
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (!_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_eventMapping[value] = f = (t) => value(GetTrackEntry(t));
|
||||||
|
_eventCount[value] = 0;
|
||||||
|
}
|
||||||
|
_o.End += f;
|
||||||
|
_eventCount[value]++;
|
||||||
|
}
|
||||||
|
remove
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_o.End -= f;
|
||||||
|
_eventCount[value]--;
|
||||||
|
if (_eventCount[value] <= 0)
|
||||||
|
{
|
||||||
|
_eventMapping.Remove(value);
|
||||||
|
_eventCount.Remove(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public event IAnimationState.TrackEntryDelegate? Complete
|
||||||
|
{
|
||||||
|
add
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (!_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_eventMapping[value] = f = (t) => value(GetTrackEntry(t));
|
||||||
|
_eventCount[value] = 0;
|
||||||
|
}
|
||||||
|
_o.Complete += f;
|
||||||
|
_eventCount[value]++;
|
||||||
|
}
|
||||||
|
remove
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_o.Complete -= f;
|
||||||
|
_eventCount[value]--;
|
||||||
|
if (_eventCount[value] <= 0)
|
||||||
|
{
|
||||||
|
_eventMapping.Remove(value);
|
||||||
|
_eventCount.Remove(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public float TimeScale { get => _o.TimeScale; set => _o.TimeScale = value; }
|
||||||
|
|
||||||
|
public void Update(float delta) => _o.Update(delta);
|
||||||
|
|
||||||
|
public void Apply(ISkeleton skeleton)
|
||||||
|
{
|
||||||
|
if (skeleton is Skeleton34 skel)
|
||||||
|
{
|
||||||
|
_o.Apply(skel.InnerObject);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Received {skeleton.GetType().Name}", nameof(skeleton));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取 <see cref="ITrackEntry"/> 对象, 不存在则创建
|
||||||
|
/// </summary>
|
||||||
|
public ITrackEntry GetTrackEntry(TrackEntry trackEntry)
|
||||||
|
{
|
||||||
|
if (!_trackEntryPool.TryGetValue(trackEntry, out var tr))
|
||||||
|
_trackEntryPool[trackEntry] = tr = new(trackEntry, this, _data);
|
||||||
|
return tr;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<ITrackEntry?> IterTracks() => _o.Tracks.Select(t => t is null ? null : GetTrackEntry(t));
|
||||||
|
|
||||||
|
public ITrackEntry? GetCurrent(int index) { var t = _o.GetCurrent(index); return t is null ? null : GetTrackEntry(t); }
|
||||||
|
|
||||||
|
public void ClearTrack(int index) => _o.ClearTrack(index);
|
||||||
|
|
||||||
|
public void ClearTracks() => _o.ClearTracks();
|
||||||
|
|
||||||
|
public ITrackEntry SetAnimation(int trackIndex, string animationName, bool loop)
|
||||||
|
=> GetTrackEntry(_o.SetAnimation(trackIndex, animationName, loop));
|
||||||
|
|
||||||
|
public ITrackEntry SetAnimation(int trackIndex, IAnimation animation, bool loop)
|
||||||
|
{
|
||||||
|
if (animation is Animation34 anime)
|
||||||
|
return GetTrackEntry(_o.SetAnimation(trackIndex, anime.InnerObject, loop));
|
||||||
|
throw new ArgumentException($"Received {animation.GetType().Name}", nameof(animation));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ITrackEntry SetEmptyAnimation(int trackIndex, float mixDuration) => GetTrackEntry(_o.SetEmptyAnimation(trackIndex, mixDuration));
|
||||||
|
|
||||||
|
public void SetEmptyAnimations(float mixDuration) => _o.SetEmptyAnimations(mixDuration);
|
||||||
|
|
||||||
|
public ITrackEntry AddAnimation(int trackIndex, string animationName, bool loop, float delay)
|
||||||
|
=> GetTrackEntry(_o.AddAnimation(trackIndex, animationName, loop, delay));
|
||||||
|
|
||||||
|
public ITrackEntry AddAnimation(int trackIndex, IAnimation animation, bool loop, float delay)
|
||||||
|
{
|
||||||
|
if (animation is Animation34 anime)
|
||||||
|
return GetTrackEntry(_o.AddAnimation(trackIndex, anime.InnerObject, loop, delay));
|
||||||
|
throw new ArgumentException($"Received {animation.GetType().Name}", nameof(animation));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ITrackEntry AddEmptyAnimation(int trackIndex, float mixDuration, float delay)
|
||||||
|
=> GetTrackEntry(_o.AddEmptyAnimation(trackIndex, mixDuration, delay));
|
||||||
|
|
||||||
|
public override string ToString() => _o.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
24
Spine/Implementations/V34/Attachments/Attachment34.cs
Normal file
24
Spine/Implementations/V34/Attachments/Attachment34.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using Spine.Interfaces.Attachments;
|
||||||
|
using SpineRuntime34;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V34.Attachments
|
||||||
|
{
|
||||||
|
internal abstract class Attachment34(Attachment innerObject) : IAttachment
|
||||||
|
{
|
||||||
|
private readonly Attachment _o = innerObject;
|
||||||
|
|
||||||
|
public virtual Attachment InnerObject => _o;
|
||||||
|
|
||||||
|
public string Name => _o.Name;
|
||||||
|
|
||||||
|
public abstract int ComputeWorldVertices(ISlot slot, ref float[] worldVertices);
|
||||||
|
|
||||||
|
public override string ToString() => _o.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Implementations.V34;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using Spine.Interfaces.Attachments;
|
||||||
|
using SpineRuntime34;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V34.Attachments
|
||||||
|
{
|
||||||
|
internal sealed class BoundingBoxAttachment34(BoundingBoxAttachment innerObject) :
|
||||||
|
Attachment34(innerObject),
|
||||||
|
IBoundingBoxAttachment
|
||||||
|
{
|
||||||
|
private readonly BoundingBoxAttachment _o = innerObject;
|
||||||
|
|
||||||
|
public override BoundingBoxAttachment InnerObject => _o;
|
||||||
|
|
||||||
|
public override int ComputeWorldVertices(ISlot slot, ref float[] worldVertices)
|
||||||
|
{
|
||||||
|
if (slot is Slot34 st)
|
||||||
|
{
|
||||||
|
var length = _o.WorldVerticesLength;
|
||||||
|
if (worldVertices.Length < length) worldVertices = new float[length];
|
||||||
|
_o.ComputeWorldVertices(st.InnerObject, worldVertices);
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot34)}, but received {slot.GetType().Name}", nameof(slot));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
46
Spine/Implementations/V34/Attachments/MeshAttachment34.cs
Normal file
46
Spine/Implementations/V34/Attachments/MeshAttachment34.cs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Implementations.V34;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using Spine.Interfaces.Attachments;
|
||||||
|
using SpineRuntime34;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V34.Attachments
|
||||||
|
{
|
||||||
|
internal sealed class MeshAttachment34(MeshAttachment innerObject) :
|
||||||
|
Attachment34(innerObject),
|
||||||
|
IMeshAttachment
|
||||||
|
{
|
||||||
|
private readonly MeshAttachment _o = innerObject;
|
||||||
|
|
||||||
|
public override MeshAttachment InnerObject => _o;
|
||||||
|
|
||||||
|
public override int ComputeWorldVertices(ISlot slot, ref float[] worldVertices)
|
||||||
|
{
|
||||||
|
if (slot is Slot34 st)
|
||||||
|
{
|
||||||
|
var length = _o.WorldVerticesLength;
|
||||||
|
if (worldVertices.Length < length) worldVertices = new float[length];
|
||||||
|
_o.ComputeWorldVertices(st.InnerObject, worldVertices);
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot34)}, but received {slot.GetType().Name}", nameof(slot));
|
||||||
|
}
|
||||||
|
|
||||||
|
public float R { get => _o.R; set => _o.R = value; }
|
||||||
|
public float G { get => _o.G; set => _o.G = value; }
|
||||||
|
public float B { get => _o.B; set => _o.B = value; }
|
||||||
|
public float A { get => _o.A; set => _o.A = value; }
|
||||||
|
|
||||||
|
public SFML.Graphics.Texture RendererObject => (SFML.Graphics.Texture)((AtlasRegion)_o.RendererObject).page.rendererObject;
|
||||||
|
|
||||||
|
public float[] UVs => _o.UVs;
|
||||||
|
|
||||||
|
public int[] Triangles => _o.Triangles;
|
||||||
|
|
||||||
|
public int HullLength => _o.HullLength;
|
||||||
|
}
|
||||||
|
}
|
||||||
33
Spine/Implementations/V34/Attachments/PathAttachment34.cs
Normal file
33
Spine/Implementations/V34/Attachments/PathAttachment34.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Implementations.V34;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using Spine.Interfaces.Attachments;
|
||||||
|
using SpineRuntime34;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V34.Attachments
|
||||||
|
{
|
||||||
|
internal sealed class PathAttachment34(PathAttachment innerObject) :
|
||||||
|
Attachment34(innerObject),
|
||||||
|
IPathAttachment
|
||||||
|
{
|
||||||
|
private readonly PathAttachment _o = innerObject;
|
||||||
|
|
||||||
|
public override PathAttachment InnerObject => _o;
|
||||||
|
|
||||||
|
public override int ComputeWorldVertices(ISlot slot, ref float[] worldVertices)
|
||||||
|
{
|
||||||
|
if (slot is Slot34 st)
|
||||||
|
{
|
||||||
|
var length = _o.WorldVerticesLength;
|
||||||
|
if (worldVertices.Length < length) worldVertices = new float[length];
|
||||||
|
_o.ComputeWorldVertices(st.InnerObject, worldVertices);
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot34)}, but received {slot.GetType().Name}", nameof(slot));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
41
Spine/Implementations/V34/Attachments/RegionAttachment34.cs
Normal file
41
Spine/Implementations/V34/Attachments/RegionAttachment34.cs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Implementations.V34;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using Spine.Interfaces.Attachments;
|
||||||
|
using SpineRuntime34;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V34.Attachments
|
||||||
|
{
|
||||||
|
internal sealed class RegionAttachment34(RegionAttachment innerObject) :
|
||||||
|
Attachment34(innerObject),
|
||||||
|
IRegionAttachment
|
||||||
|
{
|
||||||
|
private readonly RegionAttachment _o = innerObject;
|
||||||
|
|
||||||
|
public override RegionAttachment InnerObject => _o;
|
||||||
|
|
||||||
|
public override int ComputeWorldVertices(ISlot slot, ref float[] worldVertices)
|
||||||
|
{
|
||||||
|
if (slot is Slot34 st)
|
||||||
|
{
|
||||||
|
if (worldVertices.Length < 8) worldVertices = new float[8];
|
||||||
|
_o.ComputeWorldVertices(st.InnerObject.Bone, worldVertices);
|
||||||
|
return 8;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot34)}, but received {slot.GetType().Name}", nameof(slot));
|
||||||
|
}
|
||||||
|
|
||||||
|
public float R { get => _o.R; set => _o.R = value; }
|
||||||
|
public float G { get => _o.G; set => _o.G = value; }
|
||||||
|
public float B { get => _o.B; set => _o.B = value; }
|
||||||
|
public float A { get => _o.A; set => _o.A = value; }
|
||||||
|
|
||||||
|
public SFML.Graphics.Texture RendererObject => (SFML.Graphics.Texture)((AtlasRegion)_o.RendererObject).page.rendererObject;
|
||||||
|
|
||||||
|
public float[] UVs => _o.UVs;
|
||||||
|
}
|
||||||
|
}
|
||||||
33
Spine/Implementations/V34/Bone34.cs
Normal file
33
Spine/Implementations/V34/Bone34.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using SpineRuntime34;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V34
|
||||||
|
{
|
||||||
|
internal sealed class Bone34(Bone innerObject, Bone34? parent = null) : IBone
|
||||||
|
{
|
||||||
|
private readonly Bone _o = innerObject;
|
||||||
|
private readonly Bone34? _parent = parent;
|
||||||
|
|
||||||
|
public Bone InnerObject => _o;
|
||||||
|
|
||||||
|
public string Name => _o.Data.Name;
|
||||||
|
public int Index => _o.Data.Index;
|
||||||
|
|
||||||
|
public IBone? Parent => _parent;
|
||||||
|
public bool Active => true; // NOTE: 3.7 及以下没有 Active 属性, 此处总是返回 true
|
||||||
|
public float Length => _o.Data.Length;
|
||||||
|
public float WorldX => _o.WorldX;
|
||||||
|
public float WorldY => _o.WorldY;
|
||||||
|
public float A => _o.A;
|
||||||
|
public float B => _o.B;
|
||||||
|
public float C => _o.C;
|
||||||
|
public float D => _o.D;
|
||||||
|
|
||||||
|
public override string ToString() => _o.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
101
Spine/Implementations/V34/Skeleton34.cs
Normal file
101
Spine/Implementations/V34/Skeleton34.cs
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Collections.Frozen;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using SpineRuntime34;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V34
|
||||||
|
{
|
||||||
|
internal sealed class Skeleton34 : ISkeleton
|
||||||
|
{
|
||||||
|
private readonly Skeleton _o;
|
||||||
|
private readonly SpineObjectData34 _data;
|
||||||
|
|
||||||
|
private readonly ImmutableArray<IBone> _bones;
|
||||||
|
private readonly FrozenDictionary<string, IBone> _bonesByName;
|
||||||
|
private readonly ImmutableArray<ISlot> _slots;
|
||||||
|
private readonly FrozenDictionary<string, ISlot> _slotsByName;
|
||||||
|
|
||||||
|
private Skin34? _skin;
|
||||||
|
|
||||||
|
public Skeleton34(Skeleton innerObject, SpineObjectData34 data)
|
||||||
|
{
|
||||||
|
_o = innerObject;
|
||||||
|
_data = data;
|
||||||
|
|
||||||
|
List<Bone34> bones = [];
|
||||||
|
Dictionary<string, IBone> bonesByName = [];
|
||||||
|
foreach (var b in _o.Bones)
|
||||||
|
{
|
||||||
|
var bone = new Bone34(b, b.Parent is null ? null : bones[b.Parent.Data.Index]);
|
||||||
|
bones.Add(bone);
|
||||||
|
bonesByName[bone.Name] = bone;
|
||||||
|
}
|
||||||
|
_bones = bones.Cast<IBone>().ToImmutableArray();
|
||||||
|
_bonesByName = bonesByName.ToFrozenDictionary();
|
||||||
|
|
||||||
|
List<Slot34> slots = [];
|
||||||
|
Dictionary<string, ISlot> slotsByName = [];
|
||||||
|
foreach (var s in _o.Slots)
|
||||||
|
{
|
||||||
|
var slot = new Slot34(s, _data, bones[s.Bone.Data.Index]);
|
||||||
|
slots.Add(slot);
|
||||||
|
slotsByName[slot.Name] = slot;
|
||||||
|
}
|
||||||
|
_slots = slots.Cast<ISlot>().ToImmutableArray();
|
||||||
|
_slotsByName = slotsByName.ToFrozenDictionary();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Skeleton InnerObject => _o;
|
||||||
|
|
||||||
|
public string Name => _o.Data.Name;
|
||||||
|
public float R { get => _o.R; set => _o.R = value; }
|
||||||
|
public float G { get => _o.G; set => _o.G = value; }
|
||||||
|
public float B { get => _o.B; set => _o.B = value; }
|
||||||
|
public float A { get => _o.A; set => _o.A = value; }
|
||||||
|
public float X { get => _o.X; set => _o.X = value; }
|
||||||
|
public float Y { get => _o.Y; set => _o.Y = value; }
|
||||||
|
public float ScaleX { get => _o.ScaleX; set => _o.ScaleX = value; }
|
||||||
|
public float ScaleY { get => _o.ScaleY; set => _o.ScaleY = value; }
|
||||||
|
|
||||||
|
public ImmutableArray<IBone> Bones => _bones;
|
||||||
|
public FrozenDictionary<string, IBone> BonesByName => _bonesByName;
|
||||||
|
public ImmutableArray<ISlot> Slots => _slots;
|
||||||
|
public FrozenDictionary<string, ISlot> SlotsByName => _slotsByName;
|
||||||
|
|
||||||
|
public ISkin? Skin
|
||||||
|
{
|
||||||
|
get => _skin;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value is null)
|
||||||
|
{
|
||||||
|
_o.Skin = null;
|
||||||
|
_skin = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (value is Skin34 sk)
|
||||||
|
{
|
||||||
|
_o.Skin = sk.InnerObject;
|
||||||
|
_skin = sk;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Received {value.GetType().Name}", nameof(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<ISlot> IterDrawOrder() => _o.DrawOrder.Select(s => _slots[s.Data.Index]);
|
||||||
|
public void UpdateCache() => _o.UpdateCache();
|
||||||
|
public void UpdateWorldTransform(ISkeleton.Physics physics) => _o.UpdateWorldTransform();
|
||||||
|
public void SetToSetupPose() => _o.SetToSetupPose();
|
||||||
|
public void SetBonesToSetupPose() => _o.SetBonesToSetupPose();
|
||||||
|
public void SetSlotsToSetupPose() => _o.SetSlotsToSetupPose();
|
||||||
|
public void Update(float delta) => _o.Update(delta);
|
||||||
|
|
||||||
|
public override string ToString() => _o.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
42
Spine/Implementations/V34/SkeletonClipping34.cs
Normal file
42
Spine/Implementations/V34/SkeletonClipping34.cs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
using Spine.Interfaces;
|
||||||
|
using Spine.Interfaces.Attachments;
|
||||||
|
using Spine.Utils;
|
||||||
|
using SpineRuntime34;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V34
|
||||||
|
{
|
||||||
|
internal sealed class SkeletonClipping34 : ISkeletonClipping
|
||||||
|
{
|
||||||
|
public bool IsClipping => false;
|
||||||
|
|
||||||
|
public float[] ClippedVertices { get; private set; } = [];
|
||||||
|
|
||||||
|
public int ClippedVerticesLength { get; private set; } = 0;
|
||||||
|
|
||||||
|
public int[] ClippedTriangles { get; private set; } = [];
|
||||||
|
|
||||||
|
public int ClippedTrianglesLength { get; private set; } = 0;
|
||||||
|
|
||||||
|
public float[] ClippedUVs { get; private set; } = [];
|
||||||
|
|
||||||
|
public void ClipEnd(ISlot slot) { }
|
||||||
|
|
||||||
|
public void ClipEnd() { }
|
||||||
|
|
||||||
|
public void ClipStart(ISlot slot, IClippingAttachment clippingAttachment) { }
|
||||||
|
|
||||||
|
public void ClipTriangles(float[] vertices, int verticesLength, int[] triangles, int trianglesLength, float[] uvs)
|
||||||
|
{
|
||||||
|
ClippedVertices = vertices.ToArray();
|
||||||
|
ClippedVerticesLength = verticesLength;
|
||||||
|
ClippedTriangles = triangles.ToArray();
|
||||||
|
ClippedTrianglesLength = trianglesLength;
|
||||||
|
ClippedUVs = uvs.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
45
Spine/Implementations/V34/Skin34.cs
Normal file
45
Spine/Implementations/V34/Skin34.cs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using SpineRuntime34;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V34
|
||||||
|
{
|
||||||
|
internal sealed class Skin34 : ISkin
|
||||||
|
{
|
||||||
|
private readonly Skin _o;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用指定名字创建空皮肤
|
||||||
|
/// </summary>
|
||||||
|
public Skin34(string name) => _o = new(name);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 包装已有皮肤对象
|
||||||
|
/// </summary>
|
||||||
|
public Skin34(Skin innerObject) => _o = innerObject;
|
||||||
|
|
||||||
|
public Skin InnerObject => _o;
|
||||||
|
|
||||||
|
public string Name => _o.Name;
|
||||||
|
|
||||||
|
public void AddSkin(ISkin skin)
|
||||||
|
{
|
||||||
|
if (skin is Skin34 sk)
|
||||||
|
{
|
||||||
|
// NOTE: 3.7 及以下不支持 AddSkin
|
||||||
|
foreach (var (k, v) in sk._o.Attachments)
|
||||||
|
_o.AddAttachment(k.slotIndex, k.name, v);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Received {skin.GetType().Name}", nameof(skin));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clear() => _o.Attachments.Clear();
|
||||||
|
|
||||||
|
public override string ToString() => _o.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
81
Spine/Implementations/V34/Slot34.cs
Normal file
81
Spine/Implementations/V34/Slot34.cs
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Frozen;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Utils;
|
||||||
|
using SpineRuntime34;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using Spine.Interfaces.Attachments;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V34
|
||||||
|
{
|
||||||
|
internal sealed class Slot34 : ISlot
|
||||||
|
{
|
||||||
|
private readonly Slot _o;
|
||||||
|
private readonly SpineObjectData34 _data;
|
||||||
|
|
||||||
|
private readonly Bone34 _bone;
|
||||||
|
private readonly SFML.Graphics.BlendMode _blendMode;
|
||||||
|
|
||||||
|
public Slot34(Slot innerObject, SpineObjectData34 data, Bone34 bone)
|
||||||
|
{
|
||||||
|
_o = innerObject;
|
||||||
|
_data = data;
|
||||||
|
|
||||||
|
_bone = bone;
|
||||||
|
_blendMode = _o.Data.BlendMode switch
|
||||||
|
{
|
||||||
|
BlendMode.normal => SFMLBlendMode.NormalPma,
|
||||||
|
BlendMode.additive => SFMLBlendMode.AdditivePma,
|
||||||
|
BlendMode.multiply => SFMLBlendMode.MultiplyPma,
|
||||||
|
BlendMode.screen => SFMLBlendMode.ScreenPma,
|
||||||
|
_ => throw new NotImplementedException($"{_o.Data.BlendMode}"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public Slot InnerObject => _o;
|
||||||
|
|
||||||
|
public string Name => _o.Data.Name;
|
||||||
|
public int Index => _o.Data.Index;
|
||||||
|
public SFML.Graphics.BlendMode Blend => _blendMode;
|
||||||
|
|
||||||
|
public float R { get => _o.R; set => _o.R = value; }
|
||||||
|
public float G { get => _o.G; set => _o.G = value; }
|
||||||
|
public float B { get => _o.B; set => _o.B = value; }
|
||||||
|
public float A { get => _o.A; set => _o.A = value; }
|
||||||
|
public IBone Bone => _bone;
|
||||||
|
|
||||||
|
public IAttachment? Attachment
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_o.Attachment is Attachment att)
|
||||||
|
{
|
||||||
|
return _data.SlotAttachments[Name][att.Name];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value is null)
|
||||||
|
{
|
||||||
|
_o.Attachment = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (value is Attachments.Attachment34 att)
|
||||||
|
{
|
||||||
|
_o.Attachment = att.InnerObject;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Received {value.GetType().Name}", nameof(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Disabled { get; set; }
|
||||||
|
|
||||||
|
public override string ToString() => _o.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
146
Spine/Implementations/V34/SpineObjectData34.cs
Normal file
146
Spine/Implementations/V34/SpineObjectData34.cs
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Frozen;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Utils;
|
||||||
|
using SpineRuntime34;
|
||||||
|
using Spine.Implementations.V34.Attachments;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using Spine.Interfaces.Attachments;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V34
|
||||||
|
{
|
||||||
|
[SpineImplementation(3, 4)]
|
||||||
|
internal sealed class SpineObjectData34 : SpineObjectData
|
||||||
|
{
|
||||||
|
private readonly Atlas _atlas;
|
||||||
|
private readonly SkeletonData _skeletonData;
|
||||||
|
private readonly AnimationStateData _animationStateData;
|
||||||
|
|
||||||
|
private readonly ImmutableArray<ISkin> _skins;
|
||||||
|
private readonly FrozenDictionary<string, ISkin> _skinsByName;
|
||||||
|
private readonly FrozenDictionary<string, FrozenDictionary<string, IAttachment>> _slotAttachments;
|
||||||
|
private readonly ImmutableArray<IAnimation> _animations;
|
||||||
|
private readonly FrozenDictionary<string, IAnimation> _animationsByName;
|
||||||
|
|
||||||
|
public SpineObjectData34(string skelPath, string atlasPath, TextureLoader textureLoader)
|
||||||
|
: base(skelPath, atlasPath, textureLoader)
|
||||||
|
{
|
||||||
|
// 加载 atlas
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_atlas = new Atlas(atlasPath, textureLoader);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Trace(ex.ToString());
|
||||||
|
throw new InvalidDataException($"Failed to load atlas '{atlasPath}'");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Utf8Validator.IsUtf8(skelPath))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Trace(ex.ToString());
|
||||||
|
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Trace(ex.ToString());
|
||||||
|
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_atlas.Dispose();
|
||||||
|
_logger.Trace(ex.ToString());
|
||||||
|
throw new InvalidDataException($"Failed to load skeleton file {skelPath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载动画数据
|
||||||
|
_animationStateData = new AnimationStateData(_skeletonData);
|
||||||
|
|
||||||
|
// 整理皮肤和附件
|
||||||
|
Dictionary<string, Dictionary<string, IAttachment>> slotAttachments = [];
|
||||||
|
List<ISkin> skins = [];
|
||||||
|
Dictionary<string, ISkin> skinsByName = [];
|
||||||
|
foreach (var s in _skeletonData.Skins)
|
||||||
|
{
|
||||||
|
var skin = new Skin34(s);
|
||||||
|
skins.Add(skin);
|
||||||
|
skinsByName[s.Name] = skin;
|
||||||
|
foreach (var (k, att) in s.Attachments)
|
||||||
|
{
|
||||||
|
var slotName = _skeletonData.Slots.Items[k.slotIndex].Name;
|
||||||
|
if (!slotAttachments.TryGetValue(slotName, out var attachments))
|
||||||
|
slotAttachments[slotName] = attachments = [];
|
||||||
|
|
||||||
|
attachments[att.Name] = att switch
|
||||||
|
{
|
||||||
|
RegionAttachment regionAtt => new RegionAttachment34(regionAtt),
|
||||||
|
MeshAttachment meshAtt => new MeshAttachment34(meshAtt),
|
||||||
|
BoundingBoxAttachment bbAtt => new BoundingBoxAttachment34(bbAtt),
|
||||||
|
PathAttachment pathAtt => new PathAttachment34(pathAtt),
|
||||||
|
_ => throw new InvalidOperationException($"Unrecognized attachment type {att.GetType().FullName}")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_slotAttachments = slotAttachments.ToFrozenDictionary(it => it.Key, it => it.Value.ToFrozenDictionary());
|
||||||
|
_skins = skins.ToImmutableArray();
|
||||||
|
_skinsByName = skinsByName.ToFrozenDictionary();
|
||||||
|
|
||||||
|
// 整理所有动画数据
|
||||||
|
List<IAnimation> animations = [];
|
||||||
|
Dictionary<string, IAnimation> animationsByName = [];
|
||||||
|
foreach (var a in _skeletonData.Animations)
|
||||||
|
{
|
||||||
|
var anime = new Animation34(a);
|
||||||
|
animations.Add(anime);
|
||||||
|
animationsByName[anime.Name] = anime;
|
||||||
|
}
|
||||||
|
_animations = animations.ToImmutableArray();
|
||||||
|
_animationsByName = animationsByName.ToFrozenDictionary();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string SkeletonVersion => _skeletonData.Version;
|
||||||
|
|
||||||
|
public override ImmutableArray<ISkin> Skins => _skins;
|
||||||
|
|
||||||
|
public override FrozenDictionary<string, ISkin> SkinsByName => _skinsByName;
|
||||||
|
|
||||||
|
public override FrozenDictionary<string, FrozenDictionary<string, IAttachment>> SlotAttachments => _slotAttachments;
|
||||||
|
|
||||||
|
public override float DefaultMix { get => _animationStateData.DefaultMix; set => _animationStateData.DefaultMix = value; }
|
||||||
|
|
||||||
|
public override ImmutableArray<IAnimation> Animations => _animations;
|
||||||
|
|
||||||
|
public override FrozenDictionary<string, IAnimation> AnimationsByName => _animationsByName;
|
||||||
|
|
||||||
|
protected override void DisposeAtlas() => _atlas.Dispose();
|
||||||
|
|
||||||
|
public override ISkeleton CreateSkeleton() => new Skeleton34(new(_skeletonData), this);
|
||||||
|
|
||||||
|
public override IAnimationState CreateAnimationState() => new AnimationState34(new(_animationStateData), this);
|
||||||
|
|
||||||
|
public override ISkeletonClipping CreateSkeletonClipping() => new SkeletonClipping34();
|
||||||
|
|
||||||
|
public override ISkin CreateSkin(string name) => new Skin34(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
135
Spine/Implementations/V34/TrackEntry34.cs
Normal file
135
Spine/Implementations/V34/TrackEntry34.cs
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
using Spine.Interfaces;
|
||||||
|
using SpineRuntime34;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V34
|
||||||
|
{
|
||||||
|
internal sealed class TrackEntry34(TrackEntry innerObject, AnimationState34 animationState, SpineObjectData34 data): ITrackEntry
|
||||||
|
{
|
||||||
|
private readonly TrackEntry _o = innerObject;
|
||||||
|
private readonly AnimationState34 _animationState = animationState;
|
||||||
|
private readonly SpineObjectData34 _data = data;
|
||||||
|
|
||||||
|
private readonly Dictionary<IAnimationState.TrackEntryDelegate, AnimationState.TrackEntryDelegate> _eventMapping = [];
|
||||||
|
private readonly Dictionary<IAnimationState.TrackEntryDelegate, int> _eventCount = [];
|
||||||
|
|
||||||
|
public TrackEntry InnerObject => _o;
|
||||||
|
|
||||||
|
#pragma warning disable CS0067
|
||||||
|
|
||||||
|
// 3.4 及以下没有这两个事件
|
||||||
|
public event IAnimationState.TrackEntryDelegate? Interrupt;
|
||||||
|
public event IAnimationState.TrackEntryDelegate? Dispose;
|
||||||
|
|
||||||
|
#pragma warning restore CS0067
|
||||||
|
|
||||||
|
public event IAnimationState.TrackEntryDelegate? Start
|
||||||
|
{
|
||||||
|
add
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (!_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_eventMapping[value] = f = (t) => value(_animationState.GetTrackEntry(t));
|
||||||
|
_eventCount[value] = 0;
|
||||||
|
}
|
||||||
|
_o.Start += f;
|
||||||
|
_eventCount[value]++;
|
||||||
|
}
|
||||||
|
remove
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_o.Start -= f;
|
||||||
|
_eventCount[value]--;
|
||||||
|
if (_eventCount[value] <= 0)
|
||||||
|
{
|
||||||
|
_eventMapping.Remove(value);
|
||||||
|
_eventCount.Remove(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public event IAnimationState.TrackEntryDelegate? End
|
||||||
|
{
|
||||||
|
add
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (!_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_eventMapping[value] = f = (t) => value(_animationState.GetTrackEntry(t));
|
||||||
|
_eventCount[value] = 0;
|
||||||
|
}
|
||||||
|
_o.End += f;
|
||||||
|
_eventCount[value]++;
|
||||||
|
}
|
||||||
|
remove
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_o.End -= f;
|
||||||
|
_eventCount[value]--;
|
||||||
|
if (_eventCount[value] <= 0)
|
||||||
|
{
|
||||||
|
_eventMapping.Remove(value);
|
||||||
|
_eventCount.Remove(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public event IAnimationState.TrackEntryDelegate? Complete
|
||||||
|
{
|
||||||
|
add
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (!_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_eventMapping[value] = f = (t) => value(_animationState.GetTrackEntry(t));
|
||||||
|
_eventCount[value] = 0;
|
||||||
|
}
|
||||||
|
_o.Complete += f;
|
||||||
|
_eventCount[value]++;
|
||||||
|
}
|
||||||
|
remove
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_o.Complete -= f;
|
||||||
|
_eventCount[value]--;
|
||||||
|
if (_eventCount[value] <= 0)
|
||||||
|
{
|
||||||
|
_eventMapping.Remove(value);
|
||||||
|
_eventCount.Remove(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int TrackIndex { get => _o.TrackIndex; }
|
||||||
|
|
||||||
|
public IAnimation Animation { get => _data.AnimationsByName[_o.Animation.Name]; }
|
||||||
|
|
||||||
|
public ITrackEntry? Next { get { var t = _o.Next; return t is null ? null : _animationState.GetTrackEntry(t); } }
|
||||||
|
|
||||||
|
public bool Loop { get => _o.Loop; set => _o.Loop = value; }
|
||||||
|
|
||||||
|
public float TrackTime { get => _o.Time; set => _o.Time = value; }
|
||||||
|
|
||||||
|
public float TimeScale { get => _o.TimeScale; set => _o.TimeScale = value; }
|
||||||
|
|
||||||
|
public float Alpha { get => _o.Mix; set => _o.Mix = value; }
|
||||||
|
|
||||||
|
public float MixDuration { get => _o.MixDuration; set => _o.MixDuration = value; }
|
||||||
|
|
||||||
|
public override string ToString() => _o.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
23
Spine/Implementations/V35/Animation35.cs
Normal file
23
Spine/Implementations/V35/Animation35.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
using Spine.Interfaces;
|
||||||
|
using SpineRuntime35;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V35
|
||||||
|
{
|
||||||
|
internal sealed class Animation35(Animation innerObject) : IAnimation
|
||||||
|
{
|
||||||
|
private readonly Animation _o = innerObject;
|
||||||
|
|
||||||
|
public Animation InnerObject => _o;
|
||||||
|
|
||||||
|
public string Name => _o.Name;
|
||||||
|
|
||||||
|
public float Duration => _o.Duration;
|
||||||
|
|
||||||
|
public override string ToString() => _o.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
229
Spine/Implementations/V35/AnimationState35.cs
Normal file
229
Spine/Implementations/V35/AnimationState35.cs
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using SpineRuntime35;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V35
|
||||||
|
{
|
||||||
|
internal sealed class AnimationState35(AnimationState innerObject, SpineObjectData35 data) : IAnimationState
|
||||||
|
{
|
||||||
|
private readonly AnimationState _o = innerObject;
|
||||||
|
private readonly SpineObjectData35 _data = data;
|
||||||
|
|
||||||
|
private readonly Dictionary<TrackEntry, TrackEntry35> _trackEntryPool = [];
|
||||||
|
|
||||||
|
private readonly Dictionary<IAnimationState.TrackEntryDelegate, AnimationState.TrackEntryDelegate> _eventMapping = [];
|
||||||
|
private readonly Dictionary<IAnimationState.TrackEntryDelegate, int> _eventCount = [];
|
||||||
|
|
||||||
|
public AnimationState InnerObject => _o;
|
||||||
|
|
||||||
|
public event IAnimationState.TrackEntryDelegate? Start
|
||||||
|
{
|
||||||
|
add
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (!_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_eventMapping[value] = f = (t) => value(GetTrackEntry(t));
|
||||||
|
_eventCount[value] = 0;
|
||||||
|
}
|
||||||
|
_o.Start += f;
|
||||||
|
_eventCount[value]++;
|
||||||
|
}
|
||||||
|
remove
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_o.Start -= f;
|
||||||
|
_eventCount[value]--;
|
||||||
|
if (_eventCount[value] <= 0)
|
||||||
|
{
|
||||||
|
_eventMapping.Remove(value);
|
||||||
|
_eventCount.Remove(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public event IAnimationState.TrackEntryDelegate? Interrupt
|
||||||
|
{
|
||||||
|
add
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (!_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_eventMapping[value] = f = (t) => value(GetTrackEntry(t));
|
||||||
|
_eventCount[value] = 0;
|
||||||
|
}
|
||||||
|
_o.Interrupt += f;
|
||||||
|
_eventCount[value]++;
|
||||||
|
}
|
||||||
|
remove
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_o.Interrupt -= f;
|
||||||
|
_eventCount[value]--;
|
||||||
|
if (_eventCount[value] <= 0)
|
||||||
|
{
|
||||||
|
_eventMapping.Remove(value);
|
||||||
|
_eventCount.Remove(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public event IAnimationState.TrackEntryDelegate? End
|
||||||
|
{
|
||||||
|
add
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (!_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_eventMapping[value] = f = (t) => value(GetTrackEntry(t));
|
||||||
|
_eventCount[value] = 0;
|
||||||
|
}
|
||||||
|
_o.End += f;
|
||||||
|
_eventCount[value]++;
|
||||||
|
}
|
||||||
|
remove
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_o.End -= f;
|
||||||
|
_eventCount[value]--;
|
||||||
|
if (_eventCount[value] <= 0)
|
||||||
|
{
|
||||||
|
_eventMapping.Remove(value);
|
||||||
|
_eventCount.Remove(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public event IAnimationState.TrackEntryDelegate? Complete
|
||||||
|
{
|
||||||
|
add
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (!_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_eventMapping[value] = f = (t) => value(GetTrackEntry(t));
|
||||||
|
_eventCount[value] = 0;
|
||||||
|
}
|
||||||
|
_o.Complete += f;
|
||||||
|
_eventCount[value]++;
|
||||||
|
}
|
||||||
|
remove
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_o.Complete -= f;
|
||||||
|
_eventCount[value]--;
|
||||||
|
if (_eventCount[value] <= 0)
|
||||||
|
{
|
||||||
|
_eventMapping.Remove(value);
|
||||||
|
_eventCount.Remove(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public event IAnimationState.TrackEntryDelegate? Dispose
|
||||||
|
{
|
||||||
|
add
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (!_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_eventMapping[value] = f = (t) => value(GetTrackEntry(t));
|
||||||
|
_eventCount[value] = 0;
|
||||||
|
}
|
||||||
|
_o.Dispose += f;
|
||||||
|
_eventCount[value]++;
|
||||||
|
}
|
||||||
|
remove
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_o.Dispose -= f;
|
||||||
|
_eventCount[value]--;
|
||||||
|
if (_eventCount[value] <= 0)
|
||||||
|
{
|
||||||
|
_eventMapping.Remove(value);
|
||||||
|
_eventCount.Remove(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public float TimeScale { get => _o.TimeScale; set => _o.TimeScale = value; }
|
||||||
|
|
||||||
|
public void Update(float delta) => _o.Update(delta);
|
||||||
|
|
||||||
|
public void Apply(ISkeleton skeleton)
|
||||||
|
{
|
||||||
|
if (skeleton is Skeleton35 skel)
|
||||||
|
{
|
||||||
|
_o.Apply(skel.InnerObject);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Received {skeleton.GetType().Name}", nameof(skeleton));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取 <see cref="ITrackEntry"/> 对象, 不存在则创建
|
||||||
|
/// </summary>
|
||||||
|
public ITrackEntry GetTrackEntry(TrackEntry trackEntry)
|
||||||
|
{
|
||||||
|
if (!_trackEntryPool.TryGetValue(trackEntry, out var tr))
|
||||||
|
_trackEntryPool[trackEntry] = tr = new(trackEntry, this, _data);
|
||||||
|
return tr;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<ITrackEntry?> IterTracks() => _o.Tracks.Select(t => t is null ? null : GetTrackEntry(t));
|
||||||
|
|
||||||
|
public ITrackEntry? GetCurrent(int index) { var t = _o.GetCurrent(index); return t is null ? null : GetTrackEntry(t); }
|
||||||
|
|
||||||
|
public void ClearTrack(int index) => _o.ClearTrack(index);
|
||||||
|
|
||||||
|
public void ClearTracks() => _o.ClearTracks();
|
||||||
|
|
||||||
|
public ITrackEntry SetAnimation(int trackIndex, string animationName, bool loop)
|
||||||
|
=> GetTrackEntry(_o.SetAnimation(trackIndex, animationName, loop));
|
||||||
|
|
||||||
|
public ITrackEntry SetAnimation(int trackIndex, IAnimation animation, bool loop)
|
||||||
|
{
|
||||||
|
if (animation is Animation35 anime)
|
||||||
|
return GetTrackEntry(_o.SetAnimation(trackIndex, anime.InnerObject, loop));
|
||||||
|
throw new ArgumentException($"Received {animation.GetType().Name}", nameof(animation));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ITrackEntry SetEmptyAnimation(int trackIndex, float mixDuration) => GetTrackEntry(_o.SetEmptyAnimation(trackIndex, mixDuration));
|
||||||
|
|
||||||
|
public void SetEmptyAnimations(float mixDuration) => _o.SetEmptyAnimations(mixDuration);
|
||||||
|
|
||||||
|
public ITrackEntry AddAnimation(int trackIndex, string animationName, bool loop, float delay)
|
||||||
|
=> GetTrackEntry(_o.AddAnimation(trackIndex, animationName, loop, delay));
|
||||||
|
|
||||||
|
public ITrackEntry AddAnimation(int trackIndex, IAnimation animation, bool loop, float delay)
|
||||||
|
{
|
||||||
|
if (animation is Animation35 anime)
|
||||||
|
return GetTrackEntry(_o.AddAnimation(trackIndex, anime.InnerObject, loop, delay));
|
||||||
|
throw new ArgumentException($"Received {animation.GetType().Name}", nameof(animation));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ITrackEntry AddEmptyAnimation(int trackIndex, float mixDuration, float delay)
|
||||||
|
=> GetTrackEntry(_o.AddEmptyAnimation(trackIndex, mixDuration, delay));
|
||||||
|
|
||||||
|
public override string ToString() => _o.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
24
Spine/Implementations/V35/Attachments/Attachment35.cs
Normal file
24
Spine/Implementations/V35/Attachments/Attachment35.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using Spine.Interfaces.Attachments;
|
||||||
|
using SpineRuntime35;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V35.Attachments
|
||||||
|
{
|
||||||
|
internal abstract class Attachment35(Attachment innerObject) : IAttachment
|
||||||
|
{
|
||||||
|
private readonly Attachment _o = innerObject;
|
||||||
|
|
||||||
|
public virtual Attachment InnerObject => _o;
|
||||||
|
|
||||||
|
public string Name => _o.Name;
|
||||||
|
|
||||||
|
public abstract int ComputeWorldVertices(ISlot slot, ref float[] worldVertices);
|
||||||
|
|
||||||
|
public override string ToString() => _o.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Implementations.V35;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using Spine.Interfaces.Attachments;
|
||||||
|
using SpineRuntime35;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V35.Attachments
|
||||||
|
{
|
||||||
|
internal sealed class BoundingBoxAttachment35(BoundingBoxAttachment innerObject) :
|
||||||
|
Attachment35(innerObject),
|
||||||
|
IBoundingBoxAttachment
|
||||||
|
{
|
||||||
|
private readonly BoundingBoxAttachment _o = innerObject;
|
||||||
|
|
||||||
|
public override BoundingBoxAttachment InnerObject => _o;
|
||||||
|
|
||||||
|
public override int ComputeWorldVertices(ISlot slot, ref float[] worldVertices)
|
||||||
|
{
|
||||||
|
if (slot is Slot35 st)
|
||||||
|
{
|
||||||
|
var length = _o.WorldVerticesLength;
|
||||||
|
if (worldVertices.Length < length) worldVertices = new float[length];
|
||||||
|
_o.ComputeWorldVertices(st.InnerObject, worldVertices);
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot35)}, but received {slot.GetType().Name}", nameof(slot));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Implementations.V35;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using Spine.Interfaces.Attachments;
|
||||||
|
using SpineRuntime35;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V35.Attachments
|
||||||
|
{
|
||||||
|
internal sealed class ClippingAttachment35(ClippingAttachment innerObject) :
|
||||||
|
Attachment35(innerObject),
|
||||||
|
IClippingAttachment
|
||||||
|
{
|
||||||
|
private readonly ClippingAttachment _o = innerObject;
|
||||||
|
|
||||||
|
public override ClippingAttachment InnerObject => _o;
|
||||||
|
|
||||||
|
public override int ComputeWorldVertices(ISlot slot, ref float[] worldVertices)
|
||||||
|
{
|
||||||
|
if (slot is Slot35 st)
|
||||||
|
{
|
||||||
|
var length = _o.WorldVerticesLength;
|
||||||
|
if (worldVertices.Length < length) worldVertices = new float[length];
|
||||||
|
_o.ComputeWorldVertices(st.InnerObject, worldVertices);
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot35)}, but received {slot.GetType().Name}", nameof(slot));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
46
Spine/Implementations/V35/Attachments/MeshAttachment35.cs
Normal file
46
Spine/Implementations/V35/Attachments/MeshAttachment35.cs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Implementations.V35;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using Spine.Interfaces.Attachments;
|
||||||
|
using SpineRuntime35;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V35.Attachments
|
||||||
|
{
|
||||||
|
internal sealed class MeshAttachment35(MeshAttachment innerObject) :
|
||||||
|
Attachment35(innerObject),
|
||||||
|
IMeshAttachment
|
||||||
|
{
|
||||||
|
private readonly MeshAttachment _o = innerObject;
|
||||||
|
|
||||||
|
public override MeshAttachment InnerObject => _o;
|
||||||
|
|
||||||
|
public override int ComputeWorldVertices(ISlot slot, ref float[] worldVertices)
|
||||||
|
{
|
||||||
|
if (slot is Slot35 st)
|
||||||
|
{
|
||||||
|
var length = _o.WorldVerticesLength;
|
||||||
|
if (worldVertices.Length < length) worldVertices = new float[length];
|
||||||
|
_o.ComputeWorldVertices(st.InnerObject, worldVertices);
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot35)}, but received {slot.GetType().Name}", nameof(slot));
|
||||||
|
}
|
||||||
|
|
||||||
|
public float R { get => _o.R; set => _o.R = value; }
|
||||||
|
public float G { get => _o.G; set => _o.G = value; }
|
||||||
|
public float B { get => _o.B; set => _o.B = value; }
|
||||||
|
public float A { get => _o.A; set => _o.A = value; }
|
||||||
|
|
||||||
|
public SFML.Graphics.Texture RendererObject => (SFML.Graphics.Texture)((AtlasRegion)_o.RendererObject).page.rendererObject;
|
||||||
|
|
||||||
|
public float[] UVs => _o.UVs;
|
||||||
|
|
||||||
|
public int[] Triangles => _o.Triangles;
|
||||||
|
|
||||||
|
public int HullLength => _o.HullLength;
|
||||||
|
}
|
||||||
|
}
|
||||||
33
Spine/Implementations/V35/Attachments/PathAttachment35.cs
Normal file
33
Spine/Implementations/V35/Attachments/PathAttachment35.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Implementations.V35;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using Spine.Interfaces.Attachments;
|
||||||
|
using SpineRuntime35;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V35.Attachments
|
||||||
|
{
|
||||||
|
internal sealed class PathAttachment35(PathAttachment innerObject) :
|
||||||
|
Attachment35(innerObject),
|
||||||
|
IPathAttachment
|
||||||
|
{
|
||||||
|
private readonly PathAttachment _o = innerObject;
|
||||||
|
|
||||||
|
public override PathAttachment InnerObject => _o;
|
||||||
|
|
||||||
|
public override int ComputeWorldVertices(ISlot slot, ref float[] worldVertices)
|
||||||
|
{
|
||||||
|
if (slot is Slot35 st)
|
||||||
|
{
|
||||||
|
var length = _o.WorldVerticesLength;
|
||||||
|
if (worldVertices.Length < length) worldVertices = new float[length];
|
||||||
|
_o.ComputeWorldVertices(st.InnerObject, worldVertices);
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot35)}, but received {slot.GetType().Name}", nameof(slot));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
32
Spine/Implementations/V35/Attachments/PointAttachment35.cs
Normal file
32
Spine/Implementations/V35/Attachments/PointAttachment35.cs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Implementations.V35;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using Spine.Interfaces.Attachments;
|
||||||
|
using SpineRuntime35;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V35.Attachments
|
||||||
|
{
|
||||||
|
internal sealed class PointAttachment35(PointAttachment innerObject) :
|
||||||
|
Attachment35(innerObject),
|
||||||
|
IPointAttachment
|
||||||
|
{
|
||||||
|
private readonly PointAttachment _o = innerObject;
|
||||||
|
|
||||||
|
public override PointAttachment InnerObject => _o;
|
||||||
|
|
||||||
|
public override int ComputeWorldVertices(ISlot slot, ref float[] worldVertices)
|
||||||
|
{
|
||||||
|
if (slot is Slot35 st)
|
||||||
|
{
|
||||||
|
if (worldVertices.Length < 2) worldVertices = new float[2];
|
||||||
|
_o.ComputeWorldPosition(st.InnerObject.Bone, out worldVertices[0], out worldVertices[1]);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot35)}, but received {slot.GetType().Name}", nameof(slot));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
41
Spine/Implementations/V35/Attachments/RegionAttachment35.cs
Normal file
41
Spine/Implementations/V35/Attachments/RegionAttachment35.cs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Implementations.V35;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using Spine.Interfaces.Attachments;
|
||||||
|
using SpineRuntime35;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V35.Attachments
|
||||||
|
{
|
||||||
|
internal sealed class RegionAttachment35(RegionAttachment innerObject) :
|
||||||
|
Attachment35(innerObject),
|
||||||
|
IRegionAttachment
|
||||||
|
{
|
||||||
|
private readonly RegionAttachment _o = innerObject;
|
||||||
|
|
||||||
|
public override RegionAttachment InnerObject => _o;
|
||||||
|
|
||||||
|
public override int ComputeWorldVertices(ISlot slot, ref float[] worldVertices)
|
||||||
|
{
|
||||||
|
if (slot is Slot35 st)
|
||||||
|
{
|
||||||
|
if (worldVertices.Length < 8) worldVertices = new float[8];
|
||||||
|
_o.ComputeWorldVertices(st.InnerObject.Bone, worldVertices, 0);
|
||||||
|
return 8;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot35)}, but received {slot.GetType().Name}", nameof(slot));
|
||||||
|
}
|
||||||
|
|
||||||
|
public float R { get => _o.R; set => _o.R = value; }
|
||||||
|
public float G { get => _o.G; set => _o.G = value; }
|
||||||
|
public float B { get => _o.B; set => _o.B = value; }
|
||||||
|
public float A { get => _o.A; set => _o.A = value; }
|
||||||
|
|
||||||
|
public SFML.Graphics.Texture RendererObject => (SFML.Graphics.Texture)((AtlasRegion)_o.RendererObject).page.rendererObject;
|
||||||
|
|
||||||
|
public float[] UVs => _o.UVs;
|
||||||
|
}
|
||||||
|
}
|
||||||
33
Spine/Implementations/V35/Bone35.cs
Normal file
33
Spine/Implementations/V35/Bone35.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using SpineRuntime35;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V35
|
||||||
|
{
|
||||||
|
internal sealed class Bone35(Bone innerObject, Bone35? parent = null) : IBone
|
||||||
|
{
|
||||||
|
private readonly Bone _o = innerObject;
|
||||||
|
private readonly Bone35? _parent = parent;
|
||||||
|
|
||||||
|
public Bone InnerObject => _o;
|
||||||
|
|
||||||
|
public string Name => _o.Data.Name;
|
||||||
|
public int Index => _o.Data.Index;
|
||||||
|
|
||||||
|
public IBone? Parent => _parent;
|
||||||
|
public bool Active => true; // NOTE: 3.7 及以下没有 Active 属性, 此处总是返回 true
|
||||||
|
public float Length => _o.Data.Length;
|
||||||
|
public float WorldX => _o.WorldX;
|
||||||
|
public float WorldY => _o.WorldY;
|
||||||
|
public float A => _o.A;
|
||||||
|
public float B => _o.B;
|
||||||
|
public float C => _o.C;
|
||||||
|
public float D => _o.D;
|
||||||
|
|
||||||
|
public override string ToString() => _o.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
101
Spine/Implementations/V35/Skeleton35.cs
Normal file
101
Spine/Implementations/V35/Skeleton35.cs
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Collections.Frozen;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using SpineRuntime35;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V35
|
||||||
|
{
|
||||||
|
internal sealed class Skeleton35 : ISkeleton
|
||||||
|
{
|
||||||
|
private readonly Skeleton _o;
|
||||||
|
private readonly SpineObjectData35 _data;
|
||||||
|
|
||||||
|
private readonly ImmutableArray<IBone> _bones;
|
||||||
|
private readonly FrozenDictionary<string, IBone> _bonesByName;
|
||||||
|
private readonly ImmutableArray<ISlot> _slots;
|
||||||
|
private readonly FrozenDictionary<string, ISlot> _slotsByName;
|
||||||
|
|
||||||
|
private Skin35? _skin;
|
||||||
|
|
||||||
|
public Skeleton35(Skeleton innerObject, SpineObjectData35 data)
|
||||||
|
{
|
||||||
|
_o = innerObject;
|
||||||
|
_data = data;
|
||||||
|
|
||||||
|
List<Bone35> bones = [];
|
||||||
|
Dictionary<string, IBone> bonesByName = [];
|
||||||
|
foreach (var b in _o.Bones)
|
||||||
|
{
|
||||||
|
var bone = new Bone35(b, b.Parent is null ? null : bones[b.Parent.Data.Index]);
|
||||||
|
bones.Add(bone);
|
||||||
|
bonesByName[bone.Name] = bone;
|
||||||
|
}
|
||||||
|
_bones = bones.Cast<IBone>().ToImmutableArray();
|
||||||
|
_bonesByName = bonesByName.ToFrozenDictionary();
|
||||||
|
|
||||||
|
List<Slot35> slots = [];
|
||||||
|
Dictionary<string, ISlot> slotsByName = [];
|
||||||
|
foreach (var s in _o.Slots)
|
||||||
|
{
|
||||||
|
var slot = new Slot35(s, _data, bones[s.Bone.Data.Index]);
|
||||||
|
slots.Add(slot);
|
||||||
|
slotsByName[slot.Name] = slot;
|
||||||
|
}
|
||||||
|
_slots = slots.Cast<ISlot>().ToImmutableArray();
|
||||||
|
_slotsByName = slotsByName.ToFrozenDictionary();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Skeleton InnerObject => _o;
|
||||||
|
|
||||||
|
public string Name => _o.Data.Name;
|
||||||
|
public float R { get => _o.R; set => _o.R = value; }
|
||||||
|
public float G { get => _o.G; set => _o.G = value; }
|
||||||
|
public float B { get => _o.B; set => _o.B = value; }
|
||||||
|
public float A { get => _o.A; set => _o.A = value; }
|
||||||
|
public float X { get => _o.X; set => _o.X = value; }
|
||||||
|
public float Y { get => _o.Y; set => _o.Y = value; }
|
||||||
|
public float ScaleX { get => _o.ScaleX; set => _o.ScaleX = value; }
|
||||||
|
public float ScaleY { get => _o.ScaleY; set => _o.ScaleY = value; }
|
||||||
|
|
||||||
|
public ImmutableArray<IBone> Bones => _bones;
|
||||||
|
public FrozenDictionary<string, IBone> BonesByName => _bonesByName;
|
||||||
|
public ImmutableArray<ISlot> Slots => _slots;
|
||||||
|
public FrozenDictionary<string, ISlot> SlotsByName => _slotsByName;
|
||||||
|
|
||||||
|
public ISkin? Skin
|
||||||
|
{
|
||||||
|
get => _skin;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value is null)
|
||||||
|
{
|
||||||
|
_o.Skin = null;
|
||||||
|
_skin = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (value is Skin35 sk)
|
||||||
|
{
|
||||||
|
_o.Skin = sk.InnerObject;
|
||||||
|
_skin = sk;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Received {value.GetType().Name}", nameof(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<ISlot> IterDrawOrder() => _o.DrawOrder.Select(s => _slots[s.Data.Index]);
|
||||||
|
public void UpdateCache() => _o.UpdateCache();
|
||||||
|
public void UpdateWorldTransform(ISkeleton.Physics physics) => _o.UpdateWorldTransform();
|
||||||
|
public void SetToSetupPose() => _o.SetToSetupPose();
|
||||||
|
public void SetBonesToSetupPose() => _o.SetBonesToSetupPose();
|
||||||
|
public void SetSlotsToSetupPose() => _o.SetSlotsToSetupPose();
|
||||||
|
public void Update(float delta) => _o.Update(delta);
|
||||||
|
|
||||||
|
public override string ToString() => _o.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
56
Spine/Implementations/V35/SkeletonClipping35.cs
Normal file
56
Spine/Implementations/V35/SkeletonClipping35.cs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
using Spine.Interfaces;
|
||||||
|
using Spine.Interfaces.Attachments;
|
||||||
|
using Spine.Utils;
|
||||||
|
using SpineRuntime35;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V35
|
||||||
|
{
|
||||||
|
internal sealed class SkeletonClipping35 : ISkeletonClipping
|
||||||
|
{
|
||||||
|
private readonly SkeletonClipping _o = new();
|
||||||
|
|
||||||
|
public bool IsClipping => _o.IsClipping();
|
||||||
|
|
||||||
|
public float[] ClippedVertices => _o.ClippedVertices.Items;
|
||||||
|
|
||||||
|
public int ClippedVerticesLength => _o.ClippedVertices.Count;
|
||||||
|
|
||||||
|
public int[] ClippedTriangles => _o.ClippedTriangles.Items;
|
||||||
|
|
||||||
|
public int ClippedTrianglesLength => _o.ClippedTriangles.Count;
|
||||||
|
|
||||||
|
public float[] ClippedUVs => _o.ClippedUVs.Items;
|
||||||
|
|
||||||
|
public void ClipTriangles(float[] vertices, int verticesLength, int[] triangles, int trianglesLength, float[] uvs)
|
||||||
|
=> _o.ClipTriangles(vertices, verticesLength, triangles, trianglesLength, uvs);
|
||||||
|
|
||||||
|
public void ClipStart(ISlot slot, IClippingAttachment clippingAttachment)
|
||||||
|
{
|
||||||
|
if (slot is Slot35 st && clippingAttachment is Attachments.ClippingAttachment35 att)
|
||||||
|
{
|
||||||
|
_o.ClipStart(st.InnerObject, att.InnerObject);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Received {slot.GetType().Name} {clippingAttachment.GetType().Name}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClipEnd(ISlot slot)
|
||||||
|
{
|
||||||
|
if (slot is Slot35 st)
|
||||||
|
{
|
||||||
|
_o.ClipEnd(st.InnerObject);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Received {slot.GetType().Name}", nameof(slot));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClipEnd() => _o.ClipEnd();
|
||||||
|
|
||||||
|
public override string ToString() => _o.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
45
Spine/Implementations/V35/Skin35.cs
Normal file
45
Spine/Implementations/V35/Skin35.cs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using SpineRuntime35;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V35
|
||||||
|
{
|
||||||
|
internal sealed class Skin35 : ISkin
|
||||||
|
{
|
||||||
|
private readonly Skin _o;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用指定名字创建空皮肤
|
||||||
|
/// </summary>
|
||||||
|
public Skin35(string name) => _o = new(name);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 包装已有皮肤对象
|
||||||
|
/// </summary>
|
||||||
|
public Skin35(Skin innerObject) => _o = innerObject;
|
||||||
|
|
||||||
|
public Skin InnerObject => _o;
|
||||||
|
|
||||||
|
public string Name => _o.Name;
|
||||||
|
|
||||||
|
public void AddSkin(ISkin skin)
|
||||||
|
{
|
||||||
|
if (skin is Skin35 sk)
|
||||||
|
{
|
||||||
|
// NOTE: 3.7 及以下不支持 AddSkin
|
||||||
|
foreach (var (k, v) in sk._o.Attachments)
|
||||||
|
_o.AddAttachment(k.slotIndex, k.name, v);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Received {skin.GetType().Name}", nameof(skin));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clear() => _o.Attachments.Clear();
|
||||||
|
|
||||||
|
public override string ToString() => _o.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
81
Spine/Implementations/V35/Slot35.cs
Normal file
81
Spine/Implementations/V35/Slot35.cs
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Frozen;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Utils;
|
||||||
|
using SpineRuntime35;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using Spine.Interfaces.Attachments;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V35
|
||||||
|
{
|
||||||
|
internal sealed class Slot35 : ISlot
|
||||||
|
{
|
||||||
|
private readonly Slot _o;
|
||||||
|
private readonly SpineObjectData35 _data;
|
||||||
|
|
||||||
|
private readonly Bone35 _bone;
|
||||||
|
private readonly SFML.Graphics.BlendMode _blendMode;
|
||||||
|
|
||||||
|
public Slot35(Slot innerObject, SpineObjectData35 data, Bone35 bone)
|
||||||
|
{
|
||||||
|
_o = innerObject;
|
||||||
|
_data = data;
|
||||||
|
|
||||||
|
_bone = bone;
|
||||||
|
_blendMode = _o.Data.BlendMode switch
|
||||||
|
{
|
||||||
|
BlendMode.Normal => SFMLBlendMode.NormalPma,
|
||||||
|
BlendMode.Additive => SFMLBlendMode.AdditivePma,
|
||||||
|
BlendMode.Multiply => SFMLBlendMode.MultiplyPma,
|
||||||
|
BlendMode.Screen => SFMLBlendMode.ScreenPma,
|
||||||
|
_ => throw new NotImplementedException($"{_o.Data.BlendMode}"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public Slot InnerObject => _o;
|
||||||
|
|
||||||
|
public string Name => _o.Data.Name;
|
||||||
|
public int Index => _o.Data.Index;
|
||||||
|
public SFML.Graphics.BlendMode Blend => _blendMode;
|
||||||
|
|
||||||
|
public float R { get => _o.R; set => _o.R = value; }
|
||||||
|
public float G { get => _o.G; set => _o.G = value; }
|
||||||
|
public float B { get => _o.B; set => _o.B = value; }
|
||||||
|
public float A { get => _o.A; set => _o.A = value; }
|
||||||
|
public IBone Bone => _bone;
|
||||||
|
|
||||||
|
public IAttachment? Attachment
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_o.Attachment is Attachment att)
|
||||||
|
{
|
||||||
|
return _data.SlotAttachments[Name][att.Name];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value is null)
|
||||||
|
{
|
||||||
|
_o.Attachment = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (value is Attachments.Attachment35 att)
|
||||||
|
{
|
||||||
|
_o.Attachment = att.InnerObject;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Received {value.GetType().Name}", nameof(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Disabled { get; set; }
|
||||||
|
|
||||||
|
public override string ToString() => _o.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
148
Spine/Implementations/V35/SpineObjectData35.cs
Normal file
148
Spine/Implementations/V35/SpineObjectData35.cs
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Frozen;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Utils;
|
||||||
|
using SpineRuntime35;
|
||||||
|
using Spine.Implementations.V35.Attachments;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using Spine.Interfaces.Attachments;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V35
|
||||||
|
{
|
||||||
|
[SpineImplementation(3, 5)]
|
||||||
|
internal sealed class SpineObjectData35 : SpineObjectData
|
||||||
|
{
|
||||||
|
private readonly Atlas _atlas;
|
||||||
|
private readonly SkeletonData _skeletonData;
|
||||||
|
private readonly AnimationStateData _animationStateData;
|
||||||
|
|
||||||
|
private readonly ImmutableArray<ISkin> _skins;
|
||||||
|
private readonly FrozenDictionary<string, ISkin> _skinsByName;
|
||||||
|
private readonly FrozenDictionary<string, FrozenDictionary<string, IAttachment>> _slotAttachments;
|
||||||
|
private readonly ImmutableArray<IAnimation> _animations;
|
||||||
|
private readonly FrozenDictionary<string, IAnimation> _animationsByName;
|
||||||
|
|
||||||
|
public SpineObjectData35(string skelPath, string atlasPath, TextureLoader textureLoader)
|
||||||
|
: base(skelPath, atlasPath, textureLoader)
|
||||||
|
{
|
||||||
|
// 加载 atlas
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_atlas = new Atlas(atlasPath, textureLoader);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Trace(ex.ToString());
|
||||||
|
throw new InvalidDataException($"Failed to load atlas '{atlasPath}'");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Utf8Validator.IsUtf8(skelPath))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Trace(ex.ToString());
|
||||||
|
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Trace(ex.ToString());
|
||||||
|
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_atlas.Dispose();
|
||||||
|
_logger.Trace(ex.ToString());
|
||||||
|
throw new InvalidDataException($"Failed to load skeleton file {skelPath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载动画数据
|
||||||
|
_animationStateData = new AnimationStateData(_skeletonData);
|
||||||
|
|
||||||
|
// 整理皮肤和附件
|
||||||
|
Dictionary<string, Dictionary<string, IAttachment>> slotAttachments = [];
|
||||||
|
List<ISkin> skins = [];
|
||||||
|
Dictionary<string, ISkin> skinsByName = [];
|
||||||
|
foreach (var s in _skeletonData.Skins)
|
||||||
|
{
|
||||||
|
var skin = new Skin35(s);
|
||||||
|
skins.Add(skin);
|
||||||
|
skinsByName[s.Name] = skin;
|
||||||
|
foreach (var (k, att) in s.Attachments)
|
||||||
|
{
|
||||||
|
var slotName = _skeletonData.Slots.Items[k.slotIndex].Name;
|
||||||
|
if (!slotAttachments.TryGetValue(slotName, out var attachments))
|
||||||
|
slotAttachments[slotName] = attachments = [];
|
||||||
|
|
||||||
|
attachments[att.Name] = att switch
|
||||||
|
{
|
||||||
|
RegionAttachment regionAtt => new RegionAttachment35(regionAtt),
|
||||||
|
MeshAttachment meshAtt => new MeshAttachment35(meshAtt),
|
||||||
|
ClippingAttachment clipAtt => new ClippingAttachment35(clipAtt),
|
||||||
|
BoundingBoxAttachment bbAtt => new BoundingBoxAttachment35(bbAtt),
|
||||||
|
PathAttachment pathAtt => new PathAttachment35(pathAtt),
|
||||||
|
PointAttachment ptAtt => new PointAttachment35(ptAtt),
|
||||||
|
_ => throw new InvalidOperationException($"Unrecognized attachment type {att.GetType().FullName}")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_slotAttachments = slotAttachments.ToFrozenDictionary(it => it.Key, it => it.Value.ToFrozenDictionary());
|
||||||
|
_skins = skins.ToImmutableArray();
|
||||||
|
_skinsByName = skinsByName.ToFrozenDictionary();
|
||||||
|
|
||||||
|
// 整理所有动画数据
|
||||||
|
List<IAnimation> animations = [];
|
||||||
|
Dictionary<string, IAnimation> animationsByName = [];
|
||||||
|
foreach (var a in _skeletonData.Animations)
|
||||||
|
{
|
||||||
|
var anime = new Animation35(a);
|
||||||
|
animations.Add(anime);
|
||||||
|
animationsByName[anime.Name] = anime;
|
||||||
|
}
|
||||||
|
_animations = animations.ToImmutableArray();
|
||||||
|
_animationsByName = animationsByName.ToFrozenDictionary();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string SkeletonVersion => _skeletonData.Version;
|
||||||
|
|
||||||
|
public override ImmutableArray<ISkin> Skins => _skins;
|
||||||
|
|
||||||
|
public override FrozenDictionary<string, ISkin> SkinsByName => _skinsByName;
|
||||||
|
|
||||||
|
public override FrozenDictionary<string, FrozenDictionary<string, IAttachment>> SlotAttachments => _slotAttachments;
|
||||||
|
|
||||||
|
public override float DefaultMix { get => _animationStateData.DefaultMix; set => _animationStateData.DefaultMix = value; }
|
||||||
|
|
||||||
|
public override ImmutableArray<IAnimation> Animations => _animations;
|
||||||
|
|
||||||
|
public override FrozenDictionary<string, IAnimation> AnimationsByName => _animationsByName;
|
||||||
|
|
||||||
|
protected override void DisposeAtlas() => _atlas.Dispose();
|
||||||
|
|
||||||
|
public override ISkeleton CreateSkeleton() => new Skeleton35(new(_skeletonData), this);
|
||||||
|
|
||||||
|
public override IAnimationState CreateAnimationState() => new AnimationState35(new(_animationStateData), this);
|
||||||
|
|
||||||
|
public override ISkeletonClipping CreateSkeletonClipping() => new SkeletonClipping35();
|
||||||
|
|
||||||
|
public override ISkin CreateSkin(string name) => new Skin35(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
185
Spine/Implementations/V35/TrackEntry35.cs
Normal file
185
Spine/Implementations/V35/TrackEntry35.cs
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
using Spine.Interfaces;
|
||||||
|
using SpineRuntime35;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V35
|
||||||
|
{
|
||||||
|
internal sealed class TrackEntry35(TrackEntry innerObject, AnimationState35 animationState, SpineObjectData35 data): ITrackEntry
|
||||||
|
{
|
||||||
|
private readonly TrackEntry _o = innerObject;
|
||||||
|
private readonly AnimationState35 _animationState = animationState;
|
||||||
|
private readonly SpineObjectData35 _data = data;
|
||||||
|
|
||||||
|
private readonly Dictionary<IAnimationState.TrackEntryDelegate, AnimationState.TrackEntryDelegate> _eventMapping = [];
|
||||||
|
private readonly Dictionary<IAnimationState.TrackEntryDelegate, int> _eventCount = [];
|
||||||
|
|
||||||
|
public TrackEntry InnerObject => _o;
|
||||||
|
|
||||||
|
public event IAnimationState.TrackEntryDelegate? Start
|
||||||
|
{
|
||||||
|
add
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (!_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_eventMapping[value] = f = (t) => value(_animationState.GetTrackEntry(t));
|
||||||
|
_eventCount[value] = 0;
|
||||||
|
}
|
||||||
|
_o.Start += f;
|
||||||
|
_eventCount[value]++;
|
||||||
|
}
|
||||||
|
remove
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_o.Start -= f;
|
||||||
|
_eventCount[value]--;
|
||||||
|
if (_eventCount[value] <= 0)
|
||||||
|
{
|
||||||
|
_eventMapping.Remove(value);
|
||||||
|
_eventCount.Remove(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public event IAnimationState.TrackEntryDelegate? Interrupt
|
||||||
|
{
|
||||||
|
add
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (!_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_eventMapping[value] = f = (t) => value(_animationState.GetTrackEntry(t));
|
||||||
|
_eventCount[value] = 0;
|
||||||
|
}
|
||||||
|
_o.Interrupt += f;
|
||||||
|
_eventCount[value]++;
|
||||||
|
}
|
||||||
|
remove
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_o.Interrupt -= f;
|
||||||
|
_eventCount[value]--;
|
||||||
|
if (_eventCount[value] <= 0)
|
||||||
|
{
|
||||||
|
_eventMapping.Remove(value);
|
||||||
|
_eventCount.Remove(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public event IAnimationState.TrackEntryDelegate? End
|
||||||
|
{
|
||||||
|
add
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (!_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_eventMapping[value] = f = (t) => value(_animationState.GetTrackEntry(t));
|
||||||
|
_eventCount[value] = 0;
|
||||||
|
}
|
||||||
|
_o.End += f;
|
||||||
|
_eventCount[value]++;
|
||||||
|
}
|
||||||
|
remove
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_o.End -= f;
|
||||||
|
_eventCount[value]--;
|
||||||
|
if (_eventCount[value] <= 0)
|
||||||
|
{
|
||||||
|
_eventMapping.Remove(value);
|
||||||
|
_eventCount.Remove(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public event IAnimationState.TrackEntryDelegate? Complete
|
||||||
|
{
|
||||||
|
add
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (!_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_eventMapping[value] = f = (t) => value(_animationState.GetTrackEntry(t));
|
||||||
|
_eventCount[value] = 0;
|
||||||
|
}
|
||||||
|
_o.Complete += f;
|
||||||
|
_eventCount[value]++;
|
||||||
|
}
|
||||||
|
remove
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_o.Complete -= f;
|
||||||
|
_eventCount[value]--;
|
||||||
|
if (_eventCount[value] <= 0)
|
||||||
|
{
|
||||||
|
_eventMapping.Remove(value);
|
||||||
|
_eventCount.Remove(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public event IAnimationState.TrackEntryDelegate? Dispose
|
||||||
|
{
|
||||||
|
add
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (!_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_eventMapping[value] = f = (t) => value(_animationState.GetTrackEntry(t));
|
||||||
|
_eventCount[value] = 0;
|
||||||
|
}
|
||||||
|
_o.Dispose += f;
|
||||||
|
_eventCount[value]++;
|
||||||
|
}
|
||||||
|
remove
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_o.Dispose -= f;
|
||||||
|
_eventCount[value]--;
|
||||||
|
if (_eventCount[value] <= 0)
|
||||||
|
{
|
||||||
|
_eventMapping.Remove(value);
|
||||||
|
_eventCount.Remove(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int TrackIndex { get => _o.TrackIndex; }
|
||||||
|
|
||||||
|
public IAnimation Animation { get => _data.AnimationsByName[_o.Animation.Name]; }
|
||||||
|
|
||||||
|
public ITrackEntry? Next { get { var t = _o.Next; return t is null ? null : _animationState.GetTrackEntry(t); } }
|
||||||
|
|
||||||
|
public bool Loop { get => _o.Loop; set => _o.Loop = value; }
|
||||||
|
|
||||||
|
public float TrackTime { get => _o.TrackTime; set => _o.TrackTime = value; }
|
||||||
|
|
||||||
|
public float TimeScale { get => _o.TimeScale; set => _o.TimeScale = value; }
|
||||||
|
|
||||||
|
public float Alpha { get => _o.Alpha; set => _o.Alpha = value; }
|
||||||
|
|
||||||
|
public float MixDuration { get => _o.MixDuration; set => _o.MixDuration = value; }
|
||||||
|
|
||||||
|
public override string ToString() => _o.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
23
Spine/Implementations/V36/Animation36.cs
Normal file
23
Spine/Implementations/V36/Animation36.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
using Spine.Interfaces;
|
||||||
|
using SpineRuntime36;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V36
|
||||||
|
{
|
||||||
|
internal sealed class Animation36(Animation innerObject) : IAnimation
|
||||||
|
{
|
||||||
|
private readonly Animation _o = innerObject;
|
||||||
|
|
||||||
|
public Animation InnerObject => _o;
|
||||||
|
|
||||||
|
public string Name => _o.Name;
|
||||||
|
|
||||||
|
public float Duration => _o.Duration;
|
||||||
|
|
||||||
|
public override string ToString() => _o.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
229
Spine/Implementations/V36/AnimationState36.cs
Normal file
229
Spine/Implementations/V36/AnimationState36.cs
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using SpineRuntime36;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V36
|
||||||
|
{
|
||||||
|
internal sealed class AnimationState36(AnimationState innerObject, SpineObjectData36 data) : IAnimationState
|
||||||
|
{
|
||||||
|
private readonly AnimationState _o = innerObject;
|
||||||
|
private readonly SpineObjectData36 _data = data;
|
||||||
|
|
||||||
|
private readonly Dictionary<TrackEntry, TrackEntry36> _trackEntryPool = [];
|
||||||
|
|
||||||
|
private readonly Dictionary<IAnimationState.TrackEntryDelegate, AnimationState.TrackEntryDelegate> _eventMapping = [];
|
||||||
|
private readonly Dictionary<IAnimationState.TrackEntryDelegate, int> _eventCount = [];
|
||||||
|
|
||||||
|
public AnimationState InnerObject => _o;
|
||||||
|
|
||||||
|
public event IAnimationState.TrackEntryDelegate? Start
|
||||||
|
{
|
||||||
|
add
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (!_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_eventMapping[value] = f = (t) => value(GetTrackEntry(t));
|
||||||
|
_eventCount[value] = 0;
|
||||||
|
}
|
||||||
|
_o.Start += f;
|
||||||
|
_eventCount[value]++;
|
||||||
|
}
|
||||||
|
remove
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_o.Start -= f;
|
||||||
|
_eventCount[value]--;
|
||||||
|
if (_eventCount[value] <= 0)
|
||||||
|
{
|
||||||
|
_eventMapping.Remove(value);
|
||||||
|
_eventCount.Remove(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public event IAnimationState.TrackEntryDelegate? Interrupt
|
||||||
|
{
|
||||||
|
add
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (!_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_eventMapping[value] = f = (t) => value(GetTrackEntry(t));
|
||||||
|
_eventCount[value] = 0;
|
||||||
|
}
|
||||||
|
_o.Interrupt += f;
|
||||||
|
_eventCount[value]++;
|
||||||
|
}
|
||||||
|
remove
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_o.Interrupt -= f;
|
||||||
|
_eventCount[value]--;
|
||||||
|
if (_eventCount[value] <= 0)
|
||||||
|
{
|
||||||
|
_eventMapping.Remove(value);
|
||||||
|
_eventCount.Remove(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public event IAnimationState.TrackEntryDelegate? End
|
||||||
|
{
|
||||||
|
add
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (!_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_eventMapping[value] = f = (t) => value(GetTrackEntry(t));
|
||||||
|
_eventCount[value] = 0;
|
||||||
|
}
|
||||||
|
_o.End += f;
|
||||||
|
_eventCount[value]++;
|
||||||
|
}
|
||||||
|
remove
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_o.End -= f;
|
||||||
|
_eventCount[value]--;
|
||||||
|
if (_eventCount[value] <= 0)
|
||||||
|
{
|
||||||
|
_eventMapping.Remove(value);
|
||||||
|
_eventCount.Remove(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public event IAnimationState.TrackEntryDelegate? Complete
|
||||||
|
{
|
||||||
|
add
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (!_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_eventMapping[value] = f = (t) => value(GetTrackEntry(t));
|
||||||
|
_eventCount[value] = 0;
|
||||||
|
}
|
||||||
|
_o.Complete += f;
|
||||||
|
_eventCount[value]++;
|
||||||
|
}
|
||||||
|
remove
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_o.Complete -= f;
|
||||||
|
_eventCount[value]--;
|
||||||
|
if (_eventCount[value] <= 0)
|
||||||
|
{
|
||||||
|
_eventMapping.Remove(value);
|
||||||
|
_eventCount.Remove(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public event IAnimationState.TrackEntryDelegate? Dispose
|
||||||
|
{
|
||||||
|
add
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (!_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_eventMapping[value] = f = (t) => value(GetTrackEntry(t));
|
||||||
|
_eventCount[value] = 0;
|
||||||
|
}
|
||||||
|
_o.Dispose += f;
|
||||||
|
_eventCount[value]++;
|
||||||
|
}
|
||||||
|
remove
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_o.Dispose -= f;
|
||||||
|
_eventCount[value]--;
|
||||||
|
if (_eventCount[value] <= 0)
|
||||||
|
{
|
||||||
|
_eventMapping.Remove(value);
|
||||||
|
_eventCount.Remove(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public float TimeScale { get => _o.TimeScale; set => _o.TimeScale = value; }
|
||||||
|
|
||||||
|
public void Update(float delta) => _o.Update(delta);
|
||||||
|
|
||||||
|
public void Apply(ISkeleton skeleton)
|
||||||
|
{
|
||||||
|
if (skeleton is Skeleton36 skel)
|
||||||
|
{
|
||||||
|
_o.Apply(skel.InnerObject);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Received {skeleton.GetType().Name}", nameof(skeleton));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取 <see cref="ITrackEntry"/> 对象, 不存在则创建
|
||||||
|
/// </summary>
|
||||||
|
public ITrackEntry GetTrackEntry(TrackEntry trackEntry)
|
||||||
|
{
|
||||||
|
if (!_trackEntryPool.TryGetValue(trackEntry, out var tr))
|
||||||
|
_trackEntryPool[trackEntry] = tr = new(trackEntry, this, _data);
|
||||||
|
return tr;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<ITrackEntry?> IterTracks() => _o.Tracks.Select(t => t is null ? null : GetTrackEntry(t));
|
||||||
|
|
||||||
|
public ITrackEntry? GetCurrent(int index) { var t = _o.GetCurrent(index); return t is null ? null : GetTrackEntry(t); }
|
||||||
|
|
||||||
|
public void ClearTrack(int index) => _o.ClearTrack(index);
|
||||||
|
|
||||||
|
public void ClearTracks() => _o.ClearTracks();
|
||||||
|
|
||||||
|
public ITrackEntry SetAnimation(int trackIndex, string animationName, bool loop)
|
||||||
|
=> GetTrackEntry(_o.SetAnimation(trackIndex, animationName, loop));
|
||||||
|
|
||||||
|
public ITrackEntry SetAnimation(int trackIndex, IAnimation animation, bool loop)
|
||||||
|
{
|
||||||
|
if (animation is Animation36 anime)
|
||||||
|
return GetTrackEntry(_o.SetAnimation(trackIndex, anime.InnerObject, loop));
|
||||||
|
throw new ArgumentException($"Received {animation.GetType().Name}", nameof(animation));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ITrackEntry SetEmptyAnimation(int trackIndex, float mixDuration) => GetTrackEntry(_o.SetEmptyAnimation(trackIndex, mixDuration));
|
||||||
|
|
||||||
|
public void SetEmptyAnimations(float mixDuration) => _o.SetEmptyAnimations(mixDuration);
|
||||||
|
|
||||||
|
public ITrackEntry AddAnimation(int trackIndex, string animationName, bool loop, float delay)
|
||||||
|
=> GetTrackEntry(_o.AddAnimation(trackIndex, animationName, loop, delay));
|
||||||
|
|
||||||
|
public ITrackEntry AddAnimation(int trackIndex, IAnimation animation, bool loop, float delay)
|
||||||
|
{
|
||||||
|
if (animation is Animation36 anime)
|
||||||
|
return GetTrackEntry(_o.AddAnimation(trackIndex, anime.InnerObject, loop, delay));
|
||||||
|
throw new ArgumentException($"Received {animation.GetType().Name}", nameof(animation));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ITrackEntry AddEmptyAnimation(int trackIndex, float mixDuration, float delay)
|
||||||
|
=> GetTrackEntry(_o.AddEmptyAnimation(trackIndex, mixDuration, delay));
|
||||||
|
|
||||||
|
public override string ToString() => _o.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
24
Spine/Implementations/V36/Attachments/Attachment36.cs
Normal file
24
Spine/Implementations/V36/Attachments/Attachment36.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using Spine.Interfaces.Attachments;
|
||||||
|
using SpineRuntime36;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V36.Attachments
|
||||||
|
{
|
||||||
|
internal abstract class Attachment36(Attachment innerObject) : IAttachment
|
||||||
|
{
|
||||||
|
private readonly Attachment _o = innerObject;
|
||||||
|
|
||||||
|
public virtual Attachment InnerObject => _o;
|
||||||
|
|
||||||
|
public string Name => _o.Name;
|
||||||
|
|
||||||
|
public abstract int ComputeWorldVertices(ISlot slot, ref float[] worldVertices);
|
||||||
|
|
||||||
|
public override string ToString() => _o.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Implementations.V36;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using Spine.Interfaces.Attachments;
|
||||||
|
using SpineRuntime36;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V36.Attachments
|
||||||
|
{
|
||||||
|
internal sealed class BoundingBoxAttachment36(BoundingBoxAttachment innerObject) :
|
||||||
|
Attachment36(innerObject),
|
||||||
|
IBoundingBoxAttachment
|
||||||
|
{
|
||||||
|
private readonly BoundingBoxAttachment _o = innerObject;
|
||||||
|
|
||||||
|
public override BoundingBoxAttachment InnerObject => _o;
|
||||||
|
|
||||||
|
public override int ComputeWorldVertices(ISlot slot, ref float[] worldVertices)
|
||||||
|
{
|
||||||
|
if (slot is Slot36 st)
|
||||||
|
{
|
||||||
|
var length = _o.WorldVerticesLength;
|
||||||
|
if (worldVertices.Length < length) worldVertices = new float[length];
|
||||||
|
_o.ComputeWorldVertices(st.InnerObject, worldVertices);
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot36)}, but received {slot.GetType().Name}", nameof(slot));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Implementations.V36;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using Spine.Interfaces.Attachments;
|
||||||
|
using SpineRuntime36;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V36.Attachments
|
||||||
|
{
|
||||||
|
internal sealed class ClippingAttachment36(ClippingAttachment innerObject) :
|
||||||
|
Attachment36(innerObject),
|
||||||
|
IClippingAttachment
|
||||||
|
{
|
||||||
|
private readonly ClippingAttachment _o = innerObject;
|
||||||
|
|
||||||
|
public override ClippingAttachment InnerObject => _o;
|
||||||
|
|
||||||
|
public override int ComputeWorldVertices(ISlot slot, ref float[] worldVertices)
|
||||||
|
{
|
||||||
|
if (slot is Slot36 st)
|
||||||
|
{
|
||||||
|
var length = _o.WorldVerticesLength;
|
||||||
|
if (worldVertices.Length < length) worldVertices = new float[length];
|
||||||
|
_o.ComputeWorldVertices(st.InnerObject, worldVertices);
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot36)}, but received {slot.GetType().Name}", nameof(slot));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
46
Spine/Implementations/V36/Attachments/MeshAttachment36.cs
Normal file
46
Spine/Implementations/V36/Attachments/MeshAttachment36.cs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Implementations.V36;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using Spine.Interfaces.Attachments;
|
||||||
|
using SpineRuntime36;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V36.Attachments
|
||||||
|
{
|
||||||
|
internal sealed class MeshAttachment36(MeshAttachment innerObject) :
|
||||||
|
Attachment36(innerObject),
|
||||||
|
IMeshAttachment
|
||||||
|
{
|
||||||
|
private readonly MeshAttachment _o = innerObject;
|
||||||
|
|
||||||
|
public override MeshAttachment InnerObject => _o;
|
||||||
|
|
||||||
|
public override int ComputeWorldVertices(ISlot slot, ref float[] worldVertices)
|
||||||
|
{
|
||||||
|
if (slot is Slot36 st)
|
||||||
|
{
|
||||||
|
var length = _o.WorldVerticesLength;
|
||||||
|
if (worldVertices.Length < length) worldVertices = new float[length];
|
||||||
|
_o.ComputeWorldVertices(st.InnerObject, worldVertices);
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot36)}, but received {slot.GetType().Name}", nameof(slot));
|
||||||
|
}
|
||||||
|
|
||||||
|
public float R { get => _o.R; set => _o.R = value; }
|
||||||
|
public float G { get => _o.G; set => _o.G = value; }
|
||||||
|
public float B { get => _o.B; set => _o.B = value; }
|
||||||
|
public float A { get => _o.A; set => _o.A = value; }
|
||||||
|
|
||||||
|
public SFML.Graphics.Texture RendererObject => (SFML.Graphics.Texture)((AtlasRegion)_o.RendererObject).page.rendererObject;
|
||||||
|
|
||||||
|
public float[] UVs => _o.UVs;
|
||||||
|
|
||||||
|
public int[] Triangles => _o.Triangles;
|
||||||
|
|
||||||
|
public int HullLength => _o.HullLength;
|
||||||
|
}
|
||||||
|
}
|
||||||
33
Spine/Implementations/V36/Attachments/PathAttachment36.cs
Normal file
33
Spine/Implementations/V36/Attachments/PathAttachment36.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Implementations.V36;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using Spine.Interfaces.Attachments;
|
||||||
|
using SpineRuntime36;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V36.Attachments
|
||||||
|
{
|
||||||
|
internal sealed class PathAttachment36(PathAttachment innerObject) :
|
||||||
|
Attachment36(innerObject),
|
||||||
|
IPathAttachment
|
||||||
|
{
|
||||||
|
private readonly PathAttachment _o = innerObject;
|
||||||
|
|
||||||
|
public override PathAttachment InnerObject => _o;
|
||||||
|
|
||||||
|
public override int ComputeWorldVertices(ISlot slot, ref float[] worldVertices)
|
||||||
|
{
|
||||||
|
if (slot is Slot36 st)
|
||||||
|
{
|
||||||
|
var length = _o.WorldVerticesLength;
|
||||||
|
if (worldVertices.Length < length) worldVertices = new float[length];
|
||||||
|
_o.ComputeWorldVertices(st.InnerObject, worldVertices);
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot36)}, but received {slot.GetType().Name}", nameof(slot));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
32
Spine/Implementations/V36/Attachments/PointAttachment36.cs
Normal file
32
Spine/Implementations/V36/Attachments/PointAttachment36.cs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Implementations.V36;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using Spine.Interfaces.Attachments;
|
||||||
|
using SpineRuntime36;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V36.Attachments
|
||||||
|
{
|
||||||
|
internal sealed class PointAttachment36(PointAttachment innerObject) :
|
||||||
|
Attachment36(innerObject),
|
||||||
|
IPointAttachment
|
||||||
|
{
|
||||||
|
private readonly PointAttachment _o = innerObject;
|
||||||
|
|
||||||
|
public override PointAttachment InnerObject => _o;
|
||||||
|
|
||||||
|
public override int ComputeWorldVertices(ISlot slot, ref float[] worldVertices)
|
||||||
|
{
|
||||||
|
if (slot is Slot36 st)
|
||||||
|
{
|
||||||
|
if (worldVertices.Length < 2) worldVertices = new float[2];
|
||||||
|
_o.ComputeWorldPosition(st.InnerObject.Bone, out worldVertices[0], out worldVertices[1]);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot36)}, but received {slot.GetType().Name}", nameof(slot));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
41
Spine/Implementations/V36/Attachments/RegionAttachment36.cs
Normal file
41
Spine/Implementations/V36/Attachments/RegionAttachment36.cs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Implementations.V36;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using Spine.Interfaces.Attachments;
|
||||||
|
using SpineRuntime36;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V36.Attachments
|
||||||
|
{
|
||||||
|
internal sealed class RegionAttachment36(RegionAttachment innerObject) :
|
||||||
|
Attachment36(innerObject),
|
||||||
|
IRegionAttachment
|
||||||
|
{
|
||||||
|
private readonly RegionAttachment _o = innerObject;
|
||||||
|
|
||||||
|
public override RegionAttachment InnerObject => _o;
|
||||||
|
|
||||||
|
public override int ComputeWorldVertices(ISlot slot, ref float[] worldVertices)
|
||||||
|
{
|
||||||
|
if (slot is Slot36 st)
|
||||||
|
{
|
||||||
|
if (worldVertices.Length < 8) worldVertices = new float[8];
|
||||||
|
_o.ComputeWorldVertices(st.InnerObject.Bone, worldVertices, 0);
|
||||||
|
return 8;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot36)}, but received {slot.GetType().Name}", nameof(slot));
|
||||||
|
}
|
||||||
|
|
||||||
|
public float R { get => _o.R; set => _o.R = value; }
|
||||||
|
public float G { get => _o.G; set => _o.G = value; }
|
||||||
|
public float B { get => _o.B; set => _o.B = value; }
|
||||||
|
public float A { get => _o.A; set => _o.A = value; }
|
||||||
|
|
||||||
|
public SFML.Graphics.Texture RendererObject => (SFML.Graphics.Texture)((AtlasRegion)_o.RendererObject).page.rendererObject;
|
||||||
|
|
||||||
|
public float[] UVs => _o.UVs;
|
||||||
|
}
|
||||||
|
}
|
||||||
33
Spine/Implementations/V36/Bone36.cs
Normal file
33
Spine/Implementations/V36/Bone36.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using SpineRuntime36;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V36
|
||||||
|
{
|
||||||
|
internal sealed class Bone36(Bone innerObject, Bone36? parent = null) : IBone
|
||||||
|
{
|
||||||
|
private readonly Bone _o = innerObject;
|
||||||
|
private readonly Bone36? _parent = parent;
|
||||||
|
|
||||||
|
public Bone InnerObject => _o;
|
||||||
|
|
||||||
|
public string Name => _o.Data.Name;
|
||||||
|
public int Index => _o.Data.Index;
|
||||||
|
|
||||||
|
public IBone? Parent => _parent;
|
||||||
|
public bool Active => true; // NOTE: 3.7 及以下没有 Active 属性, 此处总是返回 true
|
||||||
|
public float Length => _o.Data.Length;
|
||||||
|
public float WorldX => _o.WorldX;
|
||||||
|
public float WorldY => _o.WorldY;
|
||||||
|
public float A => _o.A;
|
||||||
|
public float B => _o.B;
|
||||||
|
public float C => _o.C;
|
||||||
|
public float D => _o.D;
|
||||||
|
|
||||||
|
public override string ToString() => _o.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
101
Spine/Implementations/V36/Skeleton36.cs
Normal file
101
Spine/Implementations/V36/Skeleton36.cs
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Collections.Frozen;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using SpineRuntime36;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V36
|
||||||
|
{
|
||||||
|
internal sealed class Skeleton36 : ISkeleton
|
||||||
|
{
|
||||||
|
private readonly Skeleton _o;
|
||||||
|
private readonly SpineObjectData36 _data;
|
||||||
|
|
||||||
|
private readonly ImmutableArray<IBone> _bones;
|
||||||
|
private readonly FrozenDictionary<string, IBone> _bonesByName;
|
||||||
|
private readonly ImmutableArray<ISlot> _slots;
|
||||||
|
private readonly FrozenDictionary<string, ISlot> _slotsByName;
|
||||||
|
|
||||||
|
private Skin36? _skin;
|
||||||
|
|
||||||
|
public Skeleton36(Skeleton innerObject, SpineObjectData36 data)
|
||||||
|
{
|
||||||
|
_o = innerObject;
|
||||||
|
_data = data;
|
||||||
|
|
||||||
|
List<Bone36> bones = [];
|
||||||
|
Dictionary<string, IBone> bonesByName = [];
|
||||||
|
foreach (var b in _o.Bones)
|
||||||
|
{
|
||||||
|
var bone = new Bone36(b, b.Parent is null ? null : bones[b.Parent.Data.Index]);
|
||||||
|
bones.Add(bone);
|
||||||
|
bonesByName[bone.Name] = bone;
|
||||||
|
}
|
||||||
|
_bones = bones.Cast<IBone>().ToImmutableArray();
|
||||||
|
_bonesByName = bonesByName.ToFrozenDictionary();
|
||||||
|
|
||||||
|
List<Slot36> slots = [];
|
||||||
|
Dictionary<string, ISlot> slotsByName = [];
|
||||||
|
foreach (var s in _o.Slots)
|
||||||
|
{
|
||||||
|
var slot = new Slot36(s, _data, bones[s.Bone.Data.Index]);
|
||||||
|
slots.Add(slot);
|
||||||
|
slotsByName[slot.Name] = slot;
|
||||||
|
}
|
||||||
|
_slots = slots.Cast<ISlot>().ToImmutableArray();
|
||||||
|
_slotsByName = slotsByName.ToFrozenDictionary();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Skeleton InnerObject => _o;
|
||||||
|
|
||||||
|
public string Name => _o.Data.Name;
|
||||||
|
public float R { get => _o.R; set => _o.R = value; }
|
||||||
|
public float G { get => _o.G; set => _o.G = value; }
|
||||||
|
public float B { get => _o.B; set => _o.B = value; }
|
||||||
|
public float A { get => _o.A; set => _o.A = value; }
|
||||||
|
public float X { get => _o.X; set => _o.X = value; }
|
||||||
|
public float Y { get => _o.Y; set => _o.Y = value; }
|
||||||
|
public float ScaleX { get => _o.ScaleX; set => _o.ScaleX = value; }
|
||||||
|
public float ScaleY { get => _o.ScaleY; set => _o.ScaleY = value; }
|
||||||
|
|
||||||
|
public ImmutableArray<IBone> Bones => _bones;
|
||||||
|
public FrozenDictionary<string, IBone> BonesByName => _bonesByName;
|
||||||
|
public ImmutableArray<ISlot> Slots => _slots;
|
||||||
|
public FrozenDictionary<string, ISlot> SlotsByName => _slotsByName;
|
||||||
|
|
||||||
|
public ISkin? Skin
|
||||||
|
{
|
||||||
|
get => _skin;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value is null)
|
||||||
|
{
|
||||||
|
_o.Skin = null;
|
||||||
|
_skin = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (value is Skin36 sk)
|
||||||
|
{
|
||||||
|
_o.Skin = sk.InnerObject;
|
||||||
|
_skin = sk;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Received {value.GetType().Name}", nameof(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<ISlot> IterDrawOrder() => _o.DrawOrder.Select(s => _slots[s.Data.Index]);
|
||||||
|
public void UpdateCache() => _o.UpdateCache();
|
||||||
|
public void UpdateWorldTransform(ISkeleton.Physics physics) => _o.UpdateWorldTransform();
|
||||||
|
public void SetToSetupPose() => _o.SetToSetupPose();
|
||||||
|
public void SetBonesToSetupPose() => _o.SetBonesToSetupPose();
|
||||||
|
public void SetSlotsToSetupPose() => _o.SetSlotsToSetupPose();
|
||||||
|
public void Update(float delta) => _o.Update(delta);
|
||||||
|
|
||||||
|
public override string ToString() => _o.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
56
Spine/Implementations/V36/SkeletonClipping36.cs
Normal file
56
Spine/Implementations/V36/SkeletonClipping36.cs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
using Spine.Interfaces;
|
||||||
|
using Spine.Interfaces.Attachments;
|
||||||
|
using Spine.Utils;
|
||||||
|
using SpineRuntime36;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V36
|
||||||
|
{
|
||||||
|
internal sealed class SkeletonClipping36 : ISkeletonClipping
|
||||||
|
{
|
||||||
|
private readonly SkeletonClipping _o = new();
|
||||||
|
|
||||||
|
public bool IsClipping => _o.IsClipping;
|
||||||
|
|
||||||
|
public float[] ClippedVertices => _o.ClippedVertices.Items;
|
||||||
|
|
||||||
|
public int ClippedVerticesLength => _o.ClippedVertices.Count;
|
||||||
|
|
||||||
|
public int[] ClippedTriangles => _o.ClippedTriangles.Items;
|
||||||
|
|
||||||
|
public int ClippedTrianglesLength => _o.ClippedTriangles.Count;
|
||||||
|
|
||||||
|
public float[] ClippedUVs => _o.ClippedUVs.Items;
|
||||||
|
|
||||||
|
public void ClipTriangles(float[] vertices, int verticesLength, int[] triangles, int trianglesLength, float[] uvs)
|
||||||
|
=> _o.ClipTriangles(vertices, verticesLength, triangles, trianglesLength, uvs);
|
||||||
|
|
||||||
|
public void ClipStart(ISlot slot, IClippingAttachment clippingAttachment)
|
||||||
|
{
|
||||||
|
if (slot is Slot36 st && clippingAttachment is Attachments.ClippingAttachment36 att)
|
||||||
|
{
|
||||||
|
_o.ClipStart(st.InnerObject, att.InnerObject);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Received {slot.GetType().Name} {clippingAttachment.GetType().Name}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClipEnd(ISlot slot)
|
||||||
|
{
|
||||||
|
if (slot is Slot36 st)
|
||||||
|
{
|
||||||
|
_o.ClipEnd(st.InnerObject);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Received {slot.GetType().Name}", nameof(slot));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClipEnd() => _o.ClipEnd();
|
||||||
|
|
||||||
|
public override string ToString() => _o.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
45
Spine/Implementations/V36/Skin36.cs
Normal file
45
Spine/Implementations/V36/Skin36.cs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using SpineRuntime36;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V36
|
||||||
|
{
|
||||||
|
internal sealed class Skin36 : ISkin
|
||||||
|
{
|
||||||
|
private readonly Skin _o;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用指定名字创建空皮肤
|
||||||
|
/// </summary>
|
||||||
|
public Skin36(string name) => _o = new(name);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 包装已有皮肤对象
|
||||||
|
/// </summary>
|
||||||
|
public Skin36(Skin innerObject) => _o = innerObject;
|
||||||
|
|
||||||
|
public Skin InnerObject => _o;
|
||||||
|
|
||||||
|
public string Name => _o.Name;
|
||||||
|
|
||||||
|
public void AddSkin(ISkin skin)
|
||||||
|
{
|
||||||
|
if (skin is Skin36 sk)
|
||||||
|
{
|
||||||
|
// NOTE: 3.7 及以下不支持 AddSkin
|
||||||
|
foreach (var (k, v) in sk._o.Attachments)
|
||||||
|
_o.AddAttachment(k.slotIndex, k.name, v);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Received {skin.GetType().Name}", nameof(skin));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clear() => _o.Attachments.Clear();
|
||||||
|
|
||||||
|
public override string ToString() => _o.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
81
Spine/Implementations/V36/Slot36.cs
Normal file
81
Spine/Implementations/V36/Slot36.cs
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Frozen;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Utils;
|
||||||
|
using SpineRuntime36;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using Spine.Interfaces.Attachments;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V36
|
||||||
|
{
|
||||||
|
internal sealed class Slot36 : ISlot
|
||||||
|
{
|
||||||
|
private readonly Slot _o;
|
||||||
|
private readonly SpineObjectData36 _data;
|
||||||
|
|
||||||
|
private readonly Bone36 _bone;
|
||||||
|
private readonly SFML.Graphics.BlendMode _blendMode;
|
||||||
|
|
||||||
|
public Slot36(Slot innerObject, SpineObjectData36 data, Bone36 bone)
|
||||||
|
{
|
||||||
|
_o = innerObject;
|
||||||
|
_data = data;
|
||||||
|
|
||||||
|
_bone = bone;
|
||||||
|
_blendMode = _o.Data.BlendMode switch
|
||||||
|
{
|
||||||
|
BlendMode.Normal => SFMLBlendMode.NormalPma,
|
||||||
|
BlendMode.Additive => SFMLBlendMode.AdditivePma,
|
||||||
|
BlendMode.Multiply => SFMLBlendMode.MultiplyPma,
|
||||||
|
BlendMode.Screen => SFMLBlendMode.ScreenPma,
|
||||||
|
_ => throw new NotImplementedException($"{_o.Data.BlendMode}"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public Slot InnerObject => _o;
|
||||||
|
|
||||||
|
public string Name => _o.Data.Name;
|
||||||
|
public int Index => _o.Data.Index;
|
||||||
|
public SFML.Graphics.BlendMode Blend => _blendMode;
|
||||||
|
|
||||||
|
public float R { get => _o.R; set => _o.R = value; }
|
||||||
|
public float G { get => _o.G; set => _o.G = value; }
|
||||||
|
public float B { get => _o.B; set => _o.B = value; }
|
||||||
|
public float A { get => _o.A; set => _o.A = value; }
|
||||||
|
public IBone Bone => _bone;
|
||||||
|
|
||||||
|
public IAttachment? Attachment
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_o.Attachment is Attachment att)
|
||||||
|
{
|
||||||
|
return _data.SlotAttachments[Name][att.Name];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value is null)
|
||||||
|
{
|
||||||
|
_o.Attachment = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (value is Attachments.Attachment36 att)
|
||||||
|
{
|
||||||
|
_o.Attachment = att.InnerObject;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Received {value.GetType().Name}", nameof(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Disabled { get; set; }
|
||||||
|
|
||||||
|
public override string ToString() => _o.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
148
Spine/Implementations/V36/SpineObjectData36.cs
Normal file
148
Spine/Implementations/V36/SpineObjectData36.cs
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Frozen;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Utils;
|
||||||
|
using SpineRuntime36;
|
||||||
|
using Spine.Implementations.V36.Attachments;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using Spine.Interfaces.Attachments;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V36
|
||||||
|
{
|
||||||
|
[SpineImplementation(3, 6)]
|
||||||
|
internal sealed class SpineObjectData36 : SpineObjectData
|
||||||
|
{
|
||||||
|
private readonly Atlas _atlas;
|
||||||
|
private readonly SkeletonData _skeletonData;
|
||||||
|
private readonly AnimationStateData _animationStateData;
|
||||||
|
|
||||||
|
private readonly ImmutableArray<ISkin> _skins;
|
||||||
|
private readonly FrozenDictionary<string, ISkin> _skinsByName;
|
||||||
|
private readonly FrozenDictionary<string, FrozenDictionary<string, IAttachment>> _slotAttachments;
|
||||||
|
private readonly ImmutableArray<IAnimation> _animations;
|
||||||
|
private readonly FrozenDictionary<string, IAnimation> _animationsByName;
|
||||||
|
|
||||||
|
public SpineObjectData36(string skelPath, string atlasPath, TextureLoader textureLoader)
|
||||||
|
: base(skelPath, atlasPath, textureLoader)
|
||||||
|
{
|
||||||
|
// 加载 atlas
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_atlas = new Atlas(atlasPath, textureLoader);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Trace(ex.ToString());
|
||||||
|
throw new InvalidDataException($"Failed to load atlas '{atlasPath}'");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Utf8Validator.IsUtf8(skelPath))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Trace(ex.ToString());
|
||||||
|
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Trace(ex.ToString());
|
||||||
|
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_atlas.Dispose();
|
||||||
|
_logger.Trace(ex.ToString());
|
||||||
|
throw new InvalidDataException($"Failed to load skeleton file {skelPath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载动画数据
|
||||||
|
_animationStateData = new AnimationStateData(_skeletonData);
|
||||||
|
|
||||||
|
// 整理皮肤和附件
|
||||||
|
Dictionary<string, Dictionary<string, IAttachment>> slotAttachments = [];
|
||||||
|
List<ISkin> skins = [];
|
||||||
|
Dictionary<string, ISkin> skinsByName = [];
|
||||||
|
foreach (var s in _skeletonData.Skins)
|
||||||
|
{
|
||||||
|
var skin = new Skin36(s);
|
||||||
|
skins.Add(skin);
|
||||||
|
skinsByName[s.Name] = skin;
|
||||||
|
foreach (var (k, att) in s.Attachments)
|
||||||
|
{
|
||||||
|
var slotName = _skeletonData.Slots.Items[k.slotIndex].Name;
|
||||||
|
if (!slotAttachments.TryGetValue(slotName, out var attachments))
|
||||||
|
slotAttachments[slotName] = attachments = [];
|
||||||
|
|
||||||
|
attachments[att.Name] = att switch
|
||||||
|
{
|
||||||
|
RegionAttachment regionAtt => new RegionAttachment36(regionAtt),
|
||||||
|
MeshAttachment meshAtt => new MeshAttachment36(meshAtt),
|
||||||
|
ClippingAttachment clipAtt => new ClippingAttachment36(clipAtt),
|
||||||
|
BoundingBoxAttachment bbAtt => new BoundingBoxAttachment36(bbAtt),
|
||||||
|
PathAttachment pathAtt => new PathAttachment36(pathAtt),
|
||||||
|
PointAttachment ptAtt => new PointAttachment36(ptAtt),
|
||||||
|
_ => throw new InvalidOperationException($"Unrecognized attachment type {att.GetType().FullName}")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_slotAttachments = slotAttachments.ToFrozenDictionary(it => it.Key, it => it.Value.ToFrozenDictionary());
|
||||||
|
_skins = skins.ToImmutableArray();
|
||||||
|
_skinsByName = skinsByName.ToFrozenDictionary();
|
||||||
|
|
||||||
|
// 整理所有动画数据
|
||||||
|
List<IAnimation> animations = [];
|
||||||
|
Dictionary<string, IAnimation> animationsByName = [];
|
||||||
|
foreach (var a in _skeletonData.Animations)
|
||||||
|
{
|
||||||
|
var anime = new Animation36(a);
|
||||||
|
animations.Add(anime);
|
||||||
|
animationsByName[anime.Name] = anime;
|
||||||
|
}
|
||||||
|
_animations = animations.ToImmutableArray();
|
||||||
|
_animationsByName = animationsByName.ToFrozenDictionary();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string SkeletonVersion => _skeletonData.Version;
|
||||||
|
|
||||||
|
public override ImmutableArray<ISkin> Skins => _skins;
|
||||||
|
|
||||||
|
public override FrozenDictionary<string, ISkin> SkinsByName => _skinsByName;
|
||||||
|
|
||||||
|
public override FrozenDictionary<string, FrozenDictionary<string, IAttachment>> SlotAttachments => _slotAttachments;
|
||||||
|
|
||||||
|
public override float DefaultMix { get => _animationStateData.DefaultMix; set => _animationStateData.DefaultMix = value; }
|
||||||
|
|
||||||
|
public override ImmutableArray<IAnimation> Animations => _animations;
|
||||||
|
|
||||||
|
public override FrozenDictionary<string, IAnimation> AnimationsByName => _animationsByName;
|
||||||
|
|
||||||
|
protected override void DisposeAtlas() => _atlas.Dispose();
|
||||||
|
|
||||||
|
public override ISkeleton CreateSkeleton() => new Skeleton36(new(_skeletonData), this);
|
||||||
|
|
||||||
|
public override IAnimationState CreateAnimationState() => new AnimationState36(new(_animationStateData), this);
|
||||||
|
|
||||||
|
public override ISkeletonClipping CreateSkeletonClipping() => new SkeletonClipping36();
|
||||||
|
|
||||||
|
public override ISkin CreateSkin(string name) => new Skin36(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
185
Spine/Implementations/V36/TrackEntry36.cs
Normal file
185
Spine/Implementations/V36/TrackEntry36.cs
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
using Spine.Interfaces;
|
||||||
|
using SpineRuntime36;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V36
|
||||||
|
{
|
||||||
|
internal sealed class TrackEntry36(TrackEntry innerObject, AnimationState36 animationState, SpineObjectData36 data): ITrackEntry
|
||||||
|
{
|
||||||
|
private readonly TrackEntry _o = innerObject;
|
||||||
|
private readonly AnimationState36 _animationState = animationState;
|
||||||
|
private readonly SpineObjectData36 _data = data;
|
||||||
|
|
||||||
|
private readonly Dictionary<IAnimationState.TrackEntryDelegate, AnimationState.TrackEntryDelegate> _eventMapping = [];
|
||||||
|
private readonly Dictionary<IAnimationState.TrackEntryDelegate, int> _eventCount = [];
|
||||||
|
|
||||||
|
public TrackEntry InnerObject => _o;
|
||||||
|
|
||||||
|
public event IAnimationState.TrackEntryDelegate? Start
|
||||||
|
{
|
||||||
|
add
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (!_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_eventMapping[value] = f = (t) => value(_animationState.GetTrackEntry(t));
|
||||||
|
_eventCount[value] = 0;
|
||||||
|
}
|
||||||
|
_o.Start += f;
|
||||||
|
_eventCount[value]++;
|
||||||
|
}
|
||||||
|
remove
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_o.Start -= f;
|
||||||
|
_eventCount[value]--;
|
||||||
|
if (_eventCount[value] <= 0)
|
||||||
|
{
|
||||||
|
_eventMapping.Remove(value);
|
||||||
|
_eventCount.Remove(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public event IAnimationState.TrackEntryDelegate? Interrupt
|
||||||
|
{
|
||||||
|
add
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (!_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_eventMapping[value] = f = (t) => value(_animationState.GetTrackEntry(t));
|
||||||
|
_eventCount[value] = 0;
|
||||||
|
}
|
||||||
|
_o.Interrupt += f;
|
||||||
|
_eventCount[value]++;
|
||||||
|
}
|
||||||
|
remove
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_o.Interrupt -= f;
|
||||||
|
_eventCount[value]--;
|
||||||
|
if (_eventCount[value] <= 0)
|
||||||
|
{
|
||||||
|
_eventMapping.Remove(value);
|
||||||
|
_eventCount.Remove(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public event IAnimationState.TrackEntryDelegate? End
|
||||||
|
{
|
||||||
|
add
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (!_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_eventMapping[value] = f = (t) => value(_animationState.GetTrackEntry(t));
|
||||||
|
_eventCount[value] = 0;
|
||||||
|
}
|
||||||
|
_o.End += f;
|
||||||
|
_eventCount[value]++;
|
||||||
|
}
|
||||||
|
remove
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_o.End -= f;
|
||||||
|
_eventCount[value]--;
|
||||||
|
if (_eventCount[value] <= 0)
|
||||||
|
{
|
||||||
|
_eventMapping.Remove(value);
|
||||||
|
_eventCount.Remove(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public event IAnimationState.TrackEntryDelegate? Complete
|
||||||
|
{
|
||||||
|
add
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (!_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_eventMapping[value] = f = (t) => value(_animationState.GetTrackEntry(t));
|
||||||
|
_eventCount[value] = 0;
|
||||||
|
}
|
||||||
|
_o.Complete += f;
|
||||||
|
_eventCount[value]++;
|
||||||
|
}
|
||||||
|
remove
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_o.Complete -= f;
|
||||||
|
_eventCount[value]--;
|
||||||
|
if (_eventCount[value] <= 0)
|
||||||
|
{
|
||||||
|
_eventMapping.Remove(value);
|
||||||
|
_eventCount.Remove(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public event IAnimationState.TrackEntryDelegate? Dispose
|
||||||
|
{
|
||||||
|
add
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (!_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_eventMapping[value] = f = (t) => value(_animationState.GetTrackEntry(t));
|
||||||
|
_eventCount[value] = 0;
|
||||||
|
}
|
||||||
|
_o.Dispose += f;
|
||||||
|
_eventCount[value]++;
|
||||||
|
}
|
||||||
|
remove
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_o.Dispose -= f;
|
||||||
|
_eventCount[value]--;
|
||||||
|
if (_eventCount[value] <= 0)
|
||||||
|
{
|
||||||
|
_eventMapping.Remove(value);
|
||||||
|
_eventCount.Remove(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int TrackIndex { get => _o.TrackIndex; }
|
||||||
|
|
||||||
|
public IAnimation Animation { get => _data.AnimationsByName[_o.Animation.Name]; }
|
||||||
|
|
||||||
|
public ITrackEntry? Next { get { var t = _o.Next; return t is null ? null : _animationState.GetTrackEntry(t); } }
|
||||||
|
|
||||||
|
public bool Loop { get => _o.Loop; set => _o.Loop = value; }
|
||||||
|
|
||||||
|
public float TrackTime { get => _o.TrackTime; set => _o.TrackTime = value; }
|
||||||
|
|
||||||
|
public float TimeScale { get => _o.TimeScale; set => _o.TimeScale = value; }
|
||||||
|
|
||||||
|
public float Alpha { get => _o.Alpha; set => _o.Alpha = value; }
|
||||||
|
|
||||||
|
public float MixDuration { get => _o.MixDuration; set => _o.MixDuration = value; }
|
||||||
|
|
||||||
|
public override string ToString() => _o.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
23
Spine/Implementations/V37/Animation37.cs
Normal file
23
Spine/Implementations/V37/Animation37.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
using Spine.Interfaces;
|
||||||
|
using SpineRuntime37;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V37
|
||||||
|
{
|
||||||
|
internal sealed class Animation37(Animation innerObject) : IAnimation
|
||||||
|
{
|
||||||
|
private readonly Animation _o = innerObject;
|
||||||
|
|
||||||
|
public Animation InnerObject => _o;
|
||||||
|
|
||||||
|
public string Name => _o.Name;
|
||||||
|
|
||||||
|
public float Duration => _o.Duration;
|
||||||
|
|
||||||
|
public override string ToString() => _o.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
229
Spine/Implementations/V37/AnimationState37.cs
Normal file
229
Spine/Implementations/V37/AnimationState37.cs
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using SpineRuntime37;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V37
|
||||||
|
{
|
||||||
|
internal sealed class AnimationState37(AnimationState innerObject, SpineObjectData37 data) : IAnimationState
|
||||||
|
{
|
||||||
|
private readonly AnimationState _o = innerObject;
|
||||||
|
private readonly SpineObjectData37 _data = data;
|
||||||
|
|
||||||
|
private readonly Dictionary<TrackEntry, TrackEntry37> _trackEntryPool = [];
|
||||||
|
|
||||||
|
private readonly Dictionary<IAnimationState.TrackEntryDelegate, AnimationState.TrackEntryDelegate> _eventMapping = [];
|
||||||
|
private readonly Dictionary<IAnimationState.TrackEntryDelegate, int> _eventCount = [];
|
||||||
|
|
||||||
|
public AnimationState InnerObject => _o;
|
||||||
|
|
||||||
|
public event IAnimationState.TrackEntryDelegate? Start
|
||||||
|
{
|
||||||
|
add
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (!_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_eventMapping[value] = f = (t) => value(GetTrackEntry(t));
|
||||||
|
_eventCount[value] = 0;
|
||||||
|
}
|
||||||
|
_o.Start += f;
|
||||||
|
_eventCount[value]++;
|
||||||
|
}
|
||||||
|
remove
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_o.Start -= f;
|
||||||
|
_eventCount[value]--;
|
||||||
|
if (_eventCount[value] <= 0)
|
||||||
|
{
|
||||||
|
_eventMapping.Remove(value);
|
||||||
|
_eventCount.Remove(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public event IAnimationState.TrackEntryDelegate? Interrupt
|
||||||
|
{
|
||||||
|
add
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (!_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_eventMapping[value] = f = (t) => value(GetTrackEntry(t));
|
||||||
|
_eventCount[value] = 0;
|
||||||
|
}
|
||||||
|
_o.Interrupt += f;
|
||||||
|
_eventCount[value]++;
|
||||||
|
}
|
||||||
|
remove
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_o.Interrupt -= f;
|
||||||
|
_eventCount[value]--;
|
||||||
|
if (_eventCount[value] <= 0)
|
||||||
|
{
|
||||||
|
_eventMapping.Remove(value);
|
||||||
|
_eventCount.Remove(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public event IAnimationState.TrackEntryDelegate? End
|
||||||
|
{
|
||||||
|
add
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (!_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_eventMapping[value] = f = (t) => value(GetTrackEntry(t));
|
||||||
|
_eventCount[value] = 0;
|
||||||
|
}
|
||||||
|
_o.End += f;
|
||||||
|
_eventCount[value]++;
|
||||||
|
}
|
||||||
|
remove
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_o.End -= f;
|
||||||
|
_eventCount[value]--;
|
||||||
|
if (_eventCount[value] <= 0)
|
||||||
|
{
|
||||||
|
_eventMapping.Remove(value);
|
||||||
|
_eventCount.Remove(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public event IAnimationState.TrackEntryDelegate? Complete
|
||||||
|
{
|
||||||
|
add
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (!_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_eventMapping[value] = f = (t) => value(GetTrackEntry(t));
|
||||||
|
_eventCount[value] = 0;
|
||||||
|
}
|
||||||
|
_o.Complete += f;
|
||||||
|
_eventCount[value]++;
|
||||||
|
}
|
||||||
|
remove
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_o.Complete -= f;
|
||||||
|
_eventCount[value]--;
|
||||||
|
if (_eventCount[value] <= 0)
|
||||||
|
{
|
||||||
|
_eventMapping.Remove(value);
|
||||||
|
_eventCount.Remove(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public event IAnimationState.TrackEntryDelegate? Dispose
|
||||||
|
{
|
||||||
|
add
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (!_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_eventMapping[value] = f = (t) => value(GetTrackEntry(t));
|
||||||
|
_eventCount[value] = 0;
|
||||||
|
}
|
||||||
|
_o.Dispose += f;
|
||||||
|
_eventCount[value]++;
|
||||||
|
}
|
||||||
|
remove
|
||||||
|
{
|
||||||
|
if (value is null) return;
|
||||||
|
if (_eventMapping.TryGetValue(value, out var f))
|
||||||
|
{
|
||||||
|
_o.Dispose -= f;
|
||||||
|
_eventCount[value]--;
|
||||||
|
if (_eventCount[value] <= 0)
|
||||||
|
{
|
||||||
|
_eventMapping.Remove(value);
|
||||||
|
_eventCount.Remove(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public float TimeScale { get => _o.TimeScale; set => _o.TimeScale = value; }
|
||||||
|
|
||||||
|
public void Update(float delta) => _o.Update(delta);
|
||||||
|
|
||||||
|
public void Apply(ISkeleton skeleton)
|
||||||
|
{
|
||||||
|
if (skeleton is Skeleton37 skel)
|
||||||
|
{
|
||||||
|
_o.Apply(skel.InnerObject);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Received {skeleton.GetType().Name}", nameof(skeleton));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取 <see cref="ITrackEntry"/> 对象, 不存在则创建
|
||||||
|
/// </summary>
|
||||||
|
public ITrackEntry GetTrackEntry(TrackEntry trackEntry)
|
||||||
|
{
|
||||||
|
if (!_trackEntryPool.TryGetValue(trackEntry, out var tr))
|
||||||
|
_trackEntryPool[trackEntry] = tr = new(trackEntry, this, _data);
|
||||||
|
return tr;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<ITrackEntry?> IterTracks() => _o.Tracks.Select(t => t is null ? null : GetTrackEntry(t));
|
||||||
|
|
||||||
|
public ITrackEntry? GetCurrent(int index) { var t = _o.GetCurrent(index); return t is null ? null : GetTrackEntry(t); }
|
||||||
|
|
||||||
|
public void ClearTrack(int index) => _o.ClearTrack(index);
|
||||||
|
|
||||||
|
public void ClearTracks() => _o.ClearTracks();
|
||||||
|
|
||||||
|
public ITrackEntry SetAnimation(int trackIndex, string animationName, bool loop)
|
||||||
|
=> GetTrackEntry(_o.SetAnimation(trackIndex, animationName, loop));
|
||||||
|
|
||||||
|
public ITrackEntry SetAnimation(int trackIndex, IAnimation animation, bool loop)
|
||||||
|
{
|
||||||
|
if (animation is Animation37 anime)
|
||||||
|
return GetTrackEntry(_o.SetAnimation(trackIndex, anime.InnerObject, loop));
|
||||||
|
throw new ArgumentException($"Received {animation.GetType().Name}", nameof(animation));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ITrackEntry SetEmptyAnimation(int trackIndex, float mixDuration) => GetTrackEntry(_o.SetEmptyAnimation(trackIndex, mixDuration));
|
||||||
|
|
||||||
|
public void SetEmptyAnimations(float mixDuration) => _o.SetEmptyAnimations(mixDuration);
|
||||||
|
|
||||||
|
public ITrackEntry AddAnimation(int trackIndex, string animationName, bool loop, float delay)
|
||||||
|
=> GetTrackEntry(_o.AddAnimation(trackIndex, animationName, loop, delay));
|
||||||
|
|
||||||
|
public ITrackEntry AddAnimation(int trackIndex, IAnimation animation, bool loop, float delay)
|
||||||
|
{
|
||||||
|
if (animation is Animation37 anime)
|
||||||
|
return GetTrackEntry(_o.AddAnimation(trackIndex, anime.InnerObject, loop, delay));
|
||||||
|
throw new ArgumentException($"Received {animation.GetType().Name}", nameof(animation));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ITrackEntry AddEmptyAnimation(int trackIndex, float mixDuration, float delay)
|
||||||
|
=> GetTrackEntry(_o.AddEmptyAnimation(trackIndex, mixDuration, delay));
|
||||||
|
|
||||||
|
public override string ToString() => _o.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
24
Spine/Implementations/V37/Attachments/Attachment37.cs
Normal file
24
Spine/Implementations/V37/Attachments/Attachment37.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using Spine.Interfaces.Attachments;
|
||||||
|
using SpineRuntime37;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V37.Attachments
|
||||||
|
{
|
||||||
|
internal abstract class Attachment37(Attachment innerObject) : IAttachment
|
||||||
|
{
|
||||||
|
private readonly Attachment _o = innerObject;
|
||||||
|
|
||||||
|
public virtual Attachment InnerObject => _o;
|
||||||
|
|
||||||
|
public string Name => _o.Name;
|
||||||
|
|
||||||
|
public abstract int ComputeWorldVertices(ISlot slot, ref float[] worldVertices);
|
||||||
|
|
||||||
|
public override string ToString() => _o.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Implementations.V37;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using Spine.Interfaces.Attachments;
|
||||||
|
using SpineRuntime37;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V37.Attachments
|
||||||
|
{
|
||||||
|
internal sealed class BoundingBoxAttachment37(BoundingBoxAttachment innerObject) :
|
||||||
|
Attachment37(innerObject),
|
||||||
|
IBoundingBoxAttachment
|
||||||
|
{
|
||||||
|
private readonly BoundingBoxAttachment _o = innerObject;
|
||||||
|
|
||||||
|
public override BoundingBoxAttachment InnerObject => _o;
|
||||||
|
|
||||||
|
public override int ComputeWorldVertices(ISlot slot, ref float[] worldVertices)
|
||||||
|
{
|
||||||
|
if (slot is Slot37 st)
|
||||||
|
{
|
||||||
|
var length = _o.WorldVerticesLength;
|
||||||
|
if (worldVertices.Length < length) worldVertices = new float[length];
|
||||||
|
_o.ComputeWorldVertices(st.InnerObject, worldVertices);
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot37)}, but received {slot.GetType().Name}", nameof(slot));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Implementations.V37;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using Spine.Interfaces.Attachments;
|
||||||
|
using SpineRuntime37;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V37.Attachments
|
||||||
|
{
|
||||||
|
internal sealed class ClippingAttachment37(ClippingAttachment innerObject) :
|
||||||
|
Attachment37(innerObject),
|
||||||
|
IClippingAttachment
|
||||||
|
{
|
||||||
|
private readonly ClippingAttachment _o = innerObject;
|
||||||
|
|
||||||
|
public override ClippingAttachment InnerObject => _o;
|
||||||
|
|
||||||
|
public override int ComputeWorldVertices(ISlot slot, ref float[] worldVertices)
|
||||||
|
{
|
||||||
|
if (slot is Slot37 st)
|
||||||
|
{
|
||||||
|
var length = _o.WorldVerticesLength;
|
||||||
|
if (worldVertices.Length < length) worldVertices = new float[length];
|
||||||
|
_o.ComputeWorldVertices(st.InnerObject, worldVertices);
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot37)}, but received {slot.GetType().Name}", nameof(slot));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
46
Spine/Implementations/V37/Attachments/MeshAttachment37.cs
Normal file
46
Spine/Implementations/V37/Attachments/MeshAttachment37.cs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Implementations.V37;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using Spine.Interfaces.Attachments;
|
||||||
|
using SpineRuntime37;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V37.Attachments
|
||||||
|
{
|
||||||
|
internal sealed class MeshAttachment37(MeshAttachment innerObject) :
|
||||||
|
Attachment37(innerObject),
|
||||||
|
IMeshAttachment
|
||||||
|
{
|
||||||
|
private readonly MeshAttachment _o = innerObject;
|
||||||
|
|
||||||
|
public override MeshAttachment InnerObject => _o;
|
||||||
|
|
||||||
|
public override int ComputeWorldVertices(ISlot slot, ref float[] worldVertices)
|
||||||
|
{
|
||||||
|
if (slot is Slot37 st)
|
||||||
|
{
|
||||||
|
var length = _o.WorldVerticesLength;
|
||||||
|
if (worldVertices.Length < length) worldVertices = new float[length];
|
||||||
|
_o.ComputeWorldVertices(st.InnerObject, worldVertices);
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot37)}, but received {slot.GetType().Name}", nameof(slot));
|
||||||
|
}
|
||||||
|
|
||||||
|
public float R { get => _o.R; set => _o.R = value; }
|
||||||
|
public float G { get => _o.G; set => _o.G = value; }
|
||||||
|
public float B { get => _o.B; set => _o.B = value; }
|
||||||
|
public float A { get => _o.A; set => _o.A = value; }
|
||||||
|
|
||||||
|
public SFML.Graphics.Texture RendererObject => (SFML.Graphics.Texture)((AtlasRegion)_o.RendererObject).page.rendererObject;
|
||||||
|
|
||||||
|
public float[] UVs => _o.UVs;
|
||||||
|
|
||||||
|
public int[] Triangles => _o.Triangles;
|
||||||
|
|
||||||
|
public int HullLength => _o.HullLength;
|
||||||
|
}
|
||||||
|
}
|
||||||
33
Spine/Implementations/V37/Attachments/PathAttachment37.cs
Normal file
33
Spine/Implementations/V37/Attachments/PathAttachment37.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Implementations.V37;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using Spine.Interfaces.Attachments;
|
||||||
|
using SpineRuntime37;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V37.Attachments
|
||||||
|
{
|
||||||
|
internal sealed class PathAttachment37(PathAttachment innerObject) :
|
||||||
|
Attachment37(innerObject),
|
||||||
|
IPathAttachment
|
||||||
|
{
|
||||||
|
private readonly PathAttachment _o = innerObject;
|
||||||
|
|
||||||
|
public override PathAttachment InnerObject => _o;
|
||||||
|
|
||||||
|
public override int ComputeWorldVertices(ISlot slot, ref float[] worldVertices)
|
||||||
|
{
|
||||||
|
if (slot is Slot37 st)
|
||||||
|
{
|
||||||
|
var length = _o.WorldVerticesLength;
|
||||||
|
if (worldVertices.Length < length) worldVertices = new float[length];
|
||||||
|
_o.ComputeWorldVertices(st.InnerObject, worldVertices);
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot37)}, but received {slot.GetType().Name}", nameof(slot));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
32
Spine/Implementations/V37/Attachments/PointAttachment37.cs
Normal file
32
Spine/Implementations/V37/Attachments/PointAttachment37.cs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Implementations.V37;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using Spine.Interfaces.Attachments;
|
||||||
|
using SpineRuntime37;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V37.Attachments
|
||||||
|
{
|
||||||
|
internal sealed class PointAttachment37(PointAttachment innerObject) :
|
||||||
|
Attachment37(innerObject),
|
||||||
|
IPointAttachment
|
||||||
|
{
|
||||||
|
private readonly PointAttachment _o = innerObject;
|
||||||
|
|
||||||
|
public override PointAttachment InnerObject => _o;
|
||||||
|
|
||||||
|
public override int ComputeWorldVertices(ISlot slot, ref float[] worldVertices)
|
||||||
|
{
|
||||||
|
if (slot is Slot37 st)
|
||||||
|
{
|
||||||
|
if (worldVertices.Length < 2) worldVertices = new float[2];
|
||||||
|
_o.ComputeWorldPosition(st.InnerObject.Bone, out worldVertices[0], out worldVertices[1]);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot37)}, but received {slot.GetType().Name}", nameof(slot));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
41
Spine/Implementations/V37/Attachments/RegionAttachment37.cs
Normal file
41
Spine/Implementations/V37/Attachments/RegionAttachment37.cs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Implementations.V37;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using Spine.Interfaces.Attachments;
|
||||||
|
using SpineRuntime37;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V37.Attachments
|
||||||
|
{
|
||||||
|
internal sealed class RegionAttachment37(RegionAttachment innerObject) :
|
||||||
|
Attachment37(innerObject),
|
||||||
|
IRegionAttachment
|
||||||
|
{
|
||||||
|
private readonly RegionAttachment _o = innerObject;
|
||||||
|
|
||||||
|
public override RegionAttachment InnerObject => _o;
|
||||||
|
|
||||||
|
public override int ComputeWorldVertices(ISlot slot, ref float[] worldVertices)
|
||||||
|
{
|
||||||
|
if (slot is Slot37 st)
|
||||||
|
{
|
||||||
|
if (worldVertices.Length < 8) worldVertices = new float[8];
|
||||||
|
_o.ComputeWorldVertices(st.InnerObject.Bone, worldVertices, 0);
|
||||||
|
return 8;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot37)}, but received {slot.GetType().Name}", nameof(slot));
|
||||||
|
}
|
||||||
|
|
||||||
|
public float R { get => _o.R; set => _o.R = value; }
|
||||||
|
public float G { get => _o.G; set => _o.G = value; }
|
||||||
|
public float B { get => _o.B; set => _o.B = value; }
|
||||||
|
public float A { get => _o.A; set => _o.A = value; }
|
||||||
|
|
||||||
|
public SFML.Graphics.Texture RendererObject => (SFML.Graphics.Texture)((AtlasRegion)_o.RendererObject).page.rendererObject;
|
||||||
|
|
||||||
|
public float[] UVs => _o.UVs;
|
||||||
|
}
|
||||||
|
}
|
||||||
33
Spine/Implementations/V37/Bone37.cs
Normal file
33
Spine/Implementations/V37/Bone37.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using SpineRuntime37;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V37
|
||||||
|
{
|
||||||
|
internal sealed class Bone37(Bone innerObject, Bone37? parent = null) : IBone
|
||||||
|
{
|
||||||
|
private readonly Bone _o = innerObject;
|
||||||
|
private readonly Bone37? _parent = parent;
|
||||||
|
|
||||||
|
public Bone InnerObject => _o;
|
||||||
|
|
||||||
|
public string Name => _o.Data.Name;
|
||||||
|
public int Index => _o.Data.Index;
|
||||||
|
|
||||||
|
public IBone? Parent => _parent;
|
||||||
|
public bool Active => true; // NOTE: 3.7 及以下没有 Active 属性, 此处总是返回 true
|
||||||
|
public float Length => _o.Data.Length;
|
||||||
|
public float WorldX => _o.WorldX;
|
||||||
|
public float WorldY => _o.WorldY;
|
||||||
|
public float A => _o.A;
|
||||||
|
public float B => _o.B;
|
||||||
|
public float C => _o.C;
|
||||||
|
public float D => _o.D;
|
||||||
|
|
||||||
|
public override string ToString() => _o.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
101
Spine/Implementations/V37/Skeleton37.cs
Normal file
101
Spine/Implementations/V37/Skeleton37.cs
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Collections.Frozen;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using SpineRuntime37;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V37
|
||||||
|
{
|
||||||
|
internal sealed class Skeleton37 : ISkeleton
|
||||||
|
{
|
||||||
|
private readonly Skeleton _o;
|
||||||
|
private readonly SpineObjectData37 _data;
|
||||||
|
|
||||||
|
private readonly ImmutableArray<IBone> _bones;
|
||||||
|
private readonly FrozenDictionary<string, IBone> _bonesByName;
|
||||||
|
private readonly ImmutableArray<ISlot> _slots;
|
||||||
|
private readonly FrozenDictionary<string, ISlot> _slotsByName;
|
||||||
|
|
||||||
|
private Skin37? _skin;
|
||||||
|
|
||||||
|
public Skeleton37(Skeleton innerObject, SpineObjectData37 data)
|
||||||
|
{
|
||||||
|
_o = innerObject;
|
||||||
|
_data = data;
|
||||||
|
|
||||||
|
List<Bone37> bones = [];
|
||||||
|
Dictionary<string, IBone> bonesByName = [];
|
||||||
|
foreach (var b in _o.Bones)
|
||||||
|
{
|
||||||
|
var bone = new Bone37(b, b.Parent is null ? null : bones[b.Parent.Data.Index]);
|
||||||
|
bones.Add(bone);
|
||||||
|
bonesByName[bone.Name] = bone;
|
||||||
|
}
|
||||||
|
_bones = bones.Cast<IBone>().ToImmutableArray();
|
||||||
|
_bonesByName = bonesByName.ToFrozenDictionary();
|
||||||
|
|
||||||
|
List<Slot37> slots = [];
|
||||||
|
Dictionary<string, ISlot> slotsByName = [];
|
||||||
|
foreach (var s in _o.Slots)
|
||||||
|
{
|
||||||
|
var slot = new Slot37(s, _data, bones[s.Bone.Data.Index]);
|
||||||
|
slots.Add(slot);
|
||||||
|
slotsByName[slot.Name] = slot;
|
||||||
|
}
|
||||||
|
_slots = slots.Cast<ISlot>().ToImmutableArray();
|
||||||
|
_slotsByName = slotsByName.ToFrozenDictionary();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Skeleton InnerObject => _o;
|
||||||
|
|
||||||
|
public string Name => _o.Data.Name;
|
||||||
|
public float R { get => _o.R; set => _o.R = value; }
|
||||||
|
public float G { get => _o.G; set => _o.G = value; }
|
||||||
|
public float B { get => _o.B; set => _o.B = value; }
|
||||||
|
public float A { get => _o.A; set => _o.A = value; }
|
||||||
|
public float X { get => _o.X; set => _o.X = value; }
|
||||||
|
public float Y { get => _o.Y; set => _o.Y = value; }
|
||||||
|
public float ScaleX { get => _o.ScaleX; set => _o.ScaleX = value; }
|
||||||
|
public float ScaleY { get => _o.ScaleY; set => _o.ScaleY = value; }
|
||||||
|
|
||||||
|
public ImmutableArray<IBone> Bones => _bones;
|
||||||
|
public FrozenDictionary<string, IBone> BonesByName => _bonesByName;
|
||||||
|
public ImmutableArray<ISlot> Slots => _slots;
|
||||||
|
public FrozenDictionary<string, ISlot> SlotsByName => _slotsByName;
|
||||||
|
|
||||||
|
public ISkin? Skin
|
||||||
|
{
|
||||||
|
get => _skin;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value is null)
|
||||||
|
{
|
||||||
|
_o.Skin = null;
|
||||||
|
_skin = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (value is Skin37 sk)
|
||||||
|
{
|
||||||
|
_o.Skin = sk.InnerObject;
|
||||||
|
_skin = sk;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Received {value.GetType().Name}", nameof(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<ISlot> IterDrawOrder() => _o.DrawOrder.Select(s => _slots[s.Data.Index]);
|
||||||
|
public void UpdateCache() => _o.UpdateCache();
|
||||||
|
public void UpdateWorldTransform(ISkeleton.Physics physics) => _o.UpdateWorldTransform();
|
||||||
|
public void SetToSetupPose() => _o.SetToSetupPose();
|
||||||
|
public void SetBonesToSetupPose() => _o.SetBonesToSetupPose();
|
||||||
|
public void SetSlotsToSetupPose() => _o.SetSlotsToSetupPose();
|
||||||
|
public void Update(float delta) => _o.Update(delta);
|
||||||
|
|
||||||
|
public override string ToString() => _o.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
56
Spine/Implementations/V37/SkeletonClipping37.cs
Normal file
56
Spine/Implementations/V37/SkeletonClipping37.cs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
using Spine.Interfaces;
|
||||||
|
using Spine.Interfaces.Attachments;
|
||||||
|
using Spine.Utils;
|
||||||
|
using SpineRuntime37;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V37
|
||||||
|
{
|
||||||
|
internal sealed class SkeletonClipping37 : ISkeletonClipping
|
||||||
|
{
|
||||||
|
private readonly SkeletonClipping _o = new();
|
||||||
|
|
||||||
|
public bool IsClipping => _o.IsClipping;
|
||||||
|
|
||||||
|
public float[] ClippedVertices => _o.ClippedVertices.Items;
|
||||||
|
|
||||||
|
public int ClippedVerticesLength => _o.ClippedVertices.Count;
|
||||||
|
|
||||||
|
public int[] ClippedTriangles => _o.ClippedTriangles.Items;
|
||||||
|
|
||||||
|
public int ClippedTrianglesLength => _o.ClippedTriangles.Count;
|
||||||
|
|
||||||
|
public float[] ClippedUVs => _o.ClippedUVs.Items;
|
||||||
|
|
||||||
|
public void ClipTriangles(float[] vertices, int verticesLength, int[] triangles, int trianglesLength, float[] uvs)
|
||||||
|
=> _o.ClipTriangles(vertices, verticesLength, triangles, trianglesLength, uvs);
|
||||||
|
|
||||||
|
public void ClipStart(ISlot slot, IClippingAttachment clippingAttachment)
|
||||||
|
{
|
||||||
|
if (slot is Slot37 st && clippingAttachment is Attachments.ClippingAttachment37 att)
|
||||||
|
{
|
||||||
|
_o.ClipStart(st.InnerObject, att.InnerObject);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Received {slot.GetType().Name} {clippingAttachment.GetType().Name}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClipEnd(ISlot slot)
|
||||||
|
{
|
||||||
|
if (slot is Slot37 st)
|
||||||
|
{
|
||||||
|
_o.ClipEnd(st.InnerObject);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Received {slot.GetType().Name}", nameof(slot));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClipEnd() => _o.ClipEnd();
|
||||||
|
|
||||||
|
public override string ToString() => _o.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
45
Spine/Implementations/V37/Skin37.cs
Normal file
45
Spine/Implementations/V37/Skin37.cs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using SpineRuntime37;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V37
|
||||||
|
{
|
||||||
|
internal sealed class Skin37 : ISkin
|
||||||
|
{
|
||||||
|
private readonly Skin _o;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用指定名字创建空皮肤
|
||||||
|
/// </summary>
|
||||||
|
public Skin37(string name) => _o = new(name);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 包装已有皮肤对象
|
||||||
|
/// </summary>
|
||||||
|
public Skin37(Skin innerObject) => _o = innerObject;
|
||||||
|
|
||||||
|
public Skin InnerObject => _o;
|
||||||
|
|
||||||
|
public string Name => _o.Name;
|
||||||
|
|
||||||
|
public void AddSkin(ISkin skin)
|
||||||
|
{
|
||||||
|
if (skin is Skin37 sk)
|
||||||
|
{
|
||||||
|
// NOTE: 3.7 及以下不支持 AddSkin
|
||||||
|
foreach (var (k, v) in sk._o.Attachments)
|
||||||
|
_o.AddAttachment(k.slotIndex, k.name, v);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Received {skin.GetType().Name}", nameof(skin));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clear() => _o.Attachments.Clear();
|
||||||
|
|
||||||
|
public override string ToString() => _o.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
81
Spine/Implementations/V37/Slot37.cs
Normal file
81
Spine/Implementations/V37/Slot37.cs
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Frozen;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Utils;
|
||||||
|
using SpineRuntime37;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using Spine.Interfaces.Attachments;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V37
|
||||||
|
{
|
||||||
|
internal sealed class Slot37 : ISlot
|
||||||
|
{
|
||||||
|
private readonly Slot _o;
|
||||||
|
private readonly SpineObjectData37 _data;
|
||||||
|
|
||||||
|
private readonly Bone37 _bone;
|
||||||
|
private readonly SFML.Graphics.BlendMode _blendMode;
|
||||||
|
|
||||||
|
public Slot37(Slot innerObject, SpineObjectData37 data, Bone37 bone)
|
||||||
|
{
|
||||||
|
_o = innerObject;
|
||||||
|
_data = data;
|
||||||
|
|
||||||
|
_bone = bone;
|
||||||
|
_blendMode = _o.Data.BlendMode switch
|
||||||
|
{
|
||||||
|
BlendMode.Normal => SFMLBlendMode.NormalPma,
|
||||||
|
BlendMode.Additive => SFMLBlendMode.AdditivePma,
|
||||||
|
BlendMode.Multiply => SFMLBlendMode.MultiplyPma,
|
||||||
|
BlendMode.Screen => SFMLBlendMode.ScreenPma,
|
||||||
|
_ => throw new NotImplementedException($"{_o.Data.BlendMode}"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public Slot InnerObject => _o;
|
||||||
|
|
||||||
|
public string Name => _o.Data.Name;
|
||||||
|
public int Index => _o.Data.Index;
|
||||||
|
public SFML.Graphics.BlendMode Blend => _blendMode;
|
||||||
|
|
||||||
|
public float R { get => _o.R; set => _o.R = value; }
|
||||||
|
public float G { get => _o.G; set => _o.G = value; }
|
||||||
|
public float B { get => _o.B; set => _o.B = value; }
|
||||||
|
public float A { get => _o.A; set => _o.A = value; }
|
||||||
|
public IBone Bone => _bone;
|
||||||
|
|
||||||
|
public IAttachment? Attachment
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_o.Attachment is Attachment att)
|
||||||
|
{
|
||||||
|
return _data.SlotAttachments[Name][att.Name];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value is null)
|
||||||
|
{
|
||||||
|
_o.Attachment = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (value is Attachments.Attachment37 att)
|
||||||
|
{
|
||||||
|
_o.Attachment = att.InnerObject;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Received {value.GetType().Name}", nameof(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Disabled { get; set; }
|
||||||
|
|
||||||
|
public override string ToString() => _o.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
148
Spine/Implementations/V37/SpineObjectData37.cs
Normal file
148
Spine/Implementations/V37/SpineObjectData37.cs
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Frozen;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Spine.Utils;
|
||||||
|
using SpineRuntime37;
|
||||||
|
using Spine.Implementations.V37.Attachments;
|
||||||
|
using Spine.Interfaces;
|
||||||
|
using Spine.Interfaces.Attachments;
|
||||||
|
|
||||||
|
namespace Spine.Implementations.V37
|
||||||
|
{
|
||||||
|
[SpineImplementation(3, 7)]
|
||||||
|
internal sealed class SpineObjectData37 : SpineObjectData
|
||||||
|
{
|
||||||
|
private readonly Atlas _atlas;
|
||||||
|
private readonly SkeletonData _skeletonData;
|
||||||
|
private readonly AnimationStateData _animationStateData;
|
||||||
|
|
||||||
|
private readonly ImmutableArray<ISkin> _skins;
|
||||||
|
private readonly FrozenDictionary<string, ISkin> _skinsByName;
|
||||||
|
private readonly FrozenDictionary<string, FrozenDictionary<string, IAttachment>> _slotAttachments;
|
||||||
|
private readonly ImmutableArray<IAnimation> _animations;
|
||||||
|
private readonly FrozenDictionary<string, IAnimation> _animationsByName;
|
||||||
|
|
||||||
|
public SpineObjectData37(string skelPath, string atlasPath, TextureLoader textureLoader)
|
||||||
|
: base(skelPath, atlasPath, textureLoader)
|
||||||
|
{
|
||||||
|
// 加载 atlas
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_atlas = new Atlas(atlasPath, textureLoader);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Trace(ex.ToString());
|
||||||
|
throw new InvalidDataException($"Failed to load atlas '{atlasPath}'");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Utf8Validator.IsUtf8(skelPath))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Trace(ex.ToString());
|
||||||
|
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Trace(ex.ToString());
|
||||||
|
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_atlas.Dispose();
|
||||||
|
_logger.Trace(ex.ToString());
|
||||||
|
throw new InvalidDataException($"Failed to load skeleton file {skelPath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载动画数据
|
||||||
|
_animationStateData = new AnimationStateData(_skeletonData);
|
||||||
|
|
||||||
|
// 整理皮肤和附件
|
||||||
|
Dictionary<string, Dictionary<string, IAttachment>> slotAttachments = [];
|
||||||
|
List<ISkin> skins = [];
|
||||||
|
Dictionary<string, ISkin> skinsByName = [];
|
||||||
|
foreach (var s in _skeletonData.Skins)
|
||||||
|
{
|
||||||
|
var skin = new Skin37(s);
|
||||||
|
skins.Add(skin);
|
||||||
|
skinsByName[s.Name] = skin;
|
||||||
|
foreach (var (k, att) in s.Attachments)
|
||||||
|
{
|
||||||
|
var slotName = _skeletonData.Slots.Items[k.slotIndex].Name;
|
||||||
|
if (!slotAttachments.TryGetValue(slotName, out var attachments))
|
||||||
|
slotAttachments[slotName] = attachments = [];
|
||||||
|
|
||||||
|
attachments[att.Name] = att switch
|
||||||
|
{
|
||||||
|
RegionAttachment regionAtt => new RegionAttachment37(regionAtt),
|
||||||
|
MeshAttachment meshAtt => new MeshAttachment37(meshAtt),
|
||||||
|
ClippingAttachment clipAtt => new ClippingAttachment37(clipAtt),
|
||||||
|
BoundingBoxAttachment bbAtt => new BoundingBoxAttachment37(bbAtt),
|
||||||
|
PathAttachment pathAtt => new PathAttachment37(pathAtt),
|
||||||
|
PointAttachment ptAtt => new PointAttachment37(ptAtt),
|
||||||
|
_ => throw new InvalidOperationException($"Unrecognized attachment type {att.GetType().FullName}")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_slotAttachments = slotAttachments.ToFrozenDictionary(it => it.Key, it => it.Value.ToFrozenDictionary());
|
||||||
|
_skins = skins.ToImmutableArray();
|
||||||
|
_skinsByName = skinsByName.ToFrozenDictionary();
|
||||||
|
|
||||||
|
// 整理所有动画数据
|
||||||
|
List<IAnimation> animations = [];
|
||||||
|
Dictionary<string, IAnimation> animationsByName = [];
|
||||||
|
foreach (var a in _skeletonData.Animations)
|
||||||
|
{
|
||||||
|
var anime = new Animation37(a);
|
||||||
|
animations.Add(anime);
|
||||||
|
animationsByName[anime.Name] = anime;
|
||||||
|
}
|
||||||
|
_animations = animations.ToImmutableArray();
|
||||||
|
_animationsByName = animationsByName.ToFrozenDictionary();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string SkeletonVersion => _skeletonData.Version;
|
||||||
|
|
||||||
|
public override ImmutableArray<ISkin> Skins => _skins;
|
||||||
|
|
||||||
|
public override FrozenDictionary<string, ISkin> SkinsByName => _skinsByName;
|
||||||
|
|
||||||
|
public override FrozenDictionary<string, FrozenDictionary<string, IAttachment>> SlotAttachments => _slotAttachments;
|
||||||
|
|
||||||
|
public override float DefaultMix { get => _animationStateData.DefaultMix; set => _animationStateData.DefaultMix = value; }
|
||||||
|
|
||||||
|
public override ImmutableArray<IAnimation> Animations => _animations;
|
||||||
|
|
||||||
|
public override FrozenDictionary<string, IAnimation> AnimationsByName => _animationsByName;
|
||||||
|
|
||||||
|
protected override void DisposeAtlas() => _atlas.Dispose();
|
||||||
|
|
||||||
|
public override ISkeleton CreateSkeleton() => new Skeleton37(new(_skeletonData), this);
|
||||||
|
|
||||||
|
public override IAnimationState CreateAnimationState() => new AnimationState37(new(_animationStateData), this);
|
||||||
|
|
||||||
|
public override ISkeletonClipping CreateSkeletonClipping() => new SkeletonClipping37();
|
||||||
|
|
||||||
|
public override ISkin CreateSkin(string name) => new Skin37(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user