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
+2 -1
View File
@@ -1 +1,2 @@
node_modules
node_modules
public/models
+1 -1
View File
@@ -103,5 +103,5 @@
"name": "The Franklin Mint"
}
],
"color": "rgb(70.34482758620689,153.8793103448276,184.6551724137931)"
"color": "rgb(184,184,184)"
}
+1 -9
View File
@@ -87,14 +87,6 @@
"size": 23.5,
"thickness": 1.72,
"orientation": "medal",
"edge": {
"description": "Plain",
"picture": "/photos/bahamas/627d8a8a060cb7.98306122-original.jpg",
"thumbnail": "https://en.numista.com/catalogue/photos/bahamas/627d8a8a060cb7.98306122-180.jpg",
"picture_copyright": "Harryg",
"picture_license_name": "CC BY",
"picture_license_url": "https://creativecommons.org/licenses/by/4.0/deed.en"
},
"mints": [
{
"id": "16",
@@ -105,5 +97,5 @@
"name": "The Franklin Mint"
}
],
"color": "rgb(157,143,72)"
"color": "rgb(180,180,160)"
}
-6
View File
@@ -87,12 +87,6 @@
"size": 23,
"thickness": 1.4,
"orientation": "coin",
"edge": {
"description": "Plain",
"picture": "/photos/mexique/4308-original.jpg",
"thumbnail": "https://en.numista.com/catalogue/photos/mexique/4308-180.jpg",
"picture_copyright": "G-REMXD"
},
"mints": [
{
"id": "29",
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
@@ -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;