Add custom coin geometries

This commit is contained in:
2023-09-16 16:53:13 +01:00
parent 847d15d083
commit 9fbfd1ff1c
15 changed files with 858 additions and 32 deletions
@@ -0,0 +1,17 @@
import { useGLTF, useTexture } from "@react-three/drei";
import { CoinProps } from "@/types/coin";
export function BahamasCents10(coin: CoinProps) {
// @ts-expect-error property `nodes` actually exists
const { nodes } = useGLTF("/models/bahamas-cents-10.gltf");
const texture = useTexture("/api/merge-textures/" + coin.id);
return (
<>
<primitive object={nodes.BahamasCents10.geometry} />
<meshStandardMaterial metalness={1} roughness={0.5} map={texture} />
</>
);
}
useGLTF.preload("/models/bahamas-cents-10.gltf");
@@ -0,0 +1,17 @@
import { useGLTF, useTexture } from "@react-three/drei";
import { CoinProps } from "@/types/coin";
export function BahamasCents15(coin: CoinProps) {
// @ts-expect-error property `nodes` actually exists
const { nodes } = useGLTF("/models/bahamas-cents-15.gltf");
const texture = useTexture("/api/merge-textures/" + coin.id);
return (
<>
<primitive object={nodes.BahamasCents15.geometry} />
<meshStandardMaterial metalness={1} roughness={0.5} map={texture} />
</>
);
}
useGLTF.preload("/models/bahamas-cents-15.gltf");
@@ -0,0 +1,17 @@
import { useGLTF, useTexture } from "@react-three/drei";
import { CoinProps } from "@/types/coin";
export function EuroCents20(coin: CoinProps) {
// @ts-expect-error property `nodes` actually exists
const { nodes } = useGLTF("/models/euro-cents-20.gltf");
const texture = useTexture("/api/merge-textures/" + coin.id);
return (
<>
<primitive object={nodes.EuroCents20.geometry} />
<meshStandardMaterial metalness={1} roughness={0.5} map={texture} />
</>
);
}
useGLTF.preload("/models/euro-cents-20.gltf");
+30 -12
View File
@@ -2,6 +2,9 @@ import * as THREE from "three";
import { useLoader } from "@react-three/fiber";
import { CoinProps } from "@/types/coin";
import { useEffect, useMemo } from "react";
import { EuroCents20 } from "./custom-geometries/euro-cents-20";
import { BahamasCents15 } from "./custom-geometries/bahamas-cents-15";
import { BahamasCents10 } from "./custom-geometries/bahamas-cents-10";
const SafeEdgeMaterial = ({ edge }: Pick<CoinProps, "edge">) => {
const edgeTexture: THREE.Texture = useLoader(
@@ -23,10 +26,8 @@ const SafeEdgeMaterial = ({ edge }: Pick<CoinProps, "edge">) => {
);
};
export const CoinInstance = ({
export const BaseCoin = ({
orientation,
size,
thickness,
color,
obverse,
reverse,
@@ -55,15 +56,7 @@ export const CoinInstance = ({
return (
<>
<cylinderGeometry
attach="geometry"
args={[
(size ?? 16) / 16,
(size ?? 16) / 16,
(thickness ?? 2) / 16,
sides,
]}
/>
<cylinderGeometry attach="geometry" args={[1, 1, 1, sides]} />
{edge?.picture ? (
<SafeEdgeMaterial edge={edge} />
@@ -92,3 +85,28 @@ export const CoinInstance = ({
</>
);
};
export const CoinInstance = (coin: CoinProps) => {
if (
coin.value?.numeric_value === 0.2 &&
coin.value?.currency?.name === "Euro"
) {
return <EuroCents20 {...coin} />;
}
if (
coin.value?.currency?.name === "Dollar" &&
coin.issuer?.code === "bahamas"
) {
switch (coin.value?.numeric_value) {
case 0.15:
return <BahamasCents15 {...coin} />;
case 0.1:
return <BahamasCents10 {...coin} />;
default:
break;
}
}
return <BaseCoin {...coin} />;
};
+2 -1
View File
@@ -33,8 +33,9 @@ export const CoinInstances = (coinProps: CoinProps) => {
Math.PI * (Math.random() * 2 - 1),
Math.PI * (Math.random() * 2 - 1),
);
at(index).scaleOverride([size / 16, thickness / 16, size / 16]);
});
}, [at, count]);
}, [at, count, size, thickness]);
return (
<instancedMesh ref={ref} args={[undefined, undefined, count]}>
+13 -2
View File
@@ -27,7 +27,7 @@ const SpinninType = ({
}
if (category === "banknotes") {
ref.current.rotation.y += delta;
ref.current.rotation.x += delta;
return;
}
@@ -44,7 +44,18 @@ const SpinninType = ({
return (
<mesh {...props}>
<mesh ref={ref}>
<mesh
ref={ref}
{...(category !== "banknotes"
? {
scale: [
(typeProps.size ?? 16) / 16,
(typeProps.thickness ?? 2) / 16,
(typeProps.size ?? 16) / 16,
],
}
: { rotation: [Math.PI / 2, Math.PI, Math.PI / 2] })}
>
<Instance {...typeProps} />
</mesh>
</mesh>
+137
View File
@@ -0,0 +1,137 @@
import type { NextApiHandler } from "next";
import { readFile, readdir } from "fs/promises";
import path from "path";
import { spawn } from "child_process";
const asyncSpawn = (command: string, args: string[]) => {
return new Promise((resolve, reject) => {
const child = spawn(command, args, {
stdio: "inherit",
});
child.on("error", reject);
child.stdout?.on("data", process.stdout.write);
child.stderr?.on("data", process.stderr.write);
child.on("close", (code) => {
if (code === 0) {
resolve(code);
} else {
reject(code);
}
});
});
};
const getCoinPath = async (id: string) => {
const countries = await readdir("./public/coins");
for (const country of countries) {
const coins = await readdir(path.join("./public/coins", country));
for (const coin of coins) {
if (coin === id + ".json") {
return path.join("./public/coins", country, coin);
}
}
}
};
const getPreviousResult = async (id: string) => {
try {
return await readFile(path.join("./public/photos/joined", id + ".jpg"));
} catch {
return;
}
};
const generateImage = async (id: string) => {
const previousResult = await getPreviousResult(id);
if (previousResult) {
return previousResult;
}
const coinPath = await getCoinPath(id);
if (!coinPath) {
return;
}
const coin = JSON.parse(await readFile(coinPath, { encoding: "utf-8" }));
await asyncSpawn("convert", [
path.join("./public", coin.obverse.picture),
"-resize",
"256x256!",
path.join("./public/photos/joined", id + "-obverse.jpg"),
]);
await asyncSpawn("convert", [
path.join("./public", coin.reverse.picture),
"-resize",
"256x256!",
path.join("./public/photos/joined", id + "-reverse.jpg"),
]);
if (coin.edge?.picture) {
await asyncSpawn("convert", [
path.join("./public", coin.edge.picture),
"-resize",
"512x64!",
path.join("./public/photos/joined", id + "-edge.jpg"),
]);
} else {
await asyncSpawn("convert", [
"-size",
"512x64",
"xc:" + coin.color,
path.join("./public/photos/joined", id + "-edge.jpg"),
]);
}
await asyncSpawn("convert", [
path.join("./public/photos/joined", id + "-obverse.jpg"),
path.join("./public/photos/joined", id + "-reverse.jpg"),
"+append",
path.join("./public/photos/joined", id + ".jpg"),
]);
for (let index = 0; index < 4; index++) {
await asyncSpawn("convert", [
path.join("./public/photos/joined", id + ".jpg"),
path.join("./public/photos/joined", id + "-edge.jpg"),
"-append",
path.join("./public/photos/joined", id + ".jpg"),
]);
}
return await readFile(path.join("./public/photos/joined", id + ".jpg"));
};
const handler: NextApiHandler = async ({ query }, res) => {
if (typeof query.id !== "string") {
res.status(400).json({
message: `Invalid id ${JSON.stringify(query.id)}`,
});
return;
}
try {
const image = await generateImage(query.id);
if (!image) {
res.status(404).json({
message: `Coin ${JSON.stringify(query.id)} not found`,
});
return;
}
res.setHeader("Content-Type", "image/jpg");
res.status(200).send(image);
} catch (error) {
console.error(error);
res.status(500).json({
message: `Something went wrong`,
error: String(error),
});
}
};
export default handler;