refactor: Start on cleanup and polish

This commit is contained in:
Calum H. authored and Alejandro González committed 2025-06-14 17:41:57 +02:00
1 parent 77796e8761
commit 075a9d597a
7 files changed
+344 -838

No files matched your search

+1
View File
@@ -1 +1,2 @@
**/dist
*.gltf
@@ -1,641 +1 @@
{
"asset": { "version": "2.0", "generator": "Blockbench 4.12.4 glTF exporter" },
"scenes": [{ "nodes": [18], "name": "blockbench_export" }],
"scene": 0,
"nodes": [
{ "translation": [0, -1.5, 0], "name": "Head", "mesh": 0 },
{ "translation": [0, -1.5, 0], "name": "Hat Layer", "mesh": 1 },
{
"rotation": [
0.05228614400929896, -0.043559608510925024, 0.002282862348496242, 0.997679060715675
],
"translation": [0, 1.5, 0],
"name": "Head",
"children": [0, 1]
},
{ "translation": [0, -1.5, 0], "name": "Body", "mesh": 2 },
{ "translation": [0, -1.5, 0], "name": "Body Layer", "mesh": 3 },
{ "translation": [0, 1.5, 0], "name": "Body", "children": [3, 4] },
{ "translation": [-0.3125, -1.375, 0], "name": "Right Arm", "mesh": 4 },
{ "translation": [-0.3125, -1.375, 0], "name": "Right Arm Layer", "mesh": 5 },
{
"rotation": [0.08715574274765817, 0, 0, 0.9961946980917455],
"translation": [0.3125, 1.375, 0],
"name": "Right Arm",
"children": [6, 7]
},
{ "translation": [0.3125, -1.375, 0], "name": "Left Arm", "mesh": 6 },
{ "translation": [0.3125, -1.375, 0], "name": "Left Arm Layer", "mesh": 7 },
{
"rotation": [-0.10452846326765346, 0, 0, 0.9945218953682733],
"translation": [-0.3125, 1.375, 0],
"name": "Left Arm",
"children": [9, 10]
},
{ "translation": [-0.11875, -0.75, 0], "name": "Right Leg", "mesh": 8 },
{ "translation": [-0.11875, -0.75, 0], "name": "Right Leg Layer", "mesh": 9 },
{
"rotation": [
-0.09583115474787583, -0.0016727390282702394, 0.01737205902003089, 0.9952445947048578
],
"translation": [0.11875, 0.75, 0],
"name": "Right Leg",
"children": [12, 13]
},
{ "translation": [0.11875, -0.75, 0], "name": "Left Leg", "mesh": 10 },
{ "translation": [0.11875, -0.75, 0], "name": "Left Leg Layer", "mesh": 11 },
{
"rotation": [
0.08714246850588939, -0.0015210774457754552, -0.017385994761764084, 0.9960429728140489
],
"translation": [-0.11875, 0.75, 0],
"name": "Left Leg",
"children": [15, 16]
},
{ "children": [2, 5, 8, 11, 14, 17] }
],
"bufferViews": [
{ "buffer": 0, "byteOffset": 0, "byteLength": 288, "target": 34962, "byteStride": 12 },
{ "buffer": 0, "byteOffset": 288, "byteLength": 288, "target": 34962, "byteStride": 12 },
{ "buffer": 0, "byteOffset": 576, "byteLength": 192, "target": 34962, "byteStride": 8 },
{ "buffer": 0, "byteOffset": 768, "byteLength": 72, "target": 34963 },
{ "buffer": 0, "byteOffset": 840, "byteLength": 288, "target": 34962, "byteStride": 12 },
{ "buffer": 0, "byteOffset": 1128, "byteLength": 288, "target": 34962, "byteStride": 12 },
{ "buffer": 0, "byteOffset": 1416, "byteLength": 192, "target": 34962, "byteStride": 8 },
{ "buffer": 0, "byteOffset": 1608, "byteLength": 72, "target": 34963 },
{ "buffer": 0, "byteOffset": 1680, "byteLength": 288, "target": 34962, "byteStride": 12 },
{ "buffer": 0, "byteOffset": 1968, "byteLength": 288, "target": 34962, "byteStride": 12 },
{ "buffer": 0, "byteOffset": 2256, "byteLength": 192, "target": 34962, "byteStride": 8 },
{ "buffer": 0, "byteOffset": 2448, "byteLength": 72, "target": 34963 },
{ "buffer": 0, "byteOffset": 2520, "byteLength": 288, "target": 34962, "byteStride": 12 },
{ "buffer": 0, "byteOffset": 2808, "byteLength": 288, "target": 34962, "byteStride": 12 },
{ "buffer": 0, "byteOffset": 3096, "byteLength": 192, "target": 34962, "byteStride": 8 },
{ "buffer": 0, "byteOffset": 3288, "byteLength": 72, "target": 34963 },
{ "buffer": 0, "byteOffset": 3360, "byteLength": 288, "target": 34962, "byteStride": 12 },
{ "buffer": 0, "byteOffset": 3648, "byteLength": 288, "target": 34962, "byteStride": 12 },
{ "buffer": 0, "byteOffset": 3936, "byteLength": 192, "target": 34962, "byteStride": 8 },
{ "buffer": 0, "byteOffset": 4128, "byteLength": 72, "target": 34963 },
{ "buffer": 0, "byteOffset": 4200, "byteLength": 288, "target": 34962, "byteStride": 12 },
{ "buffer": 0, "byteOffset": 4488, "byteLength": 288, "target": 34962, "byteStride": 12 },
{ "buffer": 0, "byteOffset": 4776, "byteLength": 192, "target": 34962, "byteStride": 8 },
{ "buffer": 0, "byteOffset": 4968, "byteLength": 72, "target": 34963 },
{ "buffer": 0, "byteOffset": 5040, "byteLength": 288, "target": 34962, "byteStride": 12 },
{ "buffer": 0, "byteOffset": 5328, "byteLength": 288, "target": 34962, "byteStride": 12 },
{ "buffer": 0, "byteOffset": 5616, "byteLength": 192, "target": 34962, "byteStride": 8 },
{ "buffer": 0, "byteOffset": 5808, "byteLength": 72, "target": 34963 },
{ "buffer": 0, "byteOffset": 5880, "byteLength": 288, "target": 34962, "byteStride": 12 },
{ "buffer": 0, "byteOffset": 6168, "byteLength": 288, "target": 34962, "byteStride": 12 },
{ "buffer": 0, "byteOffset": 6456, "byteLength": 192, "target": 34962, "byteStride": 8 },
{ "buffer": 0, "byteOffset": 6648, "byteLength": 72, "target": 34963 },
{ "buffer": 0, "byteOffset": 6720, "byteLength": 288, "target": 34962, "byteStride": 12 },
{ "buffer": 0, "byteOffset": 7008, "byteLength": 288, "target": 34962, "byteStride": 12 },
{ "buffer": 0, "byteOffset": 7296, "byteLength": 192, "target": 34962, "byteStride": 8 },
{ "buffer": 0, "byteOffset": 7488, "byteLength": 72, "target": 34963 },
{ "buffer": 0, "byteOffset": 7560, "byteLength": 288, "target": 34962, "byteStride": 12 },
{ "buffer": 0, "byteOffset": 7848, "byteLength": 288, "target": 34962, "byteStride": 12 },
{ "buffer": 0, "byteOffset": 8136, "byteLength": 192, "target": 34962, "byteStride": 8 },
{ "buffer": 0, "byteOffset": 8328, "byteLength": 72, "target": 34963 },
{ "buffer": 0, "byteOffset": 8400, "byteLength": 288, "target": 34962, "byteStride": 12 },
{ "buffer": 0, "byteOffset": 8688, "byteLength": 288, "target": 34962, "byteStride": 12 },
{ "buffer": 0, "byteOffset": 8976, "byteLength": 192, "target": 34962, "byteStride": 8 },
{ "buffer": 0, "byteOffset": 9168, "byteLength": 72, "target": 34963 },
{ "buffer": 0, "byteOffset": 9240, "byteLength": 288, "target": 34962, "byteStride": 12 },
{ "buffer": 0, "byteOffset": 9528, "byteLength": 288, "target": 34962, "byteStride": 12 },
{ "buffer": 0, "byteOffset": 9816, "byteLength": 192, "target": 34962, "byteStride": 8 },
{ "buffer": 0, "byteOffset": 10008, "byteLength": 72, "target": 34963 }
],
"buffers": [
{
"byteLength": 10080,
"uri": "data:application/octet-stream;base64,AACAPgAAAEAAAIA+AACAPgAAAEAAAIC+AACAPgAAwD8AAIA+AACAPgAAwD8AAIC+AACAvgAAAEAAAIC+AACAvgAAAEAAAIA+AACAvgAAwD8AAIC+AACAvgAAwD8AAIA+AACAvgAAAEAAAIC+AACAPgAAAEAAAIC+AACAvgAAAEAAAIA+AACAPgAAAEAAAIA+AACAvgAAwD8AAIA+AACAPgAAwD8AAIA+AACAvgAAwD8AAIC+AACAPgAAwD8AAIC+AACAvgAAAEAAAIA+AACAPgAAAEAAAIA+AACAvgAAwD8AAIA+AACAPgAAwD8AAIA+AACAPgAAAEAAAIC+AACAvgAAAEAAAIC+AACAPgAAwD8AAIC+AACAvgAAwD8AAIC+AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AACAOQBAAD4AgP89AEAAPgAAgDkAwH8+AID/PQDAfz4AIIA+AEAAPgDgvz4AQAA+ACCAPgDAfz4A4L8+AMB/PgDAfz4AgP89AEAAPgCA/z0AwH8+AACAOQBAAD4AAIA5AOC/PgAAgDkAIIA+AACAOQDgvz4AgP89ACCAPgCA/z0AIMA+AEAAPgDg/z4AQAA+ACDAPgDAfz4A4P8+AMB/PgBAAD4AQAA+AMB/PgBAAD4AQAA+AMB/PgDAfz4AwH8+AAACAAEAAgADAAEABAAGAAUABgAHAAUACAAKAAkACgALAAkADAAOAA0ADgAPAA0AEAASABEAEgATABEAFAAWABUAFgAXABUAAACQPgAAAkAAAJA+AACQPgAAAkAAAJC+AACQPgAAvD8AAJA+AACQPgAAvD8AAJC+AACQvgAAAkAAAJC+AACQvgAAAkAAAJA+AACQvgAAvD8AAJC+AACQvgAAvD8AAJA+AACQvgAAAkAAAJC+AACQPgAAAkAAAJC+AACQvgAAAkAAAJA+AACQPgAAAkAAAJA+AACQvgAAvD8AAJA+AACQPgAAvD8AAJA+AACQvgAAvD8AAJC+AACQPgAAvD8AAJC+AACQvgAAAkAAAJA+AACQPgAAAkAAAJA+AACQvgAAvD8AAJA+AACQPgAAvD8AAJA+AACQPgAAAkAAAJC+AACQvgAAAkAAAJC+AACQPgAAvD8AAJC+AACQvgAAvD8AAJC+AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/ABAAPwBAAD4A8B8/AEAAPgAQAD8AwH8+APAfPwDAfz4AEEA/AEAAPgDwXz8AQAA+ABBAPwDAfz4A8F8/AMB/PgDwPz8AgP89ABAgPwCA/z0A8D8/AACAOQAQID8AAIA5APBfPwAAgDkAEEA/AACAOQDwXz8AgP89ABBAPwCA/z0AEGA/AEAAPgDwfz8AQAA+ABBgPwDAfz4A8H8/AMB/PgAQID8AQAA+APA/PwBAAD4AECA/AMB/PgDwPz8AwH8+AAACAAEAAgADAAEABAAGAAUABgAHAAUACAAKAAkACgALAAkADAAOAA0ADgAPAA0AEAASABEAEgATABEAFAAWABUAFgAXABUAAACAPgAAwD8AAAA+AACAPgAAwD8AAAC+AACAPgAAQD8AAAA+AACAPgAAQD8AAAC+AACAvgAAwD8AAAC+AACAvgAAwD8AAAA+AACAvgAAQD8AAAC+AACAvgAAQD8AAAA+AACAvgAAwD8AAAC+AACAPgAAwD8AAAC+AACAvgAAwD8AAAA+AACAPgAAwD8AAAA+AACAvgAAQD8AAAA+AACAPgAAQD8AAAA+AACAvgAAQD8AAAC+AACAPgAAQD8AAAC+AACAvgAAwD8AAAA+AACAPgAAwD8AAAA+AACAvgAAQD8AAAA+AACAPgAAQD8AAAA+AACAPgAAwD8AAAC+AACAvgAAwD8AAAC+AACAPgAAQD8AAAC+AACAvgAAQD8AAAC+AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/ACCAPgAgoD4A4J8+ACCgPgAggD4A4P8+AOCfPgDg/z4AIOA+ACCgPgDg/z4AIKA+ACDgPgDg/z4A4P8+AOD/PgDg3z4A4J8+ACCgPgDgnz4A4N8+ACCAPgAgoD4AIIA+APAPPwAggD4AIOA+ACCAPgDwDz8A4J8+ACDgPgDgnz4AEAA/ACCgPgDwHz8AIKA+ABAAPwDg/z4A8B8/AOD/PgAgoD4AIKA+AODfPgAgoD4AIKA+AOD/PgDg3z4A4P8+AAACAAEAAgADAAEABAAGAAUABgAHAAUACAAKAAkACgALAAkADAAOAA0ADgAPAA0AEAASABEAEgATABEAFAAWABUAFgAXABUAAACIPgAAwj8AABA+AACIPgAAwj8AABC+AACIPgAAPD8AABA+AACIPgAAPD8AABC+AACIvgAAwj8AABC+AACIvgAAwj8AABA+AACIvgAAPD8AABC+AACIvgAAPD8AABA+AACIvgAAwj8AABC+AACIPgAAwj8AABC+AACIvgAAwj8AABA+AACIPgAAwj8AABA+AACIvgAAPD8AABA+AACIPgAAPD8AABA+AACIvgAAPD8AABC+AACIPgAAPD8AABC+AACIvgAAwj8AABA+AACIPgAAwj8AABA+AACIvgAAPD8AABA+AACIPgAAPD8AABA+AACIPgAAwj8AABC+AACIvgAAwj8AABC+AACIPgAAPD8AABC+AACIvgAAPD8AABC+AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/ACCAPgAQED8A4J8+ABAQPwAggD4A8D8/AOCfPgDwPz8AIOA+ABAQPwDg/z4AEBA/ACDgPgDwPz8A4P8+APA/PwDg3z4A8A8/ACCgPgDwDz8A4N8+ABAAPwAgoD4AEAA/APAPPwAQAD8AIOA+ABAAPwDwDz8A8A8/ACDgPgDwDz8AEAA/ABAQPwDwHz8AEBA/ABAAPwDwPz8A8B8/APA/PwAgoD4AEBA/AODfPgAQED8AIKA+APA/PwDg3z4A8D8/AAACAAEAAgADAAEABAAGAAUABgAHAAUACAAKAAkACgALAAkADAAOAA0ADgAPAA0AEAASABEAEgATABEAFAAWABUAFgAXABUAAADgPgAAwD8AAAA+AADgPgAAwD8AAAC+AADgPgAAQD8AAAA+AADgPgAAQD8AAAC+AACAPgAAwD8AAAC+AACAPgAAwD8AAAA+AACAPgAAQD8AAAC+AACAPgAAQD8AAAA+AACAPgAAwD8AAAC+AADgPgAAwD8AAAC+AACAPgAAwD8AAAA+AADgPgAAwD8AAAA+AACAPgAAQD8AAAA+AADgPgAAQD8AAAA+AACAPgAAQD8AAAC+AADgPgAAQD8AAAC+AACAPgAAwD8AAAA+AADgPgAAwD8AAAA+AACAPgAAQD8AAAA+AADgPgAAQD8AAAA+AADgPgAAwD8AAAC+AACAPgAAwD8AAAC+AADgPgAAQD8AAAC+AACAPgAAQD8AAAC+AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACALine truncated
}
],
"accessors": [
{
"bufferView": 0,
"componentType": 5126,
"count": 24,
"max": [0.25, 2, 0.25],
"min": [-0.25, 1.5, -0.25],
"type": "VEC3"
},
{
"bufferView": 1,
"componentType": 5126,
"count": 24,
"max": [1, 1, 1],
"min": [-1, -1, -1],
"type": "VEC3"
},
{
"bufferView": 2,
"componentType": 5126,
"count": 24,
"max": [0.499755859375, 0.249755859375],
"min": [0.000244140625, 0.000244140625],
"type": "VEC2"
},
{
"bufferView": 3,
"componentType": 5123,
"count": 36,
"max": [23],
"min": [0],
"type": "SCALAR"
},
{
"bufferView": 4,
"componentType": 5126,
"count": 24,
"max": [0.28125, 2.03125, 0.28125],
"min": [-0.28125, 1.46875, -0.28125],
"type": "VEC3"
},
{
"bufferView": 5,
"componentType": 5126,
"count": 24,
"max": [1, 1, 1],
"min": [-1, -1, -1],
"type": "VEC3"
},
{
"bufferView": 6,
"componentType": 5126,
"count": 24,
"max": [0.999755859375, 0.249755859375],
"min": [0.500244140625, 0.000244140625],
"type": "VEC2"
},
{
"bufferView": 7,
"componentType": 5123,
"count": 36,
"max": [23],
"min": [0],
"type": "SCALAR"
},
{
"bufferView": 8,
"componentType": 5126,
"count": 24,
"max": [0.25, 1.5, 0.125],
"min": [-0.25, 0.75, -0.125],
"type": "VEC3"
},
{
"bufferView": 9,
"componentType": 5126,
"count": 24,
"max": [1, 1, 1],
"min": [-1, -1, -1],
"type": "VEC3"
},
{
"bufferView": 10,
"componentType": 5126,
"count": 24,
"max": [0.624755859375, 0.499755859375],
"min": [0.250244140625, 0.250244140625],
"type": "VEC2"
},
{
"bufferView": 11,
"componentType": 5123,
"count": 36,
"max": [23],
"min": [0],
"type": "SCALAR"
},
{
"bufferView": 12,
"componentType": 5126,
"count": 24,
"max": [0.265625, 1.515625, 0.140625],
"min": [-0.265625, 0.734375, -0.140625],
"type": "VEC3"
},
{
"bufferView": 13,
"componentType": 5126,
"count": 24,
"max": [1, 1, 1],
"min": [-1, -1, -1],
"type": "VEC3"
},
{
"bufferView": 14,
"componentType": 5126,
"count": 24,
"max": [0.624755859375, 0.749755859375],
"min": [0.250244140625, 0.500244140625],
"type": "VEC2"
},
{
"bufferView": 15,
"componentType": 5123,
"count": 36,
"max": [23],
"min": [0],
"type": "SCALAR"
},
{
"bufferView": 16,
"componentType": 5126,
"count": 24,
"max": [0.4375, 1.5, 0.125],
"min": [0.25, 0.75, -0.125],
"type": "VEC3"
},
{
"bufferView": 17,
"componentType": 5126,
"count": 24,
"max": [1, 1, 1],
"min": [-1, -1, -1],
"type": "VEC3"
},
{
"bufferView": 18,
"componentType": 5126,
"count": 24,
"max": [0.843505859375, 0.499755859375],
"min": [0.625244140625, 0.250244140625],
"type": "VEC2"
},
{
"bufferView": 19,
"componentType": 5123,
"count": 36,
"max": [23],
"min": [0],
"type": "SCALAR"
},
{
"bufferView": 20,
"componentType": 5126,
"count": 24,
"max": [0.453125, 1.515625, 0.140625],
"min": [0.234375, 0.734375, -0.140625],
"type": "VEC3"
},
{
"bufferView": 21,
"componentType": 5126,
"count": 24,
"max": [1, 1, 1],
"min": [-1, -1, -1],
"type": "VEC3"
},
{
"bufferView": 22,
"componentType": 5126,
"count": 24,
"max": [0.843505859375, 0.749755859375],
"min": [0.625244140625, 0.500244140625],
"type": "VEC2"
},
{
"bufferView": 23,
"componentType": 5123,
"count": 36,
"max": [23],
"min": [0],
"type": "SCALAR"
},
{
"bufferView": 24,
"componentType": 5126,
"count": 24,
"max": [-0.25, 1.5, 0.125],
"min": [-0.4375, 0.75, -0.125],
"type": "VEC3"
},
{
"bufferView": 25,
"componentType": 5126,
"count": 24,
"max": [1, 1, 1],
"min": [-1, -1, -1],
"type": "VEC3"
},
{
"bufferView": 26,
"componentType": 5126,
"count": 24,
"max": [0.718505859375, 0.999755859375],
"min": [0.500244140625, 0.750244140625],
"type": "VEC2"
},
{
"bufferView": 27,
"componentType": 5123,
"count": 36,
"max": [23],
"min": [0],
"type": "SCALAR"
},
{
"bufferView": 28,
"componentType": 5126,
"count": 24,
"max": [-0.234375, 1.515625, 0.140625],
"min": [-0.453125, 0.734375, -0.140625],
"type": "VEC3"
},
{
"bufferView": 29,
"componentType": 5126,
"count": 24,
"max": [1, 1, 1],
"min": [-1, -1, -1],
"type": "VEC3"
},
{
"bufferView": 30,
"componentType": 5126,
"count": 24,
"max": [0.968505859375, 0.999755859375],
"min": [0.750244140625, 0.750244140625],
"type": "VEC2"
},
{
"bufferView": 31,
"componentType": 5123,
"count": 36,
"max": [23],
"min": [0],
"type": "SCALAR"
},
{
"bufferView": 32,
"componentType": 5126,
"count": 24,
"max": [0.24375000596046448, 0.75, 0.125],
"min": [-0.0062500000931322575, 0, -0.125],
"type": "VEC3"
},
{
"bufferView": 33,
"componentType": 5126,
"count": 24,
"max": [1, 1, 1],
"min": [-1, -1, -1],
"type": "VEC3"
},
{
"bufferView": 34,
"componentType": 5126,
"count": 24,
"max": [0.249755859375, 0.499755859375],
"min": [0.000244140625, 0.250244140625],
"type": "VEC2"
},
{
"bufferView": 35,
"componentType": 5123,
"count": 36,
"max": [23],
"min": [0],
"type": "SCALAR"
},
{
"bufferView": 36,
"componentType": 5126,
"count": 24,
"max": [0.2593750059604645, 0.765625, 0.140625],
"min": [-0.02187499962747097, -0.015625, -0.140625],
"type": "VEC3"
},
{
"bufferView": 37,
"componentType": 5126,
"count": 24,
"max": [1, 1, 1],
"min": [-1, -1, -1],
"type": "VEC3"
},
{
"bufferView": 38,
"componentType": 5126,
"count": 24,
"max": [0.249755859375, 0.749755859375],
"min": [0.000244140625, 0.500244140625],
"type": "VEC2"
},
{
"bufferView": 39,
"componentType": 5123,
"count": 36,
"max": [23],
"min": [0],
"type": "SCALAR"
},
{
"bufferView": 40,
"componentType": 5126,
"count": 24,
"max": [0.0062500000931322575, 0.75, 0.125],
"min": [-0.24375000596046448, 0, -0.125],
"type": "VEC3"
},
{
"bufferView": 41,
"componentType": 5126,
"count": 24,
"max": [1, 1, 1],
"min": [-1, -1, -1],
"type": "VEC3"
},
{
"bufferView": 42,
"componentType": 5126,
"count": 24,
"max": [0.499755859375, 0.999755859375],
"min": [0.250244140625, 0.750244140625],
"type": "VEC2"
},
{
"bufferView": 43,
"componentType": 5123,
"count": 36,
"max": [23],
"min": [0],
"type": "SCALAR"
},
{
"bufferView": 44,
"componentType": 5126,
"count": 24,
"max": [0.02187499962747097, 0.765625, 0.140625],
"min": [-0.2593750059604645, -0.015625, -0.140625],
"type": "VEC3"
},
{
"bufferView": 45,
"componentType": 5126,
"count": 24,
"max": [1, 1, 1],
"min": [-1, -1, -1],
"type": "VEC3"
},
{
"bufferView": 46,
"componentType": 5126,
"count": 24,
"max": [0.249755859375, 0.999755859375],
"min": [0.000244140625, 0.750244140625],
"type": "VEC2"
},
{
"bufferView": 47,
"componentType": 5123,
"count": 36,
"max": [23],
"min": [0],
"type": "SCALAR"
}
],
"materials": [
{
"pbrMetallicRoughness": {
"metallicFactor": 0,
"roughnessFactor": 1,
"baseColorTexture": { "index": 0 }
},
"alphaMode": "MASK",
"alphaCutoff": 0.05,
"doubleSided": true
}
],
"textures": [{ "sampler": 0, "source": 0 }],
"samplers": [{ "magFilter": 9728, "minFilter": 9728, "wrapS": 33071, "wrapT": 33071 }],
"images": [
{
"mimeType": "image/png",
"uri": "data:image/png;base64,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"
}
],
"meshes": [
{
"primitives": [
{
"mode": 4,
"attributes": { "POSITION": 0, "NORMAL": 1, "TEXCOORD_0": 2 },
"indices": 3,
"material": 0
}
]
},
{
"primitives": [
{
"mode": 4,
"attributes": { "POSITION": 4, "NORMAL": 5, "TEXCOORD_0": 6 },
"indices": 7,
"material": 0
}
]
},
{
"primitives": [
{
"mode": 4,
"attributes": { "POSITION": 8, "NORMAL": 9, "TEXCOORD_0": 10 },
"indices": 11,
"material": 0
}
]
},
{
"primitives": [
{
"mode": 4,
"attributes": { "POSITION": 12, "NORMAL": 13, "TEXCOORD_0": 14 },
"indices": 15,
"material": 0
}
]
},
{
"primitives": [
{
"mode": 4,
"attributes": { "POSITION": 16, "NORMAL": 17, "TEXCOORD_0": 18 },
"indices": 19,
"material": 0
}
]
},
{
"primitives": [
{
"mode": 4,
"attributes": { "POSITION": 20, "NORMAL": 21, "TEXCOORD_0": 22 },
"indices": 23,
"material": 0
}
]
},
{
"primitives": [
{
"mode": 4,
"attributes": { "POSITION": 24, "NORMAL": 25, "TEXCOORD_0": 26 },
"indices": 27,
"material": 0
}
]
},
{
"primitives": [
{
"mode": 4,
"attributes": { "POSITION": 28, "NORMAL": 29, "TEXCOORD_0": 30 },
"indices": 31,
"material": 0
}
]
},
{
"primitives": [
{
"mode": 4,
"attributes": { "POSITION": 32, "NORMAL": 33, "TEXCOORD_0": 34 },
"indices": 35,
"material": 0
}
]
},
{
"primitives": [
{
"mode": 4,
"attributes": { "POSITION": 36, "NORMAL": 37, "TEXCOORD_0": 38 },
"indices": 39,
"material": 0
}
]
},
{
"primitives": [
{
"mode": 4,
"attributes": { "POSITION": 40, "NORMAL": 41, "TEXCOORD_0": 42 },
"indices": 43,
"material": 0
}
]
},
{
"primitives": [
{
"mode": 4,
"attributes": { "POSITION": 44, "NORMAL": 45, "TEXCOORD_0": 46 },
"indices": 47,
"material": 0
}
]
}
]
}
{"asset":{"version":"2.0","generator":"Blockbench 4.12.4 glTF exporter"},"scenes":[{"nodes":[18],"name":"blockbench_export"}],"scene":0,"nodes":[{"translation":[0,-1.5,0],"name":"Head","mesh":0},{"translation":[0,-1.5,0],"name":"Hat Layer","mesh":1},{"rotation":[0.05228614400929896,-0.043559608510925024,0.002282862348496242,0.997679060715675],"translation":[0,1.5,0],"name":"Head","children":[0,1]},{"translation":[0,-1.5,0],"name":"Body","mesh":2},{"translation":[0,-1.5,0],"name":"Body Layer","mesh":3},{"translation":[0,1.5,0],"name":"Body","children":[3,4]},{"translation":[-0.3125,-1.375,0],"name":"Right Arm","mesh":4},{"translation":[-0.3125,-1.375,0],"name":"Right Arm Layer","mesh":5},{"rotation":[0.08715574274765817,0,0,0.9961946980917455],"translation":[0.3125,1.375,0],"name":"Right Arm","children":[6,7]},{"translation":[0.3125,-1.375,0],"name":"Left Arm","mesh":6},{"translation":[0.3125,-1.375,0],"name":"Left Arm Layer","mesh":7},{"rotation":[-0.10452846326765346,0,0,0.9945218953682733],"translation":[-0.3125,1.375,0],"name":"Left Arm","children":[9,10]},{"translation":[-0.11875,-0.75,0],"name":"Right Leg","mesh":8},{"translation":[-0.11875,-0.75,0],"name":"Right Leg Layer","mesh":9},{"rotation":[-0.09583115474787583,-0.0016727390282702394,0.01737205902003089,0.9952445947048578],"translation":[0.11875,0.75,0],"name":"Right Leg","children":[12,13]},{"translation":[0.11875,-0.75,0],"name":"Left Leg","mesh":10},{"translation":[0.11875,-0.75,0],"name":"Left Leg Layer","mesh":11},{"rotation":[0.08714246850588939,-0.0015210774457754552,-0.017385994761764084,0.9960429728140489],"translation":[-0.11875,0.75,0],"name":"Left Leg","children":[15,16]},{"children":[2,5,8,11,14,17]}],"bufferViews":[{"buffer":0,"byteOffset":0,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":288,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":576,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":768,"byteLength":72,"target":34963},{"buffer":0,"byteOffset":840,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":1128,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":1416,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":1608,"byteLength":72,"target":34963},{"buffer":0,"byteOffset":1680,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":1968,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":2256,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":2448,"byteLength":72,"target":34963},{"buffer":0,"byteOffset":2520,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":2808,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":3096,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":3288,"byteLength":72,"target":34963},{"buffer":0,"byteOffset":3360,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":3648,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":3936,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":4128,"byteLength":72,"target":34963},{"buffer":0,"byteOffset":4200,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":4488,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":4776,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":4968,"byteLength":72,"target":34963},{"buffer":0,"byteOffset":5040,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":5328,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":5616,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":5808,"byteLength":72,"target":34963},{"buffer":0,"byteOffset":5880,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":6168,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":6456,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":6648,"byteLength":72,"target":34963},{"buffer":0,"byteOffset":6720,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":7008,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":7296,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":7488,"byteLength":72,"target":34963},{"buffer":0,"byteOffset":7560,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":7848,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":8136,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":8328,"byteLength":72,"target":34963},{"buffer":0,"byteOffset":8400,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":8688,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":8976,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":9168,"byteLength":72,"target":34963},{"buffer"Line truncated
@@ -1,5 +1,5 @@
<template>
<ModalWrapper ref="modal" @on-modal-hide="resetState">
<ModalWrapper ref="modal" @on-hide="resetState">
<template #title>
<span class="text-lg font-extrabold text-contrast">
{{ mode === 'edit' ? 'Edit skin' : 'New skin' }}
@@ -12,10 +12,12 @@
<SkinPreviewRenderer
slim-model-src="/src/assets/models/slim_player.gltf"
wide-model-src="/src/assets/models/classic_player.gltf"
cape-model-src="/src/assets/models/cape.gltf"
:variant="variant"
:texture-src="previewSkin"
:scale="1.4"
:fov="50"
:cape-src="selectedCapeTexture"
:scale="1.4" :fov="50"
:initial-rotation="Math.PI / 8"
class="h-full w-full"
/>
</div>
@@ -35,19 +37,17 @@
<UploadIcon aria-hidden="true" />
</FileInput>
<div v-if="fileName || mode === 'new'" class="flex items-center gap-2">
<div class="flex items-center gap-2" v-if="fileName || mode === 'new'">
<InfoIcon v-if="!fileName" class="text-brand-blue" />
<CheckCircleIcon v-else class="text-brand-green" />
<small v-tooltip="fileName" class="truncate">{{
fileName || 'No skin uploaded yet.'
}}</small>
<small class="truncate" v-tooltip="fileName">{{ fileName || 'No skin uploaded yet.' }}</small>
</div>
</Card>
</section>
<section>
<h2 class="text-base font-semibold mb-2">Arm style</h2>
<RadioButtons v-model="variant" :items="['CLASSIC', 'SLIM']">
<RadioButtons v-model="variant" :items="['CLASSIC','SLIM']">
<template #default="{ item }">
{{ item === 'CLASSIC' ? 'Wide' : 'Slim' }}
</template>
@@ -57,21 +57,33 @@
<section>
<h2 class="text-base font-semibold mb-2">Cape</h2>
<div class="flex gap-2">
<CapeLikeTextButton
tooltip="No Cape"
:highlighted="!selectedCape"
@click="selectCape(undefined)"
>
<template #icon>
<XIcon />
</template>
<span>No Cape</span>
</CapeLikeTextButton>
<CapeButton
v-for="cape in firstThreeCapes"
:id="cape.id"
v-for="cape in visibleCapeList"
:key="cape.id"
:id="cape.id"
:texture="cape.texture"
:name="cape.name || 'Cape'"
:selected="selectedCape?.id === cape.id"
@select="selectCape(cape)"
/>
<CapeLikeTextButton
v-if="capes?.length ?? 0 > 3"
v-if="(capes?.length ?? 0) > 2"
tooltip="View more capes"
@click="viewMoreCapes"
@mouseup="openSelectCapeModal"
>
<template #icon><ChevronRightIcon /></template>
<template #icon><ChevronRightIcon/></template>
<span>More</span>
</CapeLikeTextButton>
</div>
@@ -81,62 +93,51 @@
<div class="flex gap-2 mt-6">
<ButtonStyled color="brand" :disabled="disableSave">
<Button
v-tooltip="disableSave && 'Upload a skin first!'"
:disabled="disableSave"
@click="save"
>
<Button @click="save" :disabled="disableSave" v-tooltip="disableSave && 'Upload a skin first!'">
<CheckIcon v-if="mode === 'new'" /><SaveIcon v-else />
{{ mode === 'new' ? 'Add skin' : 'Save skin' }}
</Button>
</ButtonStyled>
<Button @click="hide"><XIcon />Cancel</Button>
<ButtonStyled v-if="mode === 'edit'" color="red">
<Button @click="handleDelete"><TrashIcon />Delete</Button>
<Button @click="hide"><XIcon/>Cancel</Button>
<ButtonStyled color="red" v-if="mode==='edit'">
<Button @click="handleDelete"><TrashIcon/>Delete</Button>
</ButtonStyled>
</div>
</ModalWrapper>
<SelectCapeModal ref="selectCapeModal" :capes="capes || []" @select="handleCapeSelected" />
</template>
<script setup lang="ts">
import { ref, computed, watch, useTemplateRef } from 'vue'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import SelectCapeModal from '@/components/ui/skin/SelectCapeModal.vue'
import {
Card,
FileInput,
SkinPreviewRenderer,
Button,
RadioButtons,
CapeButton,
CapeLikeTextButton,
ButtonStyled,
Card, FileInput, SkinPreviewRenderer,
Button, RadioButtons, CapeButton, CapeLikeTextButton, ButtonStyled
} from '@modrinth/ui'
import {
add_and_equip_custom_skin,
remove_custom_skin,
equip_skin,
unequip_skin,
type Skin,
type Cape,
type SkinModel,
get_available_skins,
type Skin, type Cape, type SkinModel
} from '@/helpers/skins.ts'
import { handleError } from '@/store/notifications'
import {
CheckCircleIcon,
InfoIcon,
UploadIcon,
CheckIcon,
SaveIcon,
XIcon,
TrashIcon,
ChevronRightIcon,
CheckCircleIcon, InfoIcon, UploadIcon,
CheckIcon, SaveIcon, XIcon, TrashIcon, ChevronRightIcon
} from '@modrinth/assets'
const modal = useTemplateRef('modal')
const mode = ref<'new' | 'edit'>('new')
const currentSkin = ref<Skin | null>(null)
const selectCapeModal = useTemplateRef('selectCapeModal')
const mode = ref<'new'|'edit'>('new')
const currentSkin = ref<Skin|null>(null)
const shouldRestoreModal = ref(false)
const fileUploadTextureBlob = ref<Uint8Array | null>(null)
const fileName = ref<string | null>(null)
const fileUploadTextureBlob = ref<Uint8Array|null>(null)
const fileName = ref<string|null>(null)
watch(fileUploadTextureBlob, () => {
if (fileName.value === null && fileUploadTextureBlob.value) {
fileName.value = 'New upload'
@@ -144,11 +145,54 @@ watch(fileUploadTextureBlob, () => {
})
const variant = ref<SkinModel>('CLASSIC')
const selectedCape = ref<Cape | undefined>(undefined)
const selectedCape = ref<Cape|undefined>(undefined)
const props = defineProps<{ capes?: Cape[] }>()
const firstThreeCapes = computed(() => props.capes?.slice(0, 3) ?? [])
const localPreviewUrl = ref<string | null>(null)
const selectedCapeTexture = computed(() => selectedCape.value?.texture)
const visibleCapeList = ref<Cape[]>([])
function initVisibleCapeList() {
if (!props.capes || props.capes.length === 0) {
visibleCapeList.value = []
return
}
if (visibleCapeList.value.length === 0) {
if (selectedCape.value) {
const otherCape = getRandomCapeExcluding(selectedCape.value.id)
visibleCapeList.value = otherCape ? [selectedCape.value, otherCape] : [selectedCape.value]
} else {
visibleCapeList.value = getRandomCapes(2)
}
}
}
function getRandomCapes(count: number): Cape[] {
if (!props.capes || props.capes.length === 0) return []
const availableCapes = [...props.capes]
const result: Cape[] = []
for (let i = 0; i < count && availableCapes.length > 0; i++) {
const randomIndex = Math.floor(Math.random() * availableCapes.length)
const cape = availableCapes.splice(randomIndex, 1)[0]
result.push(cape)
}
return result
}
function getRandomCapeExcluding(excludeId: string): Cape | undefined {
if (!props.capes || props.capes.length <= 1) return undefined
const availableCapes = props.capes.filter(cape => cape.id !== excludeId)
if (availableCapes.length === 0) return undefined
const randomIndex = Math.floor(Math.random() * availableCapes.length)
return availableCapes[randomIndex]
}
const localPreviewUrl = ref<string|null>(null)
watch(fileUploadTextureBlob, (blob, prev) => {
if (prev && localPreviewUrl.value) URL.revokeObjectURL(localPreviewUrl.value)
if (blob) localPreviewUrl.value = URL.createObjectURL(new Blob([blob]))
@@ -160,7 +204,9 @@ const previewSkin = computed(() => {
return '/src/assets/skins/steve.png'
})
const disableSave = computed(() => mode.value === 'new' && !fileUploadTextureBlob.value)
const disableSave = computed(() =>
mode.value==='new' && !fileUploadTextureBlob.value
)
function resetState() {
mode.value = 'new'
@@ -169,6 +215,8 @@ function resetState() {
fileName.value = null
variant.value = 'CLASSIC'
selectedCape.value = undefined
visibleCapeList.value = []
shouldRestoreModal.value = false
if (localPreviewUrl.value) {
URL.revokeObjectURL(localPreviewUrl.value)
localPreviewUrl.value = null
@@ -180,11 +228,13 @@ function show(e: MouseEvent, skin?: Skin) {
currentSkin.value = skin ?? null
if (skin) {
variant.value = skin.variant
selectedCape.value = props.capes?.find((c) => c.id === skin.cape_id)
selectedCape.value = props.capes?.find(c=>c.id===skin.cape_id)
} else {
variant.value = 'CLASSIC'
selectedCape.value = undefined
}
visibleCapeList.value = []
initVisibleCapeList()
modal.value?.show(e)
}
@@ -193,7 +243,7 @@ function hide() {
resetState()
}
async function onTextureSelected(files: FileList | null) {
async function onTextureSelected(files: FileList|null) {
if (!files?.length) return
const file = files[0]
fileName.value = file.name
@@ -201,11 +251,44 @@ async function onTextureSelected(files: FileList | null) {
fileUploadTextureBlob.value = new Uint8Array(buf)
}
function selectCape(c: Cape | undefined) {
selectedCape.value = c
function changeVariant(v: SkinModel){ variant.value = v }
function selectCape(cape: Cape|undefined) {
if (cape && selectedCape.value?.id !== cape.id) {
const isInVisibleList = visibleCapeList.value.some(c => c.id === cape.id)
if (!isInVisibleList && visibleCapeList.value.length > 0) {
visibleCapeList.value.splice(0, 1, cape)
}
}
selectedCape.value = cape
}
function viewMoreCapes() {
// TODO: later
function handleCapeSelected(cape: Cape | undefined) {
selectCape(cape)
if (shouldRestoreModal.value) {
setTimeout(() => {
modal.value?.show()
shouldRestoreModal.value = false
}, 0)
}
}
function openSelectCapeModal(e: MouseEvent) {
if (!selectCapeModal.value) return
shouldRestoreModal.value = true
modal.value?.hide()
setTimeout(() => {
selectCapeModal.value?.show(
e,
currentSkin.value?.texture_key,
selectedCape.value,
previewSkin.value,
variant.value
)
}, 0)
}
async function handleDelete() {
@@ -233,7 +316,7 @@ async function save() {
blob = new Uint8Array(buf)
}
await unequip_skin()
await unequip_skin();
if (mode.value === 'new') {
await add_and_equip_custom_skin(blob, variant.value, selectedCape.value)
@@ -250,8 +333,13 @@ async function save() {
}
}
watch(() => props.capes, () => {
initVisibleCapeList()
}, { immediate: true })
const emit = defineEmits<{
(e: 'saved' | 'deleted'): void
(e:'saved'):void
(e:'deleted', skin: Skin):void
}>()
defineExpose({ show, hide })
@@ -63,7 +63,9 @@ defineExpose({
<template>
<ModalWrapper ref="modal">
<template #title>
<span class="text-lg font-extrabold text-contrast">Selecting a cape</span>
<div class="flex flex-col">
<span class="text-lg font-extrabold text-heading">Change cape</span>
</div>
</template>
<div class="flex flex-col md:flex-row gap-6">
<div class="max-h-[25rem] h-[25rem] w-[16rem] min-w-[16rem] overflow-hidden relative">
@@ -78,7 +80,7 @@ defineExpose({
:variant="currentSkinVariant"
:scale="1.4"
:fov="50"
:initial-rotation="180"
:initial-rotation="Math.PI + (Math.PI / 8)"
class="h-full w-full"
/>
</div>
+78 -36
View File
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { UpdatedIcon, PlusIcon } from '@modrinth/assets'
import {UpdatedIcon, PlusIcon, ExcitedRinthbot} from '@modrinth/assets'
import { ButtonStyled, SkinPreviewRenderer, SkinButton, SkinLikeTextButton } from '@modrinth/ui'
import { ref, computed, useTemplateRef, watch } from 'vue'
import { ref, computed, useTemplateRef, watch, onMounted, onUnmounted } from 'vue'
import EditSkinModal from '@/components/ui/skin/EditSkinModal.vue'
import SelectCapeModal from '@/components/ui/skin/SelectCapeModal.vue'
import { handleError } from '@/store/notifications'
@@ -18,6 +18,7 @@ import type { Cape, Skin } from '@/helpers/skins.ts'
import { get_default_user, users } from '@/helpers/auth'
import type { RenderResult } from '@/helpers/rendering/batchSkinRenderer.ts'
import { generateSkinPreviews, map } from '@/helpers/rendering/batchSkinRenderer.ts'
import { WavingRinthbot } from '@modrinth/assets'
const editSkinModal = useTemplateRef('editSkinModal')
const selectCapeModal = useTemplateRef('selectCapeModal')
@@ -25,17 +26,14 @@ const selectCapeModal = useTemplateRef('selectCapeModal')
const settings = ref(await getSettings())
const skins = ref<Skin[]>([])
const capes = ref<Cape[]>([])
const username = ref<string | undefined>(undefined)
const currentUser = ref(undefined)
const currentUserId = ref<string | undefined>(undefined)
// @ts-ignore
const username = computed(() => currentUser.value?.profile?.name ?? undefined)
const selectedSkin = ref<Skin | null>(null)
const defaultCape = ref<Cape>()
const forceSkinRefreshKey = ref(0)
const previewSkin = computed(() =>
selectedSkin.value ? `https://vzge.me/processedskin/${selectedSkin.value.texture_key}.png` : '',
)
const savedSkins = computed(() => filterSavedSkins(skins.value))
const defaultSkins = computed(() => filterDefaultSkins(skins.value))
@@ -46,11 +44,39 @@ const currentCape = computed(() => {
return overrideCape
}
}
return defaultCape.value
})
await Promise.all([loadCapes(), loadSkins(), loadUsername()])
const skinTexture = computed(() => selectedSkin.value?.texture ?? '')
const capeTexture = computed(() => currentCape.value?.texture)
const skinVariant = computed(() => selectedSkin.value?.variant)
const skinNametag = computed(() => settings.value.hide_nametag_skins_page ? undefined : username.value)
await Promise.all([loadCapes(), loadSkins(), loadCurrentUser()])
let userCheckInterval: number | null = null
onMounted(() => {
// TODO: Surely a better way to do this?
userCheckInterval = window.setInterval(checkUserChanges, 250)
})
onUnmounted(() => {
if (userCheckInterval !== null) {
window.clearInterval(userCheckInterval)
}
})
async function checkUserChanges() {
try {
const defaultId = await get_default_user()
if (defaultId !== currentUserId.value) {
await Promise.all([loadCapes(), loadSkins(), loadCurrentUser()])
}
} catch (e) {
handleError(e)
}
}
async function loadCapes() {
capes.value = (await get_available_capes().catch(handleError)) ?? []
@@ -60,13 +86,7 @@ async function loadCapes() {
async function loadSkins() {
skins.value = (await get_available_skins().catch(handleError)) ?? []
generateSkinPreviews(skins.value, capes.value)
const previousSkinId = selectedSkin.value?.texture_key
selectedSkin.value = skins.value.find((s) => s.is_equipped) ?? null
if (previousSkinId && selectedSkin.value?.texture_key === previousSkinId) {
forceSkinRefreshKey.value++
}
}
async function changeSkin(newSkin: Skin) {
@@ -84,15 +104,18 @@ async function onSkinSaved() {
await Promise.all([loadCapes(), loadSkins()])
}
async function loadUsername() {
async function loadCurrentUser() {
try {
const defaultId = await get_default_user()
currentUserId.value = defaultId
const allAccounts = await users()
const current = allAccounts.find((acc) => acc.profile.id === defaultId)
username.value = current?.profile?.name ?? undefined
// @ts-ignore
currentUser.value = allAccounts.find((acc) => acc.profile.id === defaultId)
} catch (e) {
handleError(e)
username.value = undefined
currentUser.value = undefined
currentUserId.value = undefined
}
}
@@ -103,9 +126,7 @@ function getBakedSkinTextures(skin: Skin): RenderResult | undefined {
watch(
() => selectedSkin.value?.cape_id,
() => {
forceSkinRefreshKey.value++
},
() => {}
)
</script>
@@ -117,21 +138,28 @@ watch(
@deleted="() => loadSkins()"
/>
<SelectCapeModal ref="selectCapeModal" :capes="capes" @select="handleCapeSelected" />
<div class="p-6 grid grid-cols-[300px_1fr] xl:grid-cols-[3fr_5fr] gap-6">
<div v-if="currentUser" class="p-6 grid grid-cols-[300px_1fr] xl:grid-cols-[3fr_5fr] gap-6">
<div class="sticky top-6 self-start">
<div class="flex justify-between gap-4">
<h1 class="m-0 text-2xl font-bold">Skins</h1>
<ButtonStyled :disabled="!!selectedSkin?.cape_id">
<button
v-tooltip="
selectedSkin?.cape_id
? 'The equipped skin is overriding the default cape.'
: undefined
"
selectedSkin?.cape_id
? 'The equipped skin is overriding the default cape.'
: undefined
"
:disabled="!!selectedSkin?.cape_id"
@click="
(e: MouseEvent) => selectCapeModal?.show(e, selectedSkin?.texture_key, currentCape)
"
(e: MouseEvent) => selectCapeModal?.show(
e,
selectedSkin?.texture_key,
currentCape,
skinTexture,
skinVariant
)
"
>
<UpdatedIcon />
Change cape
@@ -140,14 +168,14 @@ watch(
</div>
<div class="h-[80vh] flex items-center justify-center">
<SkinPreviewRenderer
:key="forceSkinRefreshKey"
wide-model-src="/src/assets/models/classic_player.gltf"
slim-model-src="/src/assets/models/slim_player.gltf"
cape-model-src="/src/assets/models/cape.gltf"
:cape-src="currentCape?.texture"
:texture-src="previewSkin"
:variant="selectedSkin?.variant"
:nametag="settings.hide_nametag_skins_page ? undefined : username"
:cape-src="capeTexture"
:texture-src="skinTexture"
:variant="skinVariant"
:nametag="skinNametag"
:initial-rotation="Math.PI / 8"
/>
</div>
</div>
@@ -199,4 +227,18 @@ watch(
</section>
</div>
</div>
<div v-else class="flex items-center justify-center min-h-[50vh] p-6 pt-28">
<div class="bg-bg-raised rounded-lg p-7 flex flex-col gap-5 shadow-md relative max-w-xl w-full mx-auto">
<img :src="ExcitedRinthbot" alt="Excited Modrinth Bot" class="absolute -top-28 right-8 md:right-20 h-28 w-auto" />
<div class="absolute top-0 left-0 w-full h-[1px] opacity-40 bg-gradient-to-r from-transparent via-green-500 to-transparent" style="background: linear-gradient(to right, transparent 2rem, var(--color-green) calc(100% - 13rem), var(--color-green) calc(100% - 5rem), transparent calc(100% - 2rem))"></div>
<div class="flex flex-col gap-5">
<h1 class="text-3xl font-extrabold m-0">Login Required</h1>
<p class="text-lg m-0">
Please log into your account to use the skin management features of the Modrinth app.
</p>
</div>
</div>
</div>
</template>
@@ -13,14 +13,14 @@
shadows
alpha
:antialias="antialias"
:renderer-options="{
outputColorSpace: THREE.SRGBColorSpace,
toneMapping: THREE.NoToneMapping
}"
@pointerdown="onPointerDown"
@pointermove="onPointerMove"
@pointerup="onPointerUp"
@pointerleave="onPointerUp"
:rendererOptions="{
outputColorSpace: THREE.SRGBColorSpace,
toneMapping: THREE.NoToneMapping
}"
>
<Suspense>
<Group>
@@ -55,7 +55,7 @@
</Suspense>
<TresPerspectiveCamera
:make-default="true"
:makeDefault="true"
:fov="fov"
:position="[0, 1.5, -3.25]"
:look-at="target"
@@ -70,7 +70,7 @@
import * as THREE from 'three'
import { useGLTF } from '@tresjs/cientos'
import { useTexture, TresCanvas } from '@tresjs/core'
import { shallowRef, ref, computed, watch, markRaw } from 'vue'
import { shallowRef, ref, computed, watch, markRaw, onBeforeMount } from 'vue'
const props = withDefaults(
defineProps<{
@@ -106,11 +106,33 @@ const capeScene = shallowRef<THREE.Object3D | null>(null)
const bodyNode = shallowRef<THREE.Object3D | null>(null)
const capeAttached = ref(false)
const lastCapeSrc = ref<string | undefined>(undefined)
const texture = shallowRef<THREE.Texture | null>(null)
const capeTexture = shallowRef<THREE.Texture | null>(null)
const transparentTexture = createTransparentTexture()
function createTransparentTexture(): THREE.Texture {
const canvas = document.createElement('canvas')
canvas.width = canvas.height = 1
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D
ctx.clearRect(0, 0, 1, 1)
const texture = new THREE.CanvasTexture(canvas)
texture.needsUpdate = true
texture.colorSpace = THREE.SRGBColorSpace
texture.flipY = false
texture.magFilter = THREE.NearestFilter
texture.minFilter = THREE.NearestFilter
return texture
}
async function loadModel(src: string) {
const { scene: loadedScene } = await useGLTF(src)
scene.value = markRaw(loadedScene)
applyTextureToScene(scene.value, texture.value)
if (texture.value) {
applyTextureToScene(scene.value, texture.value)
}
bodyNode.value = null
loadedScene.traverse(node => {
@@ -121,7 +143,7 @@ async function loadModel(src: string) {
capeAttached.value = false
if (capeScene.value && bodyNode.value && props.capeSrc) {
if (capeScene.value && bodyNode.value) {
attachCapeToBody()
}
@@ -137,14 +159,45 @@ async function loadCape(src: string) {
const { scene: loadedCape } = await useGLTF(src)
capeScene.value = markRaw(loadedCape)
if (props.capeSrc) {
const tex = await useTexture([props.capeSrc])
capeTexture.value = tex
applyCapeTexture(capeScene.value, tex)
applyCapeTexture(capeScene.value, capeTexture.value)
if (bodyNode.value && !capeAttached.value) {
attachCapeToBody()
}
}
async function loadAndApplyTexture(src: string) {
if (!src) return null
const tex = await useTexture([src])
tex.colorSpace = THREE.SRGBColorSpace
tex.flipY = false
tex.magFilter = THREE.NearestFilter
tex.minFilter = THREE.NearestFilter
return tex
}
async function loadAndApplyCapeTexture(src: string | undefined) {
if (src === lastCapeSrc.value) return
lastCapeSrc.value = src
if (src) {
capeTexture.value = await loadAndApplyTexture(src)
} else {
capeTexture.value = null
}
if (bodyNode.value && capeScene.value && !capeAttached.value) {
attachCapeToBody()
if (capeScene.value) {
applyCapeTexture(capeScene.value, capeTexture.value)
}
if (capeScene.value && bodyNode.value) {
if (!src && capeAttached.value && capeScene.value.parent) {
capeScene.value.parent.remove(capeScene.value)
capeAttached.value = false
} else if (src && !capeAttached.value) {
attachCapeToBody()
}
}
}
@@ -162,106 +215,49 @@ function attachCapeToBody() {
capeAttached.value = true
}
watch(selectedModelSrc, src => loadModel(src), { immediate: true })
function applyTextureToScene(root: THREE.Object3D | null, tex: THREE.Texture | null) {
if (!root || !tex) return
watch(
() => props.capeModelSrc,
src => src && loadCape(src),
{ immediate: true }
)
const texture = ref<THREE.Texture>(await useTexture([props.textureSrc]))
const capeTexture = shallowRef<THREE.Texture | null>(null)
watch(
() => props.textureSrc,
async newSrc => {
texture.value = await useTexture([newSrc])
applyTextureToScene(scene.value, texture.value)
}
)
watch(
() => props.capeSrc,
async newCapeSrc => {
if (newCapeSrc === lastCapeSrc.value) return;
lastCapeSrc.value = newCapeSrc;
if (newCapeSrc) {
const newTexture = await useTexture([newCapeSrc]);
capeTexture.value = newTexture;
if (capeScene.value && bodyNode.value && !capeAttached.value) {
attachCapeToBody();
}
if (capeScene.value) {
applyCapeTexture(capeScene.value, newTexture);
}
} else {
capeTexture.value = null;
if (capeAttached.value && capeScene.value && capeScene.value.parent) {
capeScene.value.parent.remove(capeScene.value);
capeAttached.value = false;
}
}
},
{ immediate: true }
)
function applyTextureToScene(root: THREE.Object3D | null, tex: THREE.Texture) {
if (!root) return
tex.colorSpace = THREE.SRGBColorSpace
tex.flipY = false
tex.magFilter = THREE.NearestFilter
tex.minFilter = THREE.NearestFilter
root.traverse(child => {
if ((child as THREE.Mesh).isMesh) {
const mesh = child as THREE.Mesh
if (mesh.name == "Cape") return;
if (mesh.name === "Cape") return
const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material]
// @ts-ignore
materials.forEach((mat: THREE.MeshStandardMaterial) => {
mat.map = tex
mat.metalness = 0
mat.color.set(0xffffff)
mat.toneMapped = false
mat.roughness = 1
mat.needsUpdate = true
materials.forEach((mat, index, array) => {
const standardMat = mat as THREE.MeshStandardMaterial
standardMat.map = tex
standardMat.metalness = 0
standardMat.color.set(0xffffff)
standardMat.toneMapped = false
standardMat.roughness = 1
standardMat.needsUpdate = true
})
}
})
}
function applyCapeTexture(root: THREE.Object3D | null, tex: THREE.Texture | null) {
if (!root || !tex) return
// Force texture settings
tex.colorSpace = THREE.SRGBColorSpace
tex.flipY = false
tex.magFilter = THREE.NearestFilter
tex.minFilter = THREE.NearestFilter
tex.needsUpdate = true
if (!root) return
root.traverse(child => {
if ((child as THREE.Mesh).isMesh) {
const mesh = child as THREE.Mesh
const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material]
// @ts-ignore
materials.forEach((mat: THREE.MeshStandardMaterial) => {
mat.map = tex
mat.metalness = 0
mat.color.set(0xffffff)
mat.toneMapped = false
mat.roughness = 1
mat.side = THREE.DoubleSide
mat.needsUpdate = true
materials.forEach((mat, index, array) => {
const standardMat = mat as THREE.MeshStandardMaterial
standardMat.map = tex || transparentTexture
standardMat.transparent = true
standardMat.metalness = 0
standardMat.color.set(0xffffff)
standardMat.toneMapped = false
standardMat.roughness = 1
standardMat.side = THREE.DoubleSide
standardMat.needsUpdate = true
})
}
})
@@ -320,9 +316,29 @@ function createRadialTexture(size: number): THREE.CanvasTexture {
return new THREE.CanvasTexture(canvas)
}
applyTextureToScene(scene.value, texture.value)
loadModel(selectedModelSrc.value)
if (props.capeModelSrc) {
loadCape(props.capeModelSrc)
}
watch(selectedModelSrc, src => loadModel(src))
watch(() => props.capeModelSrc, src => src && loadCape(src))
watch(() => props.textureSrc, async newSrc => {
texture.value = await loadAndApplyTexture(newSrc)
if (scene.value && texture.value) {
applyTextureToScene(scene.value, texture.value)
}
})
watch(() => props.capeSrc, async newCapeSrc => {
await loadAndApplyCapeTexture(newCapeSrc)
})
onBeforeMount(async () => {
texture.value = await loadAndApplyTexture(props.textureSrc)
await loadModel(selectedModelSrc.value)
if (props.capeSrc) {
await loadAndApplyCapeTexture(props.capeSrc)
}
if (props.capeModelSrc) {
await loadCape(props.capeModelSrc)
}
})
</script>
-3
View File
@@ -95,9 +95,6 @@ importers:
posthog-js:
specifier: ^1.158.2
version: 1.158.2
three:
specifier: ^0.172.0
version: 0.172.0
vite-svg-loader:
specifier: ^5.1.0
version: 5.1.0(vue@3.5.13(typescript@5.5.4))