Skip to content

Commit

Permalink
Implement SVG import
Browse files Browse the repository at this point in the history
  • Loading branch information
keupoz committed Jan 30, 2024
1 parent 5df31fd commit 815e9ae
Show file tree
Hide file tree
Showing 28 changed files with 527 additions and 33 deletions.
12 changes: 12 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"file-saver": "^2.0.5",
"fontkit": "^2.0.2",
"jotai": "^2.4.2",
"pretty-bytes": "^6.1.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-dropzone": "^14.2.3",
Expand Down
31 changes: 21 additions & 10 deletions src/components/AppContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ import {
ResizablePanel,
ResizablePanelGroup,
} from "@/shadcn/components/ui/resizable";
import { useFontsStore } from "@/stores/FontSettingsStore";
import { readFile } from "@/utils/readFile";
import { SVGInfo, useFontsStore } from "@/stores/FontSettingsStore";
import { readFont } from "@/utils/readFont";
import { readSVG } from "@/utils/readSVG";
import { GearIcon } from "@radix-ui/react-icons";
import { useMediaQuery } from "@uidotdev/usehooks";
import { Buffer } from "buffer";
import { create as createFont } from "fontkit";
import { Font } from "fontkit";
import { FC } from "react";
import { useDropzone } from "react-dropzone";
import { Scene } from "./Scene";
Expand All @@ -28,17 +28,28 @@ export const AppContent: FC = () => {
"font/otf": [".otf"],
"font/woff": [".woff"],
"font/woff2": [".woff2"],
"image/svg+xml": [".svg"],
},
async onDrop(files) {
const promises = files.map(async (file) => {
const rawFont = await readFile(file);
return createFont(Buffer.from(rawFont));
});
const fontPromises: Promise<Font>[] = [];
const svgPromises: Promise<SVGInfo>[] = [];

const fonts = await Promise.all(promises);
for (const file of files) {
if (file.name.endsWith(".svg")) {
svgPromises.push(readSVG(file));
} else {
fontPromises.push(readFont(file));
}
}

const fontsPromise = Promise.all(fontPromises);
const svgsPromise = Promise.all(svgPromises);

const [fonts, svgs] = await Promise.all([fontsPromise, svgsPromise]);

useFontsStore.setState((prev) => ({
fonts: [...prev.fonts, ...fonts],
fonts: fonts.length ? [...prev.fonts, ...fonts] : prev.fonts,
svgs: svgs.length ? [...prev.svgs, ...svgs] : prev.svgs,
}));
},
});
Expand Down
6 changes: 3 additions & 3 deletions src/components/dice/DieFace/DieFace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { FC, Fragment, memo } from "react";
import { degToRad } from "three/src/math/MathUtils.js";
import { FaceInfo } from "../utils/types";
import { FaceLayout } from "./FaceLayout";
import { Text3D } from "./Text3D";
import { FaceText } from "./FaceText";
import { useInfos } from "./useInfos";

export interface DieFaceProps {
Expand Down Expand Up @@ -46,13 +46,13 @@ export const DieFace: FC<DieFaceProps> = memo(({ info, geom, fontScale }) => {
isUnderscore={state.isUnderscore}
markGap={state.markGap}
>
<Text3D
<FaceText
text={state.text}
font={textFont}
features={textFeatures}
/>

<Text3D
<FaceText
text={state.mark}
font={markFont}
features={markFeatures}
Expand Down
11 changes: 10 additions & 1 deletion src/components/dice/DieFace/FaceLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { useForceUpdate } from "@/hooks/useForceUpdate";
import { alignObject } from "@/utils/alignObject";
import { createBoxFromObject } from "@/utils/createBoxFromObject";
import { FC, PropsWithChildren, useLayoutEffect, useRef } from "react";
import { Group, Vector3 } from "three";
import { FaceLayoutContext } from "./FaceLayoutContext";

export interface FaceLayoutProps {
isUnderscore: boolean;
Expand All @@ -14,6 +16,7 @@ export const FaceLayout: FC<PropsWithChildren<FaceLayoutProps>> = ({
children,
}) => {
const rootRef = useRef<Group>(null);
const forceUpdate = useForceUpdate();

useLayoutEffect(() => {
if (!rootRef.current) return;
Expand Down Expand Up @@ -53,5 +56,11 @@ export const FaceLayout: FC<PropsWithChildren<FaceLayoutProps>> = ({
}
});

return <group ref={rootRef}>{children}</group>;
return (
<group ref={rootRef}>
<FaceLayoutContext.Provider value={forceUpdate}>
{children}
</FaceLayoutContext.Provider>
</group>
);
};
4 changes: 4 additions & 0 deletions src/components/dice/DieFace/FaceLayoutContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { createForceUpdateContext } from "@/hooks/useForceUpdate";

export const [FaceLayoutContext, useUpdateFaceLayout] =
createForceUpdateContext();
13 changes: 13 additions & 0 deletions src/components/dice/DieFace/FaceText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { FC } from "react";
import { SVG3D } from "./SVG3D";
import { Text3D, Text3DProps } from "./Text3D";

export interface FaceTextProps extends Omit<Text3DProps, "text"> {
text: string | number;
}

export const FaceText: FC<FaceTextProps> = ({ text, ...props }) => {
if (typeof text === "number") return <SVG3D id={text} />;

return <Text3D text={text} {...props} />;
};
47 changes: 47 additions & 0 deletions src/components/dice/DieFace/SVG3D.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { useUpdateCSG } from "@/components/three/csg/CSGContext";
import { FONT_MATERIAL } from "@/materials";
import { useFontSettings, useFontsStore } from "@/stores/FontSettingsStore";
import { getBoundingBox } from "@/utils/alignObject";
import { getSVGGeometry } from "@/utils/fonts/getSVGGeometry";
import { FC, memo, useMemo } from "react";
import { Vector3 } from "three";
import { useUpdateFaceLayout } from "./FaceLayoutContext";

export interface SVG3DProps {
id: number;
}

export const SVG3D: FC<SVG3DProps> = memo(({ id }) => {
useUpdateFaceLayout();
useUpdateCSG();

const svgs = useFontsStore((state) => state.svgs);
const segments = useFontSettings((state) => state.segments);
const fontScale = useFontSettings((state) => state.fontScale);
const svgScale = useFontSettings((state) => state.svgScale);

const svg = useMemo(() => {
return svgs.find((svg) => svg.id === id) ?? null;
}, [id, svgs]);

const geometry = useMemo(() => {
if (!svg) return null;

return getSVGGeometry(svg, segments);
}, [segments, svg]);

if (geometry === null) return null;

const bounds = getBoundingBox(geometry);
const size = bounds.getSize(new Vector3());

const viewboxScale = svg?.scaleByViewbox ? svg.viewboxScale : null;
const boxScale = viewboxScale ?? 1 / Math.max(size.x, size.y);
const scale = (boxScale * svgScale) / fontScale;

return (
<group scale-x={scale} scale-y={-scale}>
<brush geometry={geometry} material={FONT_MATERIAL} />
</group>
);
});
4 changes: 2 additions & 2 deletions src/components/dice/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ export interface DieOptionsStore<T extends Record<string, DieInputConfig>> {
}

export interface DieFaceStore {
text: string;
mark: string;
text: string | number;
mark: string | number;
isUnderscore: boolean;
markGap: number;
rotation: number;
Expand Down
5 changes: 4 additions & 1 deletion src/components/settings/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Tabs, TabsList, TabsTrigger } from "@/shadcn/components/ui/tabs";
import { FC, memo } from "react";
import { AppHeader } from "./partials/AppHeader";
import { DiceTab } from "./tabs/DiceTab";
import { FilesTab } from "./tabs/FilesTab";
import { FontsTab } from "./tabs/FontsTab";
import { GlobalTab } from "./tabs/GlobalTab";

Expand All @@ -11,14 +12,16 @@ export const Settings: FC = memo(() => {
<AppHeader />

<Tabs defaultValue="global" className="px-3">
<TabsList className="grid grid-cols-3">
<TabsList className="grid grid-cols-4">
<TabsTrigger value="global">Global</TabsTrigger>
<TabsTrigger value="fonts">Fonts</TabsTrigger>
<TabsTrigger value="files">Files</TabsTrigger>
<TabsTrigger value="dice">Dice</TabsTrigger>
</TabsList>

<GlobalTab />
<FontsTab />
<FilesTab />
<DiceTab />
</Tabs>
</>
Expand Down
66 changes: 66 additions & 0 deletions src/components/settings/controls/SettingsSVGSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Input } from "@/shadcn/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
} from "@/shadcn/components/ui/select";
import { useFontsStore } from "@/stores/FontSettingsStore";
import { FC, useId, useMemo } from "react";
import { SettingsRow } from "./SettingsRow";

export interface SettingsSVGSelectProps {
label: string;
value: string | number;
onChange: (value: string | number) => void;
}

export const SettingsSVGSelect: FC<SettingsSVGSelectProps> = ({
label,
value,
onChange,
}) => {
const id = useId();

const svgs = useFontsStore((state) => state.svgs);

const placeholder = useMemo(() => {
if (typeof value === "string") return "Enter text";

return svgs.find((svg) => svg.id === value)?.name ?? "SVG selected";
}, [svgs, value]);

return (
<SettingsRow label={label} id={id}>
<Input
className="col-span-6 h-8"
id={id}
type="text"
value={typeof value === "number" ? "" : value}
placeholder={placeholder}
onChange={(e) => onChange(e.currentTarget.value)}
/>

<Select
value={typeof value === "number" ? value.toString() : "-1"}
onValueChange={(value) => onChange(parseInt(value))}
>
<SelectTrigger className="col-span-2 h-8" />

<SelectContent>
{svgs.length === 0 ? (
<SelectItem value="None" disabled>
No SVGs loaded
</SelectItem>
) : (
svgs.map((item, i) => (
<SelectItem key={i} value={item.id.toString()}>
{item.name}
</SelectItem>
))
)}
</SelectContent>
</Select>
</SettingsRow>
);
};
6 changes: 3 additions & 3 deletions src/components/settings/partials/DieFaceSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { FaceInfo } from "@/components/dice/utils/types";
import { FC } from "react";
import { SettingsSVGSelect } from "../controls/SettingsSVGSelect";
import { SettingsSlider } from "../controls/SettingsSlider";
import { SettingsSwitch } from "../controls/SettingsSwitch";
import { SettingsText } from "../controls/SettingsText";

export interface DieFaceSettingsProps {
info: FaceInfo;
Expand All @@ -13,13 +13,13 @@ export const DieFaceSettings: FC<DieFaceSettingsProps> = ({ info }) => {

return (
<>
<SettingsText
<SettingsSVGSelect
label="Text"
value={state.text}
onChange={(text) => info.useStore.setState({ text })}
/>

<SettingsText
<SettingsSVGSelect
label="Mark"
value={state.mark}
onChange={(mark) => info.useStore.setState({ mark })}
Expand Down
Loading

0 comments on commit 815e9ae

Please sign in to comment.