add URL param for linking to skins

This commit is contained in:
Brian Beck 2025-10-12 18:10:05 -07:00
parent 248b969d26
commit 96d1bc66e1
33 changed files with 245 additions and 109 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
self.__BUILD_MANIFEST=function(s){return{__rewrites:{afterFiles:[],beforeFiles:[],fallback:[]},"/":[s,"static/chunks/e21e5bbe-b28e0079b469d4e8.js","static/chunks/ebc70433-4eccd1cb3af29a3e.js","static/chunks/6eb5140f-31a2b2da7903b885.js","static/chunks/5d416436-3c60fd013e24a5af.js","static/chunks/85d7bc83-1ca530d7d3f44153.js","static/chunks/3a17f596-9aeae038dfa51955.js","static/chunks/f580fadb-2911e2fbf64aae5a.js","static/chunks/515-13ff0773d41722ae.js","static/chunks/pages/index-28c32b1427b3bfd0.js"],"/_error":["static/chunks/pages/_error-54b9fcf45cb5bc62.js"],"/gallery":[s,"static/chunks/737a5600-aea383aaa2061cc6.js","static/chunks/918-3c6747f76df39072.js","static/css/922e89893536f2f9.css","static/chunks/pages/gallery-af1406fdc1af13f5.js"],sortedPages:["/","/_app","/_error","/gallery"]}}("static/chunks/cb355538-e538db8a1761f402.js"),self.__BUILD_MANIFEST_CB&&self.__BUILD_MANIFEST_CB();

View file

@ -0,0 +1 @@
self.__BUILD_MANIFEST=function(s){return{__rewrites:{afterFiles:[],beforeFiles:[],fallback:[]},"/":[s,"static/chunks/e21e5bbe-b28e0079b469d4e8.js","static/chunks/ebc70433-4eccd1cb3af29a3e.js","static/chunks/6eb5140f-31a2b2da7903b885.js","static/chunks/5d416436-3c60fd013e24a5af.js","static/chunks/85d7bc83-1ca530d7d3f44153.js","static/chunks/3a17f596-9aeae038dfa51955.js","static/chunks/f580fadb-2911e2fbf64aae5a.js","static/chunks/51-4264bf751d1f46b3.js","static/chunks/pages/index-f0171d44afbb06ea.js"],"/_error":["static/chunks/pages/_error-54b9fcf45cb5bc62.js"],"/gallery":[s,"static/chunks/737a5600-aea383aaa2061cc6.js","static/chunks/918-3c6747f76df39072.js","static/css/922e89893536f2f9.css","static/chunks/pages/gallery-eae2514110a1c5b0.js"],sortedPages:["/","/_app","/_error","/gallery"]}}("static/chunks/cb355538-e538db8a1761f402.js"),self.__BUILD_MANIFEST_CB&&self.__BUILD_MANIFEST_CB();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,2 +1,2 @@
!function(){"use strict";var e,r,_,t,n,i,u,c={},o={};function __webpack_require__(e){var r=o[e];if(void 0!==r)return r.exports;var _=o[e]={id:e,loaded:!1,exports:{}},t=!0;try{c[e].call(_.exports,_,_.exports,__webpack_require__),t=!1}finally{t&&delete o[e]}return _.loaded=!0,_.exports}__webpack_require__.m=c,e=[],__webpack_require__.O=function(r,_,t,n){if(_){n=n||0;for(var i=e.length;i>0&&e[i-1][2]>n;i--)e[i]=e[i-1];e[i]=[_,t,n];return}for(var u=1/0,i=0;i<e.length;i++){for(var _=e[i][0],t=e[i][1],n=e[i][2],c=!0,o=0;o<_.length;o++)u>=n&&Object.keys(__webpack_require__.O).every(function(e){return __webpack_require__.O[e](_[o])})?_.splice(o--,1):(c=!1,n<u&&(u=n));if(c){e.splice(i--,1);var a=t()}}return a},__webpack_require__.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return __webpack_require__.d(r,{a:r}),r},__webpack_require__.d=function(e,r){for(var _ in r)__webpack_require__.o(r,_)&&!__webpack_require__.o(e,_)&&Object.defineProperty(e,_,{enumerable:!0,get:r[_]})},__webpack_require__.f={},__webpack_require__.e=function(e){return Promise.all(Object.keys(__webpack_require__.f).reduce(function(r,_){return __webpack_require__.f[_](e,r),r},[]))},__webpack_require__.u=function(e){return"static/chunks/"+(737===e?"fb7d5399":e)+"."+({250:"10c119307e239c98",737:"bc4a70b34221e8c8",767:"0dd6b240996f3455",848:"fc0fe21cdc2e6431"})[e]+".js"},__webpack_require__.miniCssF=function(e){return"static/css/"+({214:"922e89893536f2f9",888:"2bfb0b67e396f94e"})[e]+".css"},__webpack_require__.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||Function("return this")()}catch(e){if("object"==typeof window)return window}}(),__webpack_require__.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},r={},_="_N_E:",__webpack_require__.l=function(e,t,n,i){if(r[e]){r[e].push(t);return}if(void 0!==n)for(var u,c,o=document.getElementsByTagName("script"),a=0;a<o.length;a++){var p=o[a];if(p.getAttribute("src")==e||p.getAttribute("data-webpack")==_+n){u=p;break}}u||(c=!0,(u=document.createElement("script")).charset="utf-8",u.timeout=120,__webpack_require__.nc&&u.setAttribute("nonce",__webpack_require__.nc),u.setAttribute("data-webpack",_+n),u.src=__webpack_require__.tu(e)),r[e]=[t];var onScriptComplete=function(_,t){u.onerror=u.onload=null,clearTimeout(f);var n=r[e];if(delete r[e],u.parentNode&&u.parentNode.removeChild(u),n&&n.forEach(function(e){return e(t)}),_)return _(t)},f=setTimeout(onScriptComplete.bind(null,void 0,{type:"timeout",target:u}),12e4);u.onerror=onScriptComplete.bind(null,u.onerror),u.onload=onScriptComplete.bind(null,u.onload),c&&document.head.appendChild(u)},__webpack_require__.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},__webpack_require__.nmd=function(e){return e.paths=[],e.children||(e.children=[]),e},__webpack_require__.tt=function(){return void 0===t&&(t={createScriptURL:function(e){return e}},"undefined"!=typeof trustedTypes&&trustedTypes.createPolicy&&(t=trustedTypes.createPolicy("nextjs#bundler",t))),t},__webpack_require__.tu=function(e){return __webpack_require__.tt().createScriptURL(e)},__webpack_require__.p="/t2-model-skinner/_next/",n={272:0},__webpack_require__.f.j=function(e,r){var _=__webpack_require__.o(n,e)?n[e]:void 0;if(0!==_){if(_)r.push(_[2]);else if(272!=e){var t=new Promise(function(r,t){_=n[e]=[r,t]});r.push(_[2]=t);var i=__webpack_require__.p+__webpack_require__.u(e),u=Error();__webpack_require__.l(i,function(r){if(__webpack_require__.o(n,e)&&(0!==(_=n[e])&&(n[e]=void 0),_)){var t=r&&("load"===r.type?"missing":r.type),i=r&&r.target&&r.target.src;u.message="Loading chunk "+e+" failed.\n("+t+": "+i+")",u.name="ChunkLoadError",u.type=t,u.request=i,_[1](u)}},"chunk-"+e,e)}else n[e]=0}},__webpack_require__.O.j=function(e){return 0===n[e]},i=function(e,r){var _,t,i=r[0],u=r[1],c=r[2],o=0;if(i.some(function(e){return 0!==n[e]})){for(_ in u)__webpack_require__.o(u,_)&&(__webpack_require__.m[_]=u[_]);if(c)var a=c(__webpack_require__)}for(e&&e(r);o<i.length;o++)t=i[o],__webpack_require__.o(n,t)&&n[t]&&n[t][0](),n[t]=0;return __webpack_require__.O(a)},(u=self.webpackChunk_N_E=self.webpackChunk_N_E||[]).forEach(i.bind(null,0)),u.push=i.bind(null,u.push.bind(u))}();
//# sourceMappingURL=webpack-6a8605187f718663.js.map
!function(){"use strict";var e,r,_,t,n,i,u,c={},o={};function __webpack_require__(e){var r=o[e];if(void 0!==r)return r.exports;var _=o[e]={id:e,loaded:!1,exports:{}},t=!0;try{c[e].call(_.exports,_,_.exports,__webpack_require__),t=!1}finally{t&&delete o[e]}return _.loaded=!0,_.exports}__webpack_require__.m=c,e=[],__webpack_require__.O=function(r,_,t,n){if(_){n=n||0;for(var i=e.length;i>0&&e[i-1][2]>n;i--)e[i]=e[i-1];e[i]=[_,t,n];return}for(var u=1/0,i=0;i<e.length;i++){for(var _=e[i][0],t=e[i][1],n=e[i][2],c=!0,o=0;o<_.length;o++)u>=n&&Object.keys(__webpack_require__.O).every(function(e){return __webpack_require__.O[e](_[o])})?_.splice(o--,1):(c=!1,n<u&&(u=n));if(c){e.splice(i--,1);var a=t()}}return a},__webpack_require__.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return __webpack_require__.d(r,{a:r}),r},__webpack_require__.d=function(e,r){for(var _ in r)__webpack_require__.o(r,_)&&!__webpack_require__.o(e,_)&&Object.defineProperty(e,_,{enumerable:!0,get:r[_]})},__webpack_require__.f={},__webpack_require__.e=function(e){return Promise.all(Object.keys(__webpack_require__.f).reduce(function(r,_){return __webpack_require__.f[_](e,r),r},[]))},__webpack_require__.u=function(e){return"static/chunks/"+(737===e?"fb7d5399":e)+"."+({250:"10c119307e239c98",737:"bc4a70b34221e8c8",767:"0dd6b240996f3455",848:"fc0fe21cdc2e6431"})[e]+".js"},__webpack_require__.miniCssF=function(e){return"static/css/"+({214:"922e89893536f2f9",888:"811f0a5e25f9ca76"})[e]+".css"},__webpack_require__.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||Function("return this")()}catch(e){if("object"==typeof window)return window}}(),__webpack_require__.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},r={},_="_N_E:",__webpack_require__.l=function(e,t,n,i){if(r[e]){r[e].push(t);return}if(void 0!==n)for(var u,c,o=document.getElementsByTagName("script"),a=0;a<o.length;a++){var p=o[a];if(p.getAttribute("src")==e||p.getAttribute("data-webpack")==_+n){u=p;break}}u||(c=!0,(u=document.createElement("script")).charset="utf-8",u.timeout=120,__webpack_require__.nc&&u.setAttribute("nonce",__webpack_require__.nc),u.setAttribute("data-webpack",_+n),u.src=__webpack_require__.tu(e)),r[e]=[t];var onScriptComplete=function(_,t){u.onerror=u.onload=null,clearTimeout(f);var n=r[e];if(delete r[e],u.parentNode&&u.parentNode.removeChild(u),n&&n.forEach(function(e){return e(t)}),_)return _(t)},f=setTimeout(onScriptComplete.bind(null,void 0,{type:"timeout",target:u}),12e4);u.onerror=onScriptComplete.bind(null,u.onerror),u.onload=onScriptComplete.bind(null,u.onload),c&&document.head.appendChild(u)},__webpack_require__.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},__webpack_require__.nmd=function(e){return e.paths=[],e.children||(e.children=[]),e},__webpack_require__.tt=function(){return void 0===t&&(t={createScriptURL:function(e){return e}},"undefined"!=typeof trustedTypes&&trustedTypes.createPolicy&&(t=trustedTypes.createPolicy("nextjs#bundler",t))),t},__webpack_require__.tu=function(e){return __webpack_require__.tt().createScriptURL(e)},__webpack_require__.p="/t2-model-skinner/_next/",n={272:0},__webpack_require__.f.j=function(e,r){var _=__webpack_require__.o(n,e)?n[e]:void 0;if(0!==_){if(_)r.push(_[2]);else if(272!=e){var t=new Promise(function(r,t){_=n[e]=[r,t]});r.push(_[2]=t);var i=__webpack_require__.p+__webpack_require__.u(e),u=Error();__webpack_require__.l(i,function(r){if(__webpack_require__.o(n,e)&&(0!==(_=n[e])&&(n[e]=void 0),_)){var t=r&&("load"===r.type?"missing":r.type),i=r&&r.target&&r.target.src;u.message="Loading chunk "+e+" failed.\n("+t+": "+i+")",u.name="ChunkLoadError",u.type=t,u.request=i,_[1](u)}},"chunk-"+e,e)}else n[e]=0}},__webpack_require__.O.j=function(e){return 0===n[e]},i=function(e,r){var _,t,i=r[0],u=r[1],c=r[2],o=0;if(i.some(function(e){return 0!==n[e]})){for(_ in u)__webpack_require__.o(u,_)&&(__webpack_require__.m[_]=u[_]);if(c)var a=c(__webpack_require__)}for(e&&e(r);o<i.length;o++)t=i[o],__webpack_require__.o(n,t)&&n[t]&&n[t][0](),n[t]=0;return __webpack_require__.O(a)},(u=self.webpackChunk_N_E=self.webpackChunk_N_E||[]).forEach(i.bind(null,0)),u.push=i.bind(null,u.push.bind(u))}();
//# sourceMappingURL=webpack-8dba4ecbbea48736.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -10,6 +10,8 @@ export default function CanvasToggle() {
frameCount,
selectedFrameIndex,
setSelectedFrameIndex,
sizeMultiplier,
setSizeMultiplier,
} = useTools();
return (
@ -36,6 +38,35 @@ export default function CanvasToggle() {
</button>
) : null}
</div>
<div className="CanvasToggle" hidden>
<button
type="button"
data-selected={sizeMultiplier === 1 ? "" : undefined}
onClick={() => {
setSizeMultiplier(1);
}}
>
1&times;
</button>
<button
type="button"
data-selected={sizeMultiplier === 2 ? "" : undefined}
onClick={() => {
setSizeMultiplier(2);
}}
>
2&times;
</button>
<button
type="button"
data-selected={sizeMultiplier === 4 ? "" : undefined}
onClick={() => {
setSizeMultiplier(4);
}}
>
4&times;
</button>
</div>
{hasAnimation ? (
<div className="FrameSelector">
<button

View file

@ -6,6 +6,7 @@ import type { MaterialDefinition } from "./Material";
import useWarrior from "./useWarrior";
import useImageWorker from "./useImageWorker";
import useImageLoader from "./useImageLoader";
import useTools from "./useTools";
const defaultTextureSize = [512, 512] as [number, number];
@ -26,11 +27,12 @@ export default function ColorCanvas({
const [noAlphaImageUrl, setNoAlphaImageUrl] = useState<string | null>(null);
const { removeAlphaFromArrayBuffer } = useImageWorker();
const { loadImage } = useImageLoader();
const { sizeMultiplier } = useTools();
const textureSize = useMemo(
() => materialDef.size ?? defaultTextureSize,
[materialDef]
);
const textureSize = useMemo<[number, number]>(() => {
const [width, height] = materialDef.size ?? defaultTextureSize;
return [width * sizeMultiplier, height * sizeMultiplier];
}, [materialDef.size, sizeMultiplier]);
const handleChange = useCallback<CanvasProps["onChange"]>(
async (canvas) => {
@ -86,7 +88,7 @@ export default function ColorCanvas({
loadImage,
]);
const canvasId = `${materialDef.name}:color:${frameIndex}`;
const canvasId = `${materialDef.name}:color:${frameIndex}:${sizeMultiplier}`;
return textureSize ? (
<Canvas

View file

@ -6,6 +6,7 @@ import type { MaterialDefinition } from "./Material";
import useSkin from "./useSkin";
import useWarrior from "./useWarrior";
import useImageLoader from "./useImageLoader";
import useTools from "./useTools";
const defaultTextureSize = [512, 512] as [number, number];
@ -30,11 +31,12 @@ export default function MetallicCanvas({
convertArrayBufferAlphaToGrayscale,
} = useImageWorker();
const { loadImage } = useImageLoader();
const { sizeMultiplier } = useTools();
const textureSize = useMemo(
() => materialDef.size ?? defaultTextureSize,
[materialDef]
);
const textureSize = useMemo<[number, number]>(() => {
const [width, height] = materialDef.size ?? defaultTextureSize;
return [width * sizeMultiplier, height * sizeMultiplier];
}, [materialDef.size, sizeMultiplier]);
const handleChange = useCallback<CanvasProps["onChange"]>(
async (canvas) => {
@ -111,7 +113,7 @@ export default function MetallicCanvas({
loadImage,
]);
const canvasId = `${materialDef.name}:metallic:${frameIndex}`;
const canvasId = `${materialDef.name}:metallic:${frameIndex}:${sizeMultiplier}`;
return textureSize ? (
<Canvas

View file

@ -14,6 +14,8 @@ const { publicRuntimeConfig } = getConfig();
const { materials } = publicRuntimeConfig;
const defaultTextureSize = [512, 512] as [number, number];
function lockObject(object: fabric.Object) {
object.lockMovementX = true;
object.lockMovementY = true;
@ -47,6 +49,7 @@ export default function ToolsProvider({ children }: { children: ReactNode }) {
const { actualModel, selectedModelType } = useWarrior();
const [selectedMaterialIndex, setSelectedMaterialIndex] = useState(0);
const [selectedFrameIndex, setSelectedFrameIndex] = useState(0);
const [sizeMultiplier, setSizeMultiplier] = useState<number>(1);
const materialDefs: MaterialDefinition[] = materials[actualModel];
const materialDef = materialDefs[selectedMaterialIndex] ?? null;
const frameCount = materialDef.frameCount ?? 1;
@ -55,10 +58,10 @@ export default function ToolsProvider({ children }: { children: ReactNode }) {
boolean[]
>([]);
const textureSize: [number, number] = useMemo(
() => materialDef.size ?? [512, 512],
[materialDef]
);
const textureSize: [number, number] = useMemo(() => {
const [width, height] = materialDef.size ?? defaultTextureSize;
return [width * sizeMultiplier, height * sizeMultiplier];
}, [materialDef.size, sizeMultiplier]);
const hasMetallic = !(
materialDef.metallicFactor === 0 && materialDef.roughnessFactor === 1
@ -74,6 +77,21 @@ export default function ToolsProvider({ children }: { children: ReactNode }) {
setSelectedFrameIndex(0);
}
useEffect(() => {
try {
const savedSizeMultiplier = localStorage.getItem("sizeMultiplier");
switch (savedSizeMultiplier) {
case "1":
case "2":
case "4":
setSizeMultiplier(+savedSizeMultiplier);
break;
}
} catch (err) {
// Probably blocked. That's okay.
}
}, []);
useEffect(() => {
setSelectedExportMaterials(
materialDefs.map((material) =>
@ -100,10 +118,10 @@ export default function ToolsProvider({ children }: { children: ReactNode }) {
);
const activeCanvas = materialDef
? `${materialDef.name}:${activeCanvasType}:${selectedFrameIndex}`
? `${materialDef.name}:${activeCanvasType}:${selectedFrameIndex}:${sizeMultiplier}`
: null;
const metallicCanvasId = materialDef
? `${materialDef.name}:metallic:${selectedFrameIndex}`
? `${materialDef.name}:metallic:${selectedFrameIndex}:${sizeMultiplier}`
: null;
const { canvases } = useCanvas();
const { canvas, notifyChange, undo, redo, canUndo, canRedo } =
@ -412,11 +430,19 @@ export default function ToolsProvider({ children }: { children: ReactNode }) {
const frames = new Array(frameCount).fill(null);
return frames.map(async (_, frameIndex) => {
const colorCanvas =
canvases[`${materialDef.name}:color:${frameIndex}`]?.canvas;
canvases[
`${materialDef.name}:color:${frameIndex}:${sizeMultiplier}`
]?.canvas;
const metallicCanvas =
canvases[`${materialDef.name}:metallic:${frameIndex}`]?.canvas;
canvases[
`${materialDef.name}:metallic:${frameIndex}:${sizeMultiplier}`
]?.canvas;
const textureSize = materialDef.size ?? [512, 512];
const baseTextureSize = materialDef.size ?? defaultTextureSize;
const textureSize = [
baseTextureSize[0] * sizeMultiplier,
baseTextureSize[1] * sizeMultiplier,
];
let outputImageUrl;
const colorImageUrl = colorCanvas.toDataURL({
@ -500,15 +526,17 @@ export default function ToolsProvider({ children }: { children: ReactNode }) {
(match, a, b) => (a || b).toUpperCase()
);
let zipFileName = "";
const multiplierString =
sizeMultiplier > 1 ? `-@${sizeMultiplier}x` : "";
switch (selectedModelType) {
case "player":
zipFileName = `zPlayerSkin-${name}.vl2`;
zipFileName = `zPlayerSkin-${name}${multiplierString}.vl2`;
break;
case "weapon":
zipFileName = `zWeapon${camelCaseName}-${name}.vl2`;
zipFileName = `zWeapon${camelCaseName}-${name}${multiplierString}.vl2`;
break;
case "vehicle":
zipFileName = `z${camelCaseName}-${name}.vl2`;
zipFileName = `z${camelCaseName}-${name}${multiplierString}.vl2`;
break;
}
await saveZipFile(zip, zipFileName);
@ -517,13 +545,14 @@ export default function ToolsProvider({ children }: { children: ReactNode }) {
return;
},
[
actualModel,
canvasPadding,
canvases,
combineColorAndAlphaImageUrls,
materialDefs,
selectedModelType,
selectedExportMaterials,
canvases,
sizeMultiplier,
canvasPadding,
selectedModelType,
combineColorAndAlphaImageUrls,
actualModel,
]
);
@ -572,6 +601,8 @@ export default function ToolsProvider({ children }: { children: ReactNode }) {
setSelectedFrameIndex,
hasAnimation,
frameCount,
sizeMultiplier,
setSizeMultiplier,
selectedExportMaterials,
setSelectedExportMaterials,
}),
@ -583,14 +614,14 @@ export default function ToolsProvider({ children }: { children: ReactNode }) {
brushColor,
brushSize,
hueRotate,
saturation,
brightness,
contrast,
layerMode,
setHueRotate,
saturation,
setSaturation,
brightness,
setBrightness,
contrast,
setContrast,
layerMode,
selectedObjects,
lockSelection,
unlockSelection,
@ -611,6 +642,8 @@ export default function ToolsProvider({ children }: { children: ReactNode }) {
selectedFrameIndex,
hasAnimation,
frameCount,
sizeMultiplier,
setSizeMultiplier,
selectedExportMaterials,
]
);

View file

@ -4,9 +4,10 @@ import useSettings from "./useSettings";
import { WarriorContext } from "./useWarrior";
import type { MaterialDefinition } from "./Material";
import type { Skin } from "./importUtils";
import { useRouter } from "next/router";
const { publicRuntimeConfig } = getConfig();
const { materials, modelDefaults } = publicRuntimeConfig;
const { materials, modelDefaults, defaultSkins } = publicRuntimeConfig;
const baseSkinPath = `https://exogen.github.io/t2-skins/skins`;
let IMPORTED_SKINS: Map<string, Map<string | null, Skin>> = new Map();
@ -38,6 +39,55 @@ function getFrameNames(frameZeroFile: string, frameCount: number) {
}
}
function modelToType(model: string) {
switch (model) {
case "lmale":
case "mmale":
case "hmale":
case "lfemale":
case "mfemale":
case "hfemale":
case "lbioderm":
case "mbioderm":
case "hbioderm":
return "player";
case "disc":
case "chaingun":
case "grenade_launcher":
case "sniper":
case "plasmathrower":
case "energy":
case "shocklance":
case "elf":
case "missile":
case "mortar":
case "repair":
case "targeting":
case "mine":
return "weapon";
case "vehicle_grav_scout":
case "vehicle_grav_tank":
case "vehicle_land_mpbbase":
case "vehicle_air_scout":
case "vehicle_air_bomber":
case "vehicle_air_hapc":
return "vehicle";
default:
return null;
}
}
function skinToType(actualModel: string, skinName: string) {
const defaultSkin = modelDefaults[actualModel];
if (skinName === defaultSkin) {
return "default";
} else if (defaultSkins[actualModel]?.includes(skinName)) {
return "default";
} else {
return "custom";
}
}
export function getSkinImageUrls({
basePath,
actualModel,
@ -127,32 +177,8 @@ function getModelUrl(
}
}
// const queryParamSeparator = ".";
// function parseQuerySelection(searchParams: URLSearchParams) {
// const modelWithTypeFromUrl = searchParams.get("m");
// const skinWithTypeFromUrl = searchParams.get("s");
// let selectedModel;
// let selectedModelType;
// if (typeof modelWithTypeFromUrl === "string") {
// [selectedModelType, selectedModel] =
// modelWithTypeFromUrl.split(queryParamSeparator);
// }
// let selectedSkin;
// let selectedSkinType;
// if (typeof skinWithTypeFromUrl === "string") {
// [selectedSkinType, selectedSkin] =
// skinWithTypeFromUrl.split(queryParamSeparator);
// }
// return {
// selectedModel: selectedModel || null,
// selectedModelType: selectedModelType || null,
// selectedSkin: selectedSkin || null,
// selectedSkinType: selectedSkinType || null,
// };
// }
export default function WarriorProvider({ children }: { children: ReactNode }) {
const router = useRouter();
const [selectedModel, setSelectedModel] = useState<string>("lmale");
const [selectedModelType, setSelectedModelType] = useState("player");
const [selectedSkin, setSelectedSkin] = useState<string | null>(
@ -250,6 +276,57 @@ export default function WarriorProvider({ children }: { children: ReactNode }) {
addImportedSkins,
]);
useEffect(() => {
if (router.isReady) {
const modelName = router.query.m;
if (typeof modelName === "string") {
const modelType = modelToType(modelName);
const actualModel = modelName === "hfemale" ? "hmale" : modelName;
if (modelType) {
setSelectedModel(modelName);
setSelectedModelType(modelType);
const skinPath = router.query.s;
if (typeof skinPath === "string") {
const skinType = skinToType(actualModel, skinPath);
setSelectedSkin(skinPath);
setSelectedSkinType(skinType);
console.log("set model and skin from route:", modelName, skinPath);
}
}
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [router.isReady]);
const { replace: replaceRoute } = router;
useEffect(() => {
if (router.isReady) {
if (router.query.m !== selectedModel || router.query.s !== selectedSkin) {
console.log(
"router is ready. query params do not match, replacing:",
selectedModel,
selectedSkin
);
replaceRoute(
{
pathname: router.pathname,
query: { ...router.query, m: selectedModel, s: selectedSkin },
},
undefined,
{ shallow: true }
);
}
}
}, [
replaceRoute,
router.isReady,
router.pathname,
router.query,
selectedSkin,
selectedModel,
]);
useEffect(() => {
if (selectedSkin) {
try {

View file

@ -5,11 +5,11 @@ import { BsFillGrid3X3GapFill } from "react-icons/bs";
import { useEffect, useRef, useState } from "react";
import useTools from "./useTools";
import { importMultipleFilesToModels, modelToModelType } from "./importUtils";
import useManifest from "./useManifest";
const { publicRuntimeConfig } = getConfig();
const { defaultSkins, modelDefaults /*materials*/ } = publicRuntimeConfig;
const { defaultSkins, modelDefaults } = publicRuntimeConfig;
const baseManifestPath = `https://exogen.github.io/t2-skins`;
const defaultCustomSkins = {};
const emptyMap = new Map();
@ -40,6 +40,7 @@ export default function WarriorSelector() {
null
);
const fileInputRef = useRef<HTMLInputElement | null>(null);
const [manifest, isManifestLoaded] = useManifest();
const importedSkinsForModel = importedSkins.get(actualModel) ?? emptyMap;
@ -48,33 +49,11 @@ export default function WarriorSelector() {
).filter((skin) => skin.isComplete);
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
let ignore = false;
const loadCustomSkins = async () => {
let res;
try {
res = await fetch(`${baseManifestPath}/skins.json`, { signal });
} catch (err) {
return;
}
if (!ignore) {
const json = await res.json();
if (!ignore) {
setCustomSkins(json.customSkins ?? {});
setNewSkins(json.newSkins ?? {});
}
}
};
loadCustomSkins();
return () => {
ignore = true;
controller.abort();
};
}, []);
if (isManifestLoaded) {
setCustomSkins(manifest.customSkins);
setNewSkins(manifest.newSkins);
}
}, [manifest, isManifestLoaded]);
let skinSelectValue = selectedSkin ?? "";
if (selectedSkin && selectedSkinSection) {

View file

@ -158,7 +158,11 @@ export default function GalleryPage() {
)}.${skinModel}.webp`;
return (
<div key={`${skinName}:${skinModel}`} className={styles.Skin}>
<Link
key={`${skinName}:${skinModel}`}
className={styles.Skin}
href={`/?m=${skinModel}&s=${encodeURIComponent(skinName)}`}
>
<img
className={styles.Preview}
loading="lazy"
@ -168,7 +172,7 @@ export default function GalleryPage() {
alt={skinName}
/>
<div className={styles.Name}>{skinName}</div>
</div>
</Link>
);
})}
</div>

View file

@ -335,6 +335,10 @@ select {
border-bottom-left-radius: 4px;
}
.CanvasToggle button:not(:last-child) {
border-right-color: rgb(54, 135, 123);
}
.CanvasToggle button:last-child {
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;

View file

@ -52,6 +52,10 @@ interface ToolsContextValue {
hasMetallic: boolean;
hasAnimation: boolean;
frameCount: number;
sizeMultiplier: number;
setSizeMultiplier: (
sizeMultiplier: number | ((sizeMultiplier: number) => number)
) => void;
selectedExportMaterials: boolean[];
setSelectedExportMaterials: (
selectedExportMaterials: