Feature/ Tabs improvement 2.3 (#605)

- Rebased to take the filter object from bombay
- New Object Upload Tab available in Film Set Tab and Object Tab
- Character Tab now uses the generic uploader because previewing and
taking a snapshot of a character is too much of a code challenge atm.
- added the dev-only feature tag, expressions uploads is now dev-only
- went over acceptable file types and took out refreshed all the upload
tabs
- realign the code so all upload tabs uses the exact same logic,
interfaces, and code structure, for better legibility

this PR eliminates:
https://github.com/storytold/GottaGoFast/pull/600
https://github.com/storytold/GottaGoFast/pull/598

---------

Co-authored-by: wil <wil@storyteller.ai>
This commit is contained in:
wilandwil authored and GitHub committed 2024-07-10 12:45:34 -04:00
1 parent d09837387f
commit dcec252f47
38 files changed
+736 -1497

No files matched your search

@@ -1,48 +0,0 @@
.fy-file-wrapper {
// flex-grow: 1;
// height: calc(100% - 4px);
.file-picker-button {
cursor: pointer;
}
.fy-file-wrapper-input {
visibility: hidden;
height: 0;
width: 0;
// overflow: hidden;
position: absolute;
+ label {
cursor: pointer;
}
}
.fy-uploader-layout {
display: flex;
align-items: center;
width: 100%;
.fy-uploader-layout-icon {
font-size: 2.625rem;
margin-right: 1rem;
}
}
}
.fy-file-dragging {
background: rgba(#676781, 0.25);
border-color: rgba(#fff, 0.33);
}
.fy-uploader {
[type="file"] {
+ label {
border: dashed 2px rgba(255, 255, 255, 0.12) !important;
border-radius: 0.5rem;
transition: 0.1s all;
&:hover {
// background-color: lighten($panel, 6%);
}
}
}
}
@@ -1,98 +0,0 @@
import React, { ReactNode, useId, useRef, useState } from "react";
import { AssetType } from "~/enums";
import { UploadModalObject } from "~/components";
import { twMerge } from "tailwind-merge";
interface Props {
render: (parentId: string) => ReactNode;
fileTypes?: string[];
onChange?: (file?: React.ChangeEvent) => void;
onSuccess: () => void;
panelClass?: string;
type: AssetType;
}
export function FileWrapper({
render,
fileTypes = [],
onSuccess,
type,
...rest
}: Props) {
const fileRef = useRef<HTMLInputElement>(null);
const [file, setFile] = useState<File | undefined>();
const [modalOpen, setModalOpen] = useState(false);
const [resetModal, setResetModal] = useState(false);
const [dragging, setDragging] = useState(false);
const id = "file-input-" + useId();
const accept = fileTypes
.map((fileType) => `.${fileType.toLowerCase()}`)
.join(",");
const clearFile = () => {
if (fileRef.current !== null) {
fileRef.current.value = "";
setFile(undefined);
}
};
const closeModal = () => setModalOpen(false);
const fileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setResetModal(true);
event.preventDefault();
setFile(event.target?.files?.[0]);
setModalOpen(true);
};
const onDragDrop = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
};
const onDragEvent =
(onOff: boolean) =>
(event: React.DragEvent<HTMLDivElement>): void => {
setDragging(onOff);
onDragDrop(event);
};
const onDrop = (event: React.DragEvent<HTMLDivElement>): void => {
setDragging(false);
onDragDrop(event);
if (event.dataTransfer.files && event.dataTransfer.files[0]) {
setResetModal(true);
setFile(event.dataTransfer.files[0]);
setModalOpen(true);
}
};
return (
<div
className={twMerge(
"flex h-full flex-col gap-3.5 overflow-y-auto",
dragging ? "border-white/33 border bg-ui-controls-button" : "",
)}
onDragLeave={onDragEvent(false)}
onDragOver={onDragEvent(true)}
onDrop={onDrop}
>
<input
accept={accept}
className="absolute hidden h-0 w-0"
onChange={fileChange}
type="file"
id={id}
ref={fileRef}
{...rest}
/>
{render(id)}
<UploadModalObject
closeModal={closeModal}
file={file!}
isOpen={modalOpen}
onClose={clearFile}
onSuccess={onSuccess}
resetModal={resetModal}
setResetModal={setResetModal}
type={type}
/>
</div>
);
}
@@ -1,12 +0,0 @@
import React from "react";
export default function LoadingIcon() {
return <>
<span
className="spinner-border spinner-border-sm ms-3"
role="status"
aria-hidden="true"
></span>
<span className="visually-hidden">Loading...</span>
</>;
};
@@ -1 +0,0 @@
export * from "./FileWrapper";
@@ -1,10 +1,17 @@
import { Button, H6, Input, ListDropdown } from "~/components";
import { FileUploader } from "./FileUploader";
import { useState } from "react";
import { MediaFileAnimationType } from "~/enums";
import {
THUMBNAILS_FILE_TYPE,
MediaFileAnimationType,
FilterEngineCategories,
} from "~/enums";
import { uploadAssets } from "./uploadAssets";
import { UploaderState } from "~/models";
interface Props {
title: string;
engineCategory: FilterEngineCategories;
fileTypes: string[];
onClose: () => void;
options?: {
@@ -12,21 +19,16 @@ interface Props {
hasLength?: boolean;
hasThumbnailUpload?: boolean;
};
onSubmit: (options: {
title: string;
typeOption?: MediaFileAnimationType;
assetFile: File;
length: number;
thumbnailFile: File | null;
}) => void;
onUploadProgress: (newState: UploaderState) => void;
}
export const UploadFiles = ({
fileTypes,
engineCategory,
onClose,
title,
options,
onSubmit,
onUploadProgress,
}: Props) => {
const fileSubtypes = options?.fileSubtypes;
const hasLength = options?.hasLength;
@@ -69,12 +71,15 @@ export const UploadFiles = ({
}));
return;
}
onSubmit({
uploadAssets({
title: uploadTitle.value,
assetFile: assetFile.value,
thumbnailFile: thumbnailFile,
length: uploadLength ?? 1000,
typeOption,
engineCategory: engineCategory,
animationType: typeOption,
thumbnailFile: thumbnailFile || undefined,
length: uploadLength,
progressCallback: onUploadProgress,
});
};
@@ -118,7 +123,7 @@ export const UploadFiles = ({
{hasThumbnailUpload && (
<FileUploader
title="Upload Thumbnail (optional)"
fileTypes={["PNG", "JGP", "GIF"]}
fileTypes={Object.values(THUMBNAILS_FILE_TYPE)}
file={thumbnailFile}
setFile={setThumbnailFile}
/>
@@ -1,18 +1,13 @@
import { useEffect, useState } from "react";
import { v4 as uuidv4 } from "uuid";
import { TransitionDialogue, LoadingDots } from "~/components";
import { UploadAssetError } from "./UploadAssetError";
import { UploadSuccess } from "./UploadSuccess";
import { UploadFiles } from "./UploadFiles";
import { MediaFilesApi, MediaUploadApi } from "~/Classes/ApiManager";
import {
FilterEngineCategories,
MediaFileAnimationType,
UploaderState,
} from "~/enums";
import { getFileName } from "~/utilities";
import { FilterEngineCategories, UploaderStates } from "~/enums";
import { UploaderState, initialUploaderState } from "~/models";
interface Props {
onClose: () => void;
@@ -28,10 +23,6 @@ interface Props {
};
}
const initialState = {
status: UploaderState.ready,
};
export function UploadModal({
isOpen,
onClose,
@@ -41,13 +32,15 @@ export function UploadModal({
type,
options,
}: Props) {
const [uploaderState, setUploaderState] = useState<{
status: UploaderState;
errorMessage?: string;
}>(initialState);
const [uploaderState, setUploaderState] =
useState<UploaderState>(initialUploaderState);
const updateUploaderState = (newState: UploaderState) => {
setUploaderState(newState);
};
const resetModalState = () => {
setUploaderState(initialState);
setUploaderState(initialUploaderState);
};
useEffect(() => {
@@ -56,98 +49,29 @@ export function UploadModal({
}
}, [isOpen]);
const onSubmit = async ({
title,
typeOption,
assetFile,
length,
thumbnailFile,
}: {
title: string;
typeOption?: MediaFileAnimationType;
assetFile: File;
length: number;
thumbnailFile: File | null;
}) => {
setUploaderState({ status: UploaderState.uploadingAsset });
const mediaUploadApi = new MediaUploadApi();
const assetReponse = await mediaUploadApi.UploadNewEngineAsset({
file: assetFile,
fileName: assetFile.name,
engine_category: type,
maybe_animation_type: typeOption,
maybe_duration_millis: length,
maybe_title: title,
uuid: uuidv4(),
});
if (!assetReponse.success || !assetReponse.data) {
setUploaderState({
status: UploaderState.assetError,
errorMessage: assetReponse.errorMessage,
});
return;
}
const assetToken = assetReponse.data;
if (!thumbnailFile) {
setUploaderState({ status: UploaderState.success });
return;
}
setUploaderState({ status: UploaderState.uploadingCover });
const thumbnailResponse = await mediaUploadApi.UploadImage({
uuid: uuidv4(),
blob: thumbnailFile,
fileName: getFileName(thumbnailFile),
maybe_title: "thumbnail_" + title,
});
if (!thumbnailResponse.success || !thumbnailResponse.data) {
setUploaderState({
status: UploaderState.coverCreateError,
errorMessage: thumbnailResponse.errorMessage,
});
return;
}
setUploaderState({ status: UploaderState.settingCover });
const thumbnailToken = thumbnailResponse.data;
const mediaFilesApi = new MediaFilesApi();
const setThumbnailResponse = await mediaFilesApi.UpdateCoverImage({
mediaFileToken: assetToken,
imageToken: thumbnailToken,
});
if (!setThumbnailResponse.success) {
setUploaderState({
status: UploaderState.coverSetError,
errorMessage: setThumbnailResponse.errorMessage,
});
return;
}
setUploaderState({ status: UploaderState.success });
};
const UploaderModalContent = () => {
switch (uploaderState.status) {
case UploaderState.ready:
case UploaderStates.ready:
return (
<UploadFiles
title={title}
engineCategory={type}
fileTypes={fileTypes}
options={options}
onClose={onClose}
onSubmit={onSubmit}
onUploadProgress={updateUploaderState}
/>
);
case UploaderState.uploadingAsset:
case UploaderState.uploadingCover:
case UploaderState.settingCover:
case UploaderStates.uploadingAsset:
case UploaderStates.uploadingCover:
case UploaderStates.settingCover:
return (
<>
<LoadingDots className="mb-1 bg-transparent" />
<div className="w-100 text-center opacity-50">Uploading...</div>
</>
);
case UploaderState.success:
case UploaderStates.success:
return (
<UploadSuccess
title={title}
@@ -157,7 +81,7 @@ export function UploadModal({
}}
/>
);
case UploaderState.assetError:
case UploaderStates.assetError:
return (
<UploadAssetError
onCancel={onClose}
@@ -168,8 +92,8 @@ export function UploadModal({
errorMessage={uploaderState.errorMessage}
/>
);
case UploaderState.coverCreateError:
case UploaderState.coverSetError:
case UploaderStates.coverCreateError:
case UploaderStates.coverSetError:
return (
<UploadAssetError
onCancel={onClose}
@@ -0,0 +1,86 @@
import { v4 as uuidv4 } from "uuid";
import {
FilterEngineCategories,
MediaFileAnimationType,
UploaderStates,
} from "~/enums";
import { getFileName } from "~/utilities";
import { MediaUploadApi, MediaFilesApi } from "~/Classes/ApiManager";
import { UploaderState } from "~/models";
export const uploadAssets = async ({
title,
assetFile,
thumbnailFile,
engineCategory,
animationType,
length,
progressCallback,
}: {
title: string;
assetFile: File;
engineCategory: FilterEngineCategories;
thumbnailFile?: File;
animationType?: MediaFileAnimationType;
length?: number;
progressCallback: (newState: UploaderState) => void;
}) => {
progressCallback({ status: UploaderStates.uploadingAsset });
const mediaUploadApi = new MediaUploadApi();
const assetReponse = await mediaUploadApi.UploadNewEngineAsset({
file: assetFile,
fileName: assetFile.name,
engine_category: engineCategory,
maybe_animation_type: animationType,
maybe_duration_millis: length,
maybe_title: title,
uuid: uuidv4(),
});
if (!assetReponse.success || !assetReponse.data) {
progressCallback({
status: UploaderStates.assetError,
errorMessage: assetReponse.errorMessage,
});
return;
}
const assetToken = assetReponse.data;
if (!thumbnailFile) {
progressCallback({ status: UploaderStates.success });
return;
}
progressCallback({ status: UploaderStates.uploadingCover });
const thumbnailResponse = await mediaUploadApi.UploadImage({
uuid: uuidv4(),
blob: thumbnailFile,
fileName: getFileName(thumbnailFile),
maybe_title: "thumbnail_" + title,
});
if (!thumbnailResponse.success || !thumbnailResponse.data) {
progressCallback({
status: UploaderStates.coverCreateError,
errorMessage: thumbnailResponse.errorMessage,
});
return;
}
progressCallback({ status: UploaderStates.settingCover });
const thumbnailToken = thumbnailResponse.data;
const mediaFilesApi = new MediaFilesApi();
const setThumbnailResponse = await mediaFilesApi.UpdateCoverImage({
mediaFileToken: assetToken,
imageToken: thumbnailToken,
});
if (!setThumbnailResponse.success) {
progressCallback({
status: UploaderStates.coverSetError,
errorMessage: setThumbnailResponse.errorMessage,
});
return;
}
progressCallback({ status: UploaderStates.success });
};
@@ -1,40 +1,31 @@
import { useCallback, useEffect, useState, useRef } from "react";
import { MediaFileAnimationType } from "~/enums";
import { Button, H6, Input, ListDropdown } from "~/components";
import { FileUploader } from "../UploadModal/FileUploader";
import { loadPreviewOnCanvas } from "./utilities";
import { loadPreviewOnCanvas, snapshotCanvasAsThumbnail } from "./utilities";
import { upload3DObjects } from "./utilities/upload3DObjects";
import { UploaderState } from "~/models";
import { MediaFileAnimationType } from "~/enums";
interface Props {
title: string;
fileTypes: string[];
onClose: () => void;
options?: {
fileSubtypes?: { [key: string]: string }[];
hasLength?: boolean;
hasThumbnailUpload?: boolean;
};
onSubmit: (options: {
title: string;
typeOption?: MediaFileAnimationType;
assetFile: File;
length: number;
thumbnailFile: File | null;
}) => void;
onClose: () => void;
onUploadProgress: (newState: UploaderState) => void;
}
export const UploadFiles3D = ({
fileTypes,
onClose,
title,
fileTypes,
options,
onSubmit,
onClose,
onUploadProgress,
}: Props) => {
const fileSubtypes = options?.fileSubtypes;
const hasLength = options?.hasLength;
const hasThumbnailUpload = options?.hasThumbnailUpload;
const canvasRef = useRef<HTMLCanvasElement | undefined>(undefined);
const canvasCallbackRef = useCallback((node: HTMLCanvasElement) => {
if (node !== null) {
@@ -42,13 +33,16 @@ export const UploadFiles3D = ({
}
}, []);
const [typeOption, setTypeOption] = useState<
MediaFileAnimationType | undefined
>(
const fileSubtypes = options?.fileSubtypes;
// const hasLength = options?.hasLength;
// const hasThumbnailUpload = options?.hasThumbnailUpload;
const [subtype, setSubtype] = useState<MediaFileAnimationType | undefined>(
fileSubtypes
? (Object.values(fileSubtypes[0])[0] as MediaFileAnimationType)
: undefined,
);
const [uploadTitle, setUploadTitle] = useState<{
value: string;
error?: string;
@@ -66,8 +60,9 @@ export const UploadFiles3D = ({
message?: string;
}>({ type: "init" });
const [uploadLength, setUploadLength] = useState<number>();
const [thumbnailFile, setThumbnailFile] = useState<File | null>(null);
const [thumbnailFile, setThumbnailFile] = useState<Blob | undefined>(
undefined,
);
const handleSubmit = () => {
if (!uploadTitle.value) {
@@ -86,12 +81,12 @@ export const UploadFiles3D = ({
return;
}
onSubmit({
upload3DObjects({
title: uploadTitle.value,
assetFile: assetFile.value,
thumbnailFile: thumbnailFile,
length: uploadLength ?? 1000,
typeOption,
thumbnailSnapshot: thumbnailFile,
animationType: subtype,
progressCallback: onUploadProgress,
});
};
@@ -107,6 +102,19 @@ export const UploadFiles3D = ({
}
}, [assetFile.value]);
useEffect(() => {
if (previewStatus.type === "OK" && canvasRef.current) {
snapshotCanvasAsThumbnail({
targetNode: canvasRef.current!,
resultCallback: (snapshotBlob) => {
if (snapshotBlob) {
setThumbnailFile(snapshotBlob);
}
},
});
}
}, [previewStatus]);
return (
<>
<div className="mb-4 flex flex-col gap-4">
@@ -117,20 +125,7 @@ export const UploadFiles3D = ({
onChange={(event) => setUploadTitle({ value: event.target.value })}
className={uploadTitle.error ? "mb-3" : ""}
/>
{fileSubtypes && fileSubtypes.length > 1 && (
<ListDropdown
list={fileSubtypes}
onSelect={(value) => setTypeOption(value as MediaFileAnimationType)}
/>
)}
{hasLength && (
<Input
type="number"
placeholder="Enter the length in ms (optional)"
value={uploadLength}
onChange={(event) => setUploadLength(parseInt(event.target.value))}
/>
)}
<FileUploader
title={title}
fileTypes={fileTypes}
@@ -141,6 +136,12 @@ export const UploadFiles3D = ({
});
}}
/>
{fileSubtypes && fileSubtypes.length > 1 && (
<ListDropdown
list={fileSubtypes}
onSelect={(value) => setSubtype(value as MediaFileAnimationType)}
/>
)}
{assetFile.error && (
<H6 className="z-10 text-red">{assetFile.error}</H6>
)}
@@ -163,14 +164,6 @@ export const UploadFiles3D = ({
)}
</div>
{hasThumbnailUpload && (
<FileUploader
title="Upload Thumbnail (optional)"
fileTypes={["PNG", "JGP", "GIF"]}
file={thumbnailFile}
setFile={setThumbnailFile}
/>
)}
<div className="flex justify-end gap-4">
<Button variant="primary" onClick={handleSubmit}>
Upload
@@ -0,0 +1,111 @@
import { useEffect, useState } from "react";
import { TransitionDialogue, LoadingDots } from "~/components";
import { UploadAssetError } from "../UploadModal/UploadAssetError";
import { UploadSuccess } from "../UploadModal/UploadSuccess";
import { UploadFiles3D } from "./UploadFiles3D";
import { FilterEngineCategories, UploaderStates } from "~/enums";
import { initialUploaderState, UploaderState } from "~/models";
interface Props {
onClose: () => void;
onSuccess: () => void;
isOpen: boolean;
title: string;
fileTypes: string[];
options?: {
fileSubtypes?: { [key: string]: string }[];
hasLength?: boolean;
hasThumbnailUpload?: boolean;
};
}
export function UploadModal3D({
isOpen,
onClose,
onSuccess,
title,
fileTypes,
options,
}: Props) {
const [uploaderState, setUploaderState] =
useState<UploaderState>(initialUploaderState);
const updateUploaderState = (newState: UploaderState) => {
setUploaderState(newState);
};
const resetModalState = () => {
setUploaderState(initialUploaderState);
};
useEffect(() => {
if (isOpen) {
resetModalState();
}
}, [isOpen]);
const UploaderModalContent = () => {
switch (uploaderState.status) {
case UploaderStates.ready:
return (
<UploadFiles3D
title={title}
fileTypes={fileTypes}
options={options}
onClose={onClose}
onUploadProgress={updateUploaderState}
/>
);
case UploaderStates.uploadingAsset:
case UploaderStates.uploadingCover:
case UploaderStates.settingCover:
return (
<>
<LoadingDots className="mb-1 bg-transparent" />
<div className="w-100 text-center opacity-50">Uploading...</div>
</>
);
case UploaderStates.success:
return (
<UploadSuccess
title={title}
onOk={() => {
onSuccess();
onClose();
}}
/>
);
case UploaderStates.assetError:
return (
<UploadAssetError
onCancel={onClose}
onRetry={() => {
resetModalState();
}}
type={FilterEngineCategories.OBJECT}
errorMessage={uploaderState.errorMessage}
/>
);
case UploaderStates.coverCreateError:
case UploaderStates.coverSetError:
return (
<UploadAssetError
onCancel={onClose}
onRetry={() => {
resetModalState();
}}
type={"Thumbnail"}
errorMessage={uploaderState.errorMessage}
/>
);
}
};
return (
<TransitionDialogue isOpen={isOpen} onClose={onClose} title={title}>
<UploaderModalContent />
</TransitionDialogue>
);
}
@@ -0,0 +1 @@
export * from "./UploadModal3D";
@@ -1,2 +1,3 @@
export * from "./loadPreviewOnCanvas";
export * from "./thumbnailHelpers";
export * from "./uploadAsset";
@@ -1,6 +1,5 @@
import * as THREE from "three";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { FBXLoader } from "three/addons/loaders/FBXLoader.js";
import { FontLoader } from "three/addons/loaders/FontLoader.js";
import { TextGeometry } from "three/addons/geometries/TextGeometry.js";
@@ -46,8 +45,6 @@ export const loadPreviewOnCanvas = ({
// load the file into the preview mini-scene depending of the file's type
if (file.name.includes(".glb")) {
glbLoader({ file, scene, camera, renderer, statusCallback });
} else if (file.name.includes(".fbx")) {
fbxLoader({ file, scene, camera, renderer, statusCallback });
} else if (file.name.includes(".pmd")) {
pmdLoader({ file, scene, camera, renderer, statusCallback });
} else if (
@@ -87,7 +84,6 @@ const glbLoader = ({
loader.load(
URL.createObjectURL(file),
(data) => {
console.log("preview object loader: onLoaded");
data.scene.children.forEach((child) => {
child.userData["color"] = "#FFFFFF";
scene.add(child);
@@ -120,6 +116,10 @@ const glbLoader = ({
});
}
renderer.render(scene, camera);
statusCallback({
type: "OK",
message: "Preview should be available",
});
});
},
undefined, // nothing to-do onProgress
@@ -133,28 +133,6 @@ const glbLoader = ({
);
};
const fbxLoader = ({ file, statusCallback }: LoaderInterface) => {
const loader = new FBXLoader();
loader.load(
URL.createObjectURL(file),
(data) => {
console.log(data);
statusCallback({
type: "FBX Loader Error",
message: "Sorry, FBX Loader is still under development",
});
},
undefined, // nothing to-do onProgress
(loaderError) => {
/*onError*/
statusCallback({
type: "FBX Loader Error",
message: String(loaderError),
});
},
);
};
const pmdLoader = ({
camera,
scene,
@@ -186,6 +164,10 @@ const pmdLoader = ({
textMesh.position.set(-22, -5, 0);
scene.add(textMesh);
renderer.render(scene, camera);
statusCallback({
type: "OK",
message: "Preview should be available",
});
},
undefined, // nothing on Progress
(loaderError) => {
@@ -223,4 +205,8 @@ const imagePlaneLoader = ({ file, scene, statusCallback }: LoaderInterface) => {
obj.receiveShadow = true;
obj.castShadow = true;
scene.add(obj);
statusCallback({
type: "OK",
message: "Preview should be available",
});
};
@@ -0,0 +1,54 @@
import { MediaFilesApi, MediaUploadApi } from "~/Classes/ApiManager";
import { v4 as uuidv4 } from "uuid";
import { getFileName } from "~/utilities";
export const snapshotCanvasAsThumbnail = async ({
targetNode,
resultCallback,
}: {
targetNode: HTMLCanvasElement | undefined;
resultCallback: (blob: Blob | undefined) => void;
}) => {
if (!targetNode) {
//TODO: Verbose Error
return undefined;
}
targetNode.toBlob((blob: Blob | null) => {
resultCallback(blob || undefined);
});
};
export const uploadSnapshotAsThumbnail = async ({
assetTitle = "unknown_asset",
blob,
}: {
assetTitle?: string;
blob: Blob;
}) => {
const thumbnailFile = new File([blob], "storyteller-cap.png");
const thumbnailTitle = "thumbnail_" + assetTitle;
const mediaUploadApi = new MediaUploadApi();
return await mediaUploadApi.UploadImage({
uuid: uuidv4(),
blob: thumbnailFile,
fileName: getFileName(thumbnailFile),
maybe_title: thumbnailTitle,
});
};
export const setThumbnail = async ({
assetToken,
thumbnailToken,
}: {
assetToken: string;
thumbnailToken: string;
}) => {
const mediaFilesApi = new MediaFilesApi();
return await mediaFilesApi.UpdateCoverImage({
mediaFileToken: assetToken,
imageToken: thumbnailToken,
});
};
@@ -0,0 +1,69 @@
import { MediaFileAnimationType, UploaderStates } from "~/enums";
import { setThumbnail, uploadSnapshotAsThumbnail } from "./thumbnailHelpers";
import { uploadAsset } from "./uploadAsset";
import { UploaderState } from "~/models";
export const upload3DObjects = async ({
title,
assetFile,
thumbnailSnapshot,
animationType,
progressCallback,
}: {
title: string;
assetFile: File;
thumbnailSnapshot: Blob | undefined;
animationType?: MediaFileAnimationType;
progressCallback: (newState: UploaderState) => void;
}) => {
progressCallback({ status: UploaderStates.uploadingAsset });
const assetReponse = await uploadAsset({
file: assetFile,
title: title,
animationType: animationType,
});
if (!assetReponse.success || !assetReponse.data) {
progressCallback({
status: UploaderStates.assetError,
errorMessage: assetReponse.errorMessage,
});
return;
}
const assetToken = assetReponse.data;
if (!thumbnailSnapshot) {
progressCallback({ status: UploaderStates.success });
return;
}
progressCallback({ status: UploaderStates.uploadingCover });
const thumbnailResponse = await uploadSnapshotAsThumbnail({
assetTitle: title,
blob: thumbnailSnapshot,
});
if (!thumbnailResponse.success || !thumbnailResponse.data) {
progressCallback({
status: UploaderStates.coverCreateError,
errorMessage: thumbnailResponse.errorMessage,
});
return;
}
progressCallback({ status: UploaderStates.settingCover });
const thumbnailToken = thumbnailResponse.data;
const setThumbnailResponse = await setThumbnail({
assetToken: assetToken,
thumbnailToken: thumbnailToken,
});
if (!setThumbnailResponse.success) {
progressCallback({
status: UploaderStates.coverSetError,
errorMessage: setThumbnailResponse.errorMessage,
});
return;
}
progressCallback({ status: UploaderStates.success });
};
@@ -25,24 +25,23 @@ export const uploadAsset = async ({
maybe_animation_type: animationType,
uuid: uuidv4(),
});
case ".png":
case ".gif":
case ".jpg":
case ".jpeg":
return mediaUploadApi.UploadNewEngineAsset({
file: file,
fileName: file.name,
engine_category: FilterEngineCategories.IMAGE_PLANE,
maybe_animation_type: animationType,
maybe_title: title,
uuid: uuidv4(),
});
// case ".png":
// case ".gif":
// case ".jpg":
// case ".jpeg":
// return mediaUploadApi.UploadNewEngineAsset({
// file: file,
// fileName: file.name,
// engine_category: FilterEngineCategories.IMAGE_PLANE,
// maybe_animation_type: animationType,
// maybe_title: title,
// uuid: uuidv4(),
// });
default:
return mediaUploadApi.UploadNewEngineAsset({
file: file,
fileName: file.name,
engine_category: FilterEngineCategories.OBJECT,
maybe_animation_type: animationType,
maybe_title: title,
uuid: uuidv4(),
});
@@ -1,193 +0,0 @@
import { useEffect, useState } from "react";
import { v4 as uuidv4 } from "uuid";
import { TransitionDialogue, LoadingDots } from "~/components";
import { UploadAssetError } from "../UploadModal/UploadAssetError";
import { UploadSuccess } from "../UploadModal/UploadSuccess";
import { UploadFiles3D } from "./UploadFiles3D";
import { MediaFilesApi, MediaUploadApi } from "~/Classes/ApiManager";
import { FilterEngineCategories, MediaFileAnimationType } from "~/enums";
import { getFileName } from "~/utilities";
import { uploadAsset } from "./utilities";
interface Props {
onClose: () => void;
onSuccess: () => void;
isOpen: boolean;
title: string;
fileTypes: string[];
type: FilterEngineCategories;
options?: {
fileSubtypes?: { [key: string]: string }[];
hasLength?: boolean;
hasThumbnailUpload?: boolean;
};
}
enum UploaderState {
ready,
uploadingAsset,
uploadingCover,
settingCover,
success,
assetError,
coverCreateError,
coverSetError,
}
const initialState = {
status: UploaderState.ready,
};
export function UploadModal3DPreview({
isOpen,
onClose,
onSuccess,
title,
fileTypes,
type,
options,
}: Props) {
const [uploaderState, setUploaderState] = useState<{
status: UploaderState;
errorMessage?: string;
}>(initialState);
const resetModalState = () => {
setUploaderState(initialState);
};
useEffect(() => {
if (isOpen) {
resetModalState();
}
}, [isOpen]);
const onSubmit = async ({
title,
typeOption,
assetFile,
thumbnailFile,
}: {
title: string;
typeOption?: MediaFileAnimationType;
assetFile: File;
thumbnailFile: File | null;
}) => {
setUploaderState({ status: UploaderState.uploadingAsset });
const mediaUploadApi = new MediaUploadApi();
const assetReponse = await uploadAsset({
file: assetFile,
title: title,
animationType: typeOption,
});
if (!assetReponse.success || !assetReponse.data) {
setUploaderState({
status: UploaderState.assetError,
errorMessage: assetReponse.errorMessage,
});
return;
}
const assetToken = assetReponse.data;
if (!thumbnailFile) {
setUploaderState({ status: UploaderState.success });
return;
}
setUploaderState({ status: UploaderState.uploadingCover });
const thumbnailResponse = await mediaUploadApi.UploadImage({
uuid: uuidv4(),
blob: thumbnailFile,
fileName: getFileName(thumbnailFile),
maybe_title: "thumbnail_" + title,
});
if (!thumbnailResponse.success || !thumbnailResponse.data) {
setUploaderState({
status: UploaderState.coverCreateError,
errorMessage: thumbnailResponse.errorMessage,
});
return;
}
setUploaderState({ status: UploaderState.settingCover });
const thumbnailToken = thumbnailResponse.data;
const mediaFilesApi = new MediaFilesApi();
const setThumbnailResponse = await mediaFilesApi.UpdateCoverImage({
mediaFileToken: assetToken,
imageToken: thumbnailToken,
});
if (!setThumbnailResponse.success) {
setUploaderState({
status: UploaderState.coverSetError,
errorMessage: setThumbnailResponse.errorMessage,
});
return;
}
setUploaderState({ status: UploaderState.success });
};
const UploaderModalContent = () => {
switch (uploaderState.status) {
case UploaderState.ready:
return (
<UploadFiles3D
title={title}
fileTypes={fileTypes}
options={options}
onClose={onClose}
onSubmit={onSubmit}
/>
);
case UploaderState.uploadingAsset:
case UploaderState.uploadingCover:
case UploaderState.settingCover:
return (
<>
<LoadingDots className="mb-1 bg-transparent" />
<div className="w-100 text-center opacity-50">Uploading...</div>
</>
);
case UploaderState.success:
return (
<UploadSuccess
title={title}
onOk={() => {
onSuccess();
onClose();
}}
/>
);
case UploaderState.assetError:
return (
<UploadAssetError
onCancel={onClose}
onRetry={() => {
resetModalState();
}}
type={type}
errorMessage={uploaderState.errorMessage}
/>
);
case UploaderState.coverCreateError:
case UploaderState.coverSetError:
return (
<UploadAssetError
onCancel={onClose}
onRetry={() => {
resetModalState();
}}
type={"Thumbnail"}
errorMessage={uploaderState.errorMessage}
/>
);
}
};
return (
<TransitionDialogue isOpen={isOpen} onClose={onClose} title={title}>
<UploaderModalContent />
</TransitionDialogue>
);
}
@@ -1 +0,0 @@
export * from "./UploadModal3DPreview";
@@ -1,44 +1,23 @@
import { useState } from "react";
import { MediaFileAnimationType } from "~/enums";
import { Button, H6, Input, ListDropdown } from "~/components";
import { Button, H6, Input } from "~/components";
import { FileUploader } from "../UploadModal/FileUploader";
import { uploadImagePlane } from "./uploadImagePlane";
import { UploaderState } from "~/models";
interface Props {
title: string;
fileTypes: string[];
onClose: () => void;
options?: {
fileSubtypes?: { [key: string]: string }[];
hasLength?: boolean;
hasThumbnailUpload?: boolean;
};
onSubmit: (options: {
title: string;
typeOption?: MediaFileAnimationType;
assetFile: File;
length: number;
thumbnailFile: File | null;
}) => void;
onUploadProgress: (newState: UploaderState) => void;
}
export const UploadFilesImages = ({
fileTypes,
onClose,
title,
options,
onSubmit,
onUploadProgress,
}: Props) => {
const fileSubtypes = options?.fileSubtypes;
const [typeOption, setTypeOption] = useState<
MediaFileAnimationType | undefined
>(
fileSubtypes
? (Object.values(fileSubtypes[0])[0] as MediaFileAnimationType)
: undefined,
);
const [uploadTitle, setUploadTitle] = useState<{
value: string;
error?: string;
@@ -68,10 +47,10 @@ export const UploadFilesImages = ({
return;
}
onSubmit({
uploadImagePlane({
title: uploadTitle.value,
assetFile: assetFile.value,
typeOption,
progressCallback: onUploadProgress,
});
};
@@ -85,12 +64,6 @@ export const UploadFilesImages = ({
onChange={(event) => setUploadTitle({ value: event.target.value })}
className={uploadTitle.error ? "mb-3" : ""}
/>
{fileSubtypes && fileSubtypes.length > 1 && (
<ListDropdown
list={fileSubtypes}
onSelect={(value) => setTypeOption(value as MediaFileAnimationType)}
/>
)}
<FileUploader
title={title}
fileTypes={fileTypes}
@@ -1,15 +1,12 @@
import { useEffect, useState } from "react";
import { v4 as uuidv4 } from "uuid";
import { TransitionDialogue, LoadingDots } from "~/components";
import { UploadAssetError } from "../UploadModal/UploadAssetError";
import { UploadSuccess } from "../UploadModal/UploadSuccess";
import { UploadFilesImages } from "./UploadFilesImages";
import { MediaFilesApi, MediaUploadApi } from "~/Classes/ApiManager";
import { FilterEngineCategories, MediaFileAnimationType } from "~/enums";
import { getFileName } from "~/utilities";
import { FilterEngineCategories, UploaderStates } from "~/enums";
import { initialUploaderState, UploaderState } from "~/models";
interface Props {
onClose: () => void;
@@ -17,43 +14,26 @@ interface Props {
isOpen: boolean;
title: string;
fileTypes: string[];
type: FilterEngineCategories;
options?: {
fileSubtypes?: { [key: string]: string }[];
};
}
enum UploaderState {
ready,
uploadingAsset,
uploadingCover,
settingCover,
success,
assetError,
coverCreateError,
coverSetError,
}
const initialState = {
status: UploaderState.ready,
};
export function UploadModalImages({
isOpen,
onClose,
onSuccess,
title,
fileTypes,
type,
options,
}: Props) {
const [uploaderState, setUploaderState] = useState<{
status: UploaderState;
errorMessage?: string;
}>(initialState);
const [uploaderState, setUploaderState] =
useState<UploaderState>(initialUploaderState);
const updateUploaderState = (newState: UploaderState) => {
setUploaderState(newState);
};
const resetModalState = () => {
setUploaderState(initialState);
setUploaderState(initialUploaderState);
};
useEffect(() => {
@@ -62,87 +42,27 @@ export function UploadModalImages({
}
}, [isOpen]);
const onSubmit = async ({
title,
assetFile,
}: {
title: string;
typeOption?: MediaFileAnimationType;
assetFile: File;
thumbnailFile: File | null;
}) => {
setUploaderState({ status: UploaderState.uploadingAsset });
const mediaUploadApi = new MediaUploadApi();
const assetReponse = await mediaUploadApi.UploadNewEngineAsset({
file: assetFile,
fileName: assetFile.name,
engine_category: FilterEngineCategories.IMAGE_PLANE,
maybe_title: title,
uuid: uuidv4(),
});
if (!assetReponse.success || !assetReponse.data) {
setUploaderState({
status: UploaderState.assetError,
errorMessage: assetReponse.errorMessage,
});
return;
}
setUploaderState({ status: UploaderState.uploadingCover });
const thumbnailResponse = await mediaUploadApi.UploadImage({
uuid: uuidv4(),
blob: assetFile,
fileName: getFileName(assetFile),
maybe_title: "thumbnail_" + title,
});
if (!thumbnailResponse.success || !thumbnailResponse.data) {
setUploaderState({
status: UploaderState.coverCreateError,
errorMessage: thumbnailResponse.errorMessage,
});
return;
}
setUploaderState({ status: UploaderState.settingCover });
const mediaFilesApi = new MediaFilesApi();
const setThumbnailResponse = await mediaFilesApi.UpdateCoverImage({
mediaFileToken: assetReponse.data,
imageToken: thumbnailResponse.data,
});
if (!setThumbnailResponse.success) {
setUploaderState({
status: UploaderState.coverSetError,
errorMessage: setThumbnailResponse.errorMessage,
});
return;
}
setUploaderState({ status: UploaderState.success });
};
const UploaderModalContent = () => {
switch (uploaderState.status) {
case UploaderState.ready:
case UploaderStates.ready:
return (
<UploadFilesImages
title={title}
fileTypes={fileTypes}
options={options}
onClose={onClose}
onSubmit={onSubmit}
onUploadProgress={updateUploaderState}
/>
);
case UploaderState.uploadingAsset:
case UploaderState.uploadingCover:
case UploaderState.settingCover:
case UploaderStates.uploadingAsset:
case UploaderStates.uploadingCover:
case UploaderStates.settingCover:
return (
<>
<LoadingDots className="mb-1 bg-transparent" />
<div className="w-100 text-center opacity-50">Uploading...</div>
</>
);
case UploaderState.success:
case UploaderStates.success:
return (
<UploadSuccess
title={title}
@@ -152,19 +72,19 @@ export function UploadModalImages({
}}
/>
);
case UploaderState.assetError:
case UploaderStates.assetError:
return (
<UploadAssetError
onCancel={onClose}
onRetry={() => {
resetModalState();
}}
type={type}
type={FilterEngineCategories.IMAGE_PLANE}
errorMessage={uploaderState.errorMessage}
/>
);
case UploaderState.coverCreateError:
case UploaderState.coverSetError:
case UploaderStates.coverCreateError:
case UploaderStates.coverSetError:
return (
<UploadAssetError
onCancel={onClose}
@@ -0,0 +1,64 @@
import { v4 as uuidv4 } from "uuid";
import { UploaderStates, FilterEngineCategories } from "~/enums";
import { MediaUploadApi, MediaFilesApi } from "~/Classes/ApiManager";
import { getFileName } from "~/utilities";
import { UploaderState } from "~/models";
export const uploadImagePlane = async ({
title,
assetFile,
progressCallback,
}: {
title: string;
assetFile: File;
progressCallback: (newState: UploaderState) => void;
}) => {
progressCallback({ status: UploaderStates.uploadingAsset });
const mediaUploadApi = new MediaUploadApi();
const assetReponse = await mediaUploadApi.UploadNewEngineAsset({
file: assetFile,
fileName: assetFile.name,
engine_category: FilterEngineCategories.IMAGE_PLANE,
maybe_title: title,
uuid: uuidv4(),
});
if (!assetReponse.success || !assetReponse.data) {
progressCallback({
status: UploaderStates.assetError,
errorMessage: assetReponse.errorMessage,
});
return;
}
progressCallback({ status: UploaderStates.uploadingCover });
const thumbnailResponse = await mediaUploadApi.UploadImage({
uuid: uuidv4(),
blob: assetFile,
fileName: getFileName(assetFile),
maybe_title: "thumbnail_" + title,
});
if (!thumbnailResponse.success || !thumbnailResponse.data) {
progressCallback({
status: UploaderStates.coverCreateError,
errorMessage: thumbnailResponse.errorMessage,
});
return;
}
progressCallback({ status: UploaderStates.settingCover });
const mediaFilesApi = new MediaFilesApi();
const setThumbnailResponse = await mediaFilesApi.UpdateCoverImage({
mediaFileToken: assetReponse.data,
imageToken: thumbnailResponse.data,
});
if (!setThumbnailResponse.success) {
progressCallback({
status: UploaderStates.coverSetError,
errorMessage: setThumbnailResponse.errorMessage,
});
return;
}
progressCallback({ status: UploaderStates.success });
};
+1 -2
View File
@@ -7,7 +7,6 @@ export * from "./ButtonIconSelect";
export * from "./ButtonLink";
export * from "./ConfirmationModal";
export * from "./FileUploader";
export * from "./FileWrapper"; //need to rid of scss
export * from "./FilterButtons"; //this relies on a list of filters locally, could be more flexible
export * from "./Gravatar";
export * from "./Input"; //need to decouple hotkey signal
@@ -26,7 +25,7 @@ export * from "./Tooltip";
export * from "./TransitionDialogue"; //need to decouple hotkey signal
export * from "./Typography";
export * from "./UploadModal";
export * from "./UploadModal3DPreview";
export * from "./UploadModal3D";
export * from "./UploadModalImages";
export * from "./WaveformPlayer";
@@ -1,16 +0,0 @@
.obj-uploader-modal-load-view {
.uploader-dots {
background: transparent;
margin-bottom: 1rem;
}
.uploader-message {
width: 100%;
opacity: 0.5;
text-align: center;
}
}
.object-preview-canvas {
width: 100%;
overflow: hidden;
}
@@ -1,542 +0,0 @@
import { useCallback, useEffect, useState } from "react";
import {
Button,
Input,
Select,
TransitionDialogue,
LoadingDots,
} from "~/components";
import { v4 as uuidv4 } from "uuid";
import * as THREE from "three";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
// import { MMDLoader } from "three/addons/loaders/MMDLoader.js";
import { FontLoader } from "three/addons/loaders/FontLoader.js";
import { TextGeometry } from "three/addons/geometries/TextGeometry.js";
import {
AssetType,
FilterEngineCategories,
MediaFileAnimationType,
} from "~/enums";
import "./UploadModal.scss";
import { MediaFilesApi, MediaUploadApi } from "~/Classes/ApiManager";
import { getFileExtension, getFileName } from "~/utilities";
interface Props {
closeModal: () => void;
file: File;
onClose: () => void;
onSuccess: () => void;
isOpen: boolean;
resetModal: boolean;
setResetModal: (b: boolean) => void;
type: AssetType;
}
enum UploaderState {
ready,
uploadingAsset,
uploadingCover,
settingCover,
success,
assetError,
coverCreateError,
coverSetError,
loaderError,
}
export function UploadModalObject({
closeModal,
file,
isOpen,
onClose,
onSuccess,
resetModal,
setResetModal,
type,
}: Props) {
const [title, titleSet] = useState("");
// const [visibility, visibilitySet] = useState("public");
const [objUploadStatus, objUploadStatusSet] = useState(UploaderState.ready);
const [assetToken, assetTokenSet] = useState("");
const [coverToken, coverTokenSet] = useState("");
const [targetNode, targetNodeSet] = useState<HTMLCanvasElement | null>(null);
const [animationType, animationTypeSet] = useState<
MediaFileAnimationType | undefined
>(undefined);
const isCharacter = type === AssetType.CHARACTER;
const objPreviewRef = useCallback((node: HTMLCanvasElement) => {
if (node !== null) {
targetNodeSet(node);
}
}, []);
const resetModalState = useCallback(() => {
setResetModal(false);
titleSet("");
// visibilitySet("");
objUploadStatusSet(UploaderState.ready);
assetTokenSet("");
coverTokenSet("");
animationTypeSet(undefined);
}, [setResetModal]);
useEffect(() => {
// modal reset needed because we don't unmount components for some reason
if (resetModal) {
resetModalState();
}
if (targetNode !== null) {
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, 1, 0.1, 1000);
camera.position.z = 2;
const renderer = new THREE.WebGLRenderer({
alpha: true,
antialias: true,
canvas: targetNode,
preserveDrawingBuffer: true,
});
renderer.setSize(300 || 0, 300 || 0);
const color = 0xfcece7;
const light = new THREE.HemisphereLight(color, 0x8d8d8d, 3.0);
scene.add(light);
if (file) {
if (file.name.includes(".glb")) {
const loader = new GLTFLoader();
loader.load(
URL.createObjectURL(file),
(glb) => {
glb.scene.children.forEach((child) => {
child.userData["color"] = "#FFFFFF";
scene.add(child);
let maxSize = 2;
if (scene.children.length > 0) {
scene.children.forEach((child) => {
child.traverse((object: THREE.Object3D | THREE.Mesh) => {
// Assuming `object` is your Three.js object and you know it's a Mesh
if (object instanceof THREE.Mesh) {
object.geometry.computeBoundingBox();
const boundingBox = object.geometry.boundingBox;
const center = new THREE.Vector3();
boundingBox.getCenter(center);
const dimensions = new THREE.Vector3();
boundingBox.getSize(dimensions);
const maxDim = Math.max(
dimensions.x,
dimensions.y,
dimensions.z,
);
if (maxSize < maxDim) {
maxSize = maxDim;
camera.position.set(-maxDim, maxDim, maxDim);
camera.lookAt(center);
camera.updateProjectionMatrix();
}
}
});
});
}
renderer.render(scene, camera);
});
},
() => {},
(loaderError) => {
console.log("loader error:", loaderError);
objUploadStatusSet(UploaderState.loaderError);
},
);
} else if (file.name.includes(".pmd")) {
camera.position.z = 30;
const loader = new FontLoader();
loader.load(
"https://threejs.org/examples/fonts/helvetiker_regular.typeface.json",
function (font) {
const textGeometry = new TextGeometry("MMD", {
font: font,
size: 100,
depth: 5,
curveSegments: 12,
bevelEnabled: true,
bevelThickness: 1,
bevelSize: 1,
bevelOffset: 0,
bevelSegments: 5,
});
textGeometry.computeBoundingBox();
const textMaterial = new THREE.MeshPhongMaterial({
color: 0xffffff,
});
const textMesh = new THREE.Mesh(textGeometry, textMaterial);
textMesh.scale.set(0.15, 0.15, 0.01);
textMesh.position.set(-22, -5, 0);
scene.add(textMesh);
renderer.render(scene, camera);
},
);
} else if (
file.name.includes(".png") ||
file.name.includes(".jpg") ||
file.name.includes(".jpeg") ||
file.name.includes(".gif")
) {
const geometry = new THREE.PlaneGeometry(1, 1);
const loader = new THREE.TextureLoader();
const texture = loader.load(URL.createObjectURL(file));
texture.colorSpace = THREE.SRGBColorSpace;
console.log(texture);
const image_material = new THREE.MeshBasicMaterial({
color: 0xffffff,
map: texture,
});
const obj = new THREE.Mesh(geometry, image_material);
obj.receiveShadow = true;
obj.castShadow = true;
scene.add(obj);
}
const animate = function () {
renderer.render(scene, camera);
};
renderer.setAnimationLoop(animate);
}
}
}, [file, targetNode, resetModal, resetModalState]);
// for visibility later
// const visiblityOpts = [
// {
// label: "public",
// value: "public",
// },
// {
// label: "private",
// value: "private",
// },
// ];
// const visiblityId = `fy-uploader-modal-visibility-${useId()}`;
const setCoverImage = async (assetToken: string, thumbnailToken: string) => {
objUploadStatusSet(UploaderState.settingCover);
const mediaFilesApi = new MediaFilesApi();
const setThumbnailResponse = await mediaFilesApi.UpdateCoverImage({
mediaFileToken: assetToken,
imageToken: thumbnailToken,
});
if (!setThumbnailResponse.success) {
objUploadStatusSet(UploaderState.coverSetError);
return;
}
objUploadStatusSet(UploaderState.success);
};
const createCoverImage = async (assetToken: string) => {
if (!targetNode) {
//TODO: Verbose Error
return;
}
await targetNode.toBlob(async (blob: Blob | null) => {
if (!blob) {
//TODO: Verbose Error
return;
}
const thumbnailFile = new File([blob], "storyteller-cap.png");
objUploadStatusSet(UploaderState.uploadingCover);
const mediaUploadApi = new MediaUploadApi();
const thumbnailResponse = await mediaUploadApi.UploadImage({
uuid: uuidv4(),
blob: thumbnailFile,
fileName: getFileName(thumbnailFile),
maybe_title: "thumbnail_" + title,
});
if (!thumbnailResponse.success || !thumbnailResponse.data) {
objUploadStatusSet(UploaderState.coverCreateError);
return;
}
coverTokenSet(thumbnailResponse.data);
setCoverImage(assetToken, thumbnailResponse.data);
});
};
const uploadAsset = async () => {
objUploadStatusSet(UploaderState.uploadingAsset);
const mediaUploadApi = new MediaUploadApi();
const fileExtension = getFileExtension(file);
console.log(fileExtension);
const assetReponse = await (async () => {
switch (fileExtension) {
case ".zip":
return mediaUploadApi.UploadPmx({
file: file,
fileName: file.name,
engine_category: FilterEngineCategories.CHARACTER,
maybe_title: title,
maybe_animation_type: animationType,
uuid: uuidv4(),
});
case ".png":
case ".gif":
case ".jpg":
case ".jpeg":
return mediaUploadApi.UploadNewEngineAsset({
file: file,
fileName: file.name,
engine_category: FilterEngineCategories.IMAGE_PLANE,
maybe_animation_type: animationType,
maybe_title: title,
uuid: uuidv4(),
});
default:
return mediaUploadApi.UploadNewEngineAsset({
file: file,
fileName: file.name,
engine_category: isCharacter
? FilterEngineCategories.CHARACTER
: FilterEngineCategories.OBJECT,
maybe_animation_type: animationType,
maybe_title: title,
uuid: uuidv4(),
});
}
})();
if (!assetReponse.success || !assetReponse.data) {
objUploadStatusSet(UploaderState.assetError);
return;
}
assetTokenSet(assetReponse.data);
createCoverImage(assetReponse.data);
};
const animationOptions = [
{ label: "ARKit", value: MediaFileAnimationType.ArKit },
{
label: "MikuMikuDance",
value: MediaFileAnimationType.MikuMikuDance,
},
{
label: "MikuMikuDance ARKit",
value: MediaFileAnimationType.MikuMikuDanceArKit,
},
{ label: "Mixamo", value: MediaFileAnimationType.Mixamo },
{
label: "Mixamo ARKit",
value: MediaFileAnimationType.MixamoArKit,
},
{ label: "MocapNet", value: MediaFileAnimationType.MocapNet },
{
label: "MocapNet ARKit",
value: MediaFileAnimationType.MocapNetArKit,
},
{ label: "Move AI", value: MediaFileAnimationType.MoveAi },
{
label: "Move AI ARKit",
value: MediaFileAnimationType.MoveAiArKit,
},
{ label: "Rigify", value: MediaFileAnimationType.Rigify },
{
label: "Rigify ARKit",
value: MediaFileAnimationType.MoveAiArKit,
},
];
const objUploaderContent = () => {
switch (objUploadStatus) {
case UploaderState.ready:
return (
<>
<canvas
{...{
className: "object-preview-canvas m-auto",
ref: objPreviewRef,
}}
></canvas>
<Input
{...{
label: "Name",
onChange: ({ target }: React.ChangeEvent<HTMLInputElement>) =>
titleSet(target.value),
placeholder: "Enter a name",
value: title,
}}
/>
{isCharacter ? (
<>
<label
className="mb-2 mt-3 block"
htmlFor="upload-modal-animation-type-select"
>
Animation type
</label>
<Select
options={animationOptions}
onChange={(value: MediaFileAnimationType) => {
animationTypeSet(value);
}}
placeholder="Select an animation type (optional)"
value={animationType}
/>
</>
) : null}
<div className="mt-6 flex justify-end gap-2">
<Button
{...{
onClick: () => {
closeModal();
onClose();
},
variant: "secondary",
}}
>
Cancel
</Button>
<Button
{...{
onClick: () => uploadAsset(),
}}
>
Upload
</Button>
</div>
</>
);
case UploaderState.uploadingAsset:
case UploaderState.uploadingCover:
case UploaderState.settingCover:
return (
<div {...{ className: "obj-uploader-modal-load-view" }}>
<LoadingDots {...{ className: "uploader-dots" }} />
<div {...{ className: "uploader-message" }}>Uploading...</div>
</div>
);
case UploaderState.success:
return (
<div {...{ className: "obj-uploader-modal-success-view" }}>
<div
{...{
className: "uploader-message",
}}
>{`Added ${title} to your library`}</div>
<div className="mt-6 flex justify-end gap-2">
<Button
{...{
onClick: () => {
onSuccess();
closeModal();
onClose();
},
variant: "primary",
}}
>
Ok
</Button>
</div>
</div>
);
case UploaderState.assetError:
case UploaderState.coverCreateError:
case UploaderState.coverSetError:
return (
<>
{objUploadStatus === UploaderState.assetError
? "There was a problem uploading your object."
: "There was a problem creating art for your object"}
<div className="mt-6 flex justify-end gap-2">
<Button
{...{
onClick: () => {
closeModal();
onClose();
},
variant: "secondary",
}}
>
Cancel
</Button>
<Button
{...{
onClick: () => {
switch (objUploadStatus) {
case UploaderState.assetError: {
uploadAsset();
break;
}
case UploaderState.coverCreateError: {
createCoverImage(assetToken);
break;
}
case UploaderState.coverSetError: {
setCoverImage(assetToken, coverToken);
break;
}
}
},
variant: "primary",
}}
>
Try again
</Button>
</div>
</>
);
case UploaderState.loaderError:
return (
<>
There was a problem loading your object. Try another.
<div className="mt-6 flex justify-end gap-2">
<Button
{...{
onClick: () => {
closeModal();
onClose();
},
variant: "secondary",
}}
>
Cancel
</Button>
<Button
{...{
onClick: () => {
objUploadStatusSet(UploaderState.ready);
resetModalState();
},
variant: "primary",
}}
>
Try another
</Button>
</div>
</>
);
}
};
return (
<TransitionDialogue
{...{
isOpen,
onClose: () => {
closeModal();
onClose();
resetModalState();
},
title: `Upload ${isCharacter ? "Character" : "Object"}`,
}}
>
{objUploaderContent()}
</TransitionDialogue>
);
}
@@ -1,81 +0,0 @@
import * as THREE from "three";
// from here https://wejn.org/2020/12/cracking-the-threejs-object-fitting-nut/
export const fitCameraToCenteredObject = function (
camera: THREE.PerspectiveCamera,
object: THREE.Object3D<THREE.Object3DEventMap>,
offset?: number,
orbitControls?: { target: THREE.Vector3; maxDistance: number },
) {
const boundingBox = new THREE.Box3();
boundingBox.setFromObject(object);
const size = new THREE.Vector3();
boundingBox.getSize(size);
// figure out how to fit the box in the view:
// 1. figure out horizontal FOV (on non-1.0 aspects)
// 2. figure out distance from the object in X and Y planes
// 3. select the max distance (to fit both sides in)
//
// The reason is as follows:
//
// Imagine a bounding box (BB) is centered at (0,0,0).
// Camera has vertical FOV (camera.fov) and horizontal FOV
// (camera.fov scaled by aspect, see fovh below)
//
// Therefore if you want to put the entire object into the field of view,
// you have to compute the distance as: z/2 (half of Z size of the BB
// protruding towards us) plus for both X and Y size of BB you have to
// figure out the distance created by the appropriate FOV.
//
// The FOV is always a triangle:
//
// (size/2)
// +--------+
// | /
// | /
// | /
// | F° /
// | /
// | /
// | /
// |/
//
// F° is half of respective FOV, so to compute the distance (the length
// of the straight line) one has to: `size/2 / Math.tan(F)`.
//
// FTR, from https://threejs.org/docs/#api/en/cameras/PerspectiveCamera
// the camera.fov is the vertical FOV.
const fov = camera.fov * (Math.PI / 180);
const fovh = 2 * Math.atan(Math.tan(fov / 2) * camera.aspect);
const dx = size.z / 2 + Math.abs(size.x / 2 / Math.tan(fovh / 2));
const dy = size.z / 2 + Math.abs(size.y / 2 / Math.tan(fov / 2));
let cameraZ = Math.max(dx, dy);
// offset the camera, if desired (to avoid filling the whole canvas)
if (offset !== undefined && offset !== 0) cameraZ *= offset;
camera.position.set(
0,
0.75, // this centers it vertically enough while testing, but probably needs to be dynamic
cameraZ,
);
// set the far plane of the camera so that it easily encompasses the whole object
const minZ = boundingBox.min.z;
const cameraToFarEdge = minZ < 0 ? -minZ + cameraZ : cameraZ - minZ;
camera.far = cameraToFarEdge * 3;
camera.updateProjectionMatrix();
if (orbitControls !== undefined) {
// set camera to rotate around the center
orbitControls.target = new THREE.Vector3(0, 0, 0);
// prevent camera from zooming out far enough to create far plane cutoff
orbitControls.maxDistance = cameraToFarEdge * 2;
}
};
@@ -1 +0,0 @@
export * from "./UploadModalObject";
-1
View File
@@ -6,4 +6,3 @@ export * from "./PremiumLock";
// all the Upload stuff will depend on the API Manager Signal
export * from "./UploadAudioButtonDialogue";
export * from "./UploadAudioComponent";
export * from "./UploadModalObject"; //need to rid of scss
+1
View File
@@ -1,4 +1,5 @@
export enum FeatureFlags {
DEV_ONLY = "dev-only",
SHOW_SEARCH_OBJECTS = "show-search-objects",
SHOW_IMAGEPLANE_TAB = "show-imageplane-tab",
SHOW_SETS_TAB = "show-sets-tab",
+17 -2
View File
@@ -1,3 +1,8 @@
export enum ANIMATION_FILE_TYPE {
GLB = "GLB",
VMD = "VMD",
}
export enum AUDIO_FILE_TYPE {
MP3 = "MP3",
WAV = "WAV",
@@ -5,7 +10,13 @@ export enum AUDIO_FILE_TYPE {
OGG = "OGG",
}
export enum IMAGE_FILE_TYPE {
export enum IMAGEPLANE_FILE_TYPE {
PNG = "PNG",
JPG = "JPG",
JPEG = "JPEG",
}
export enum THUMBNAILS_FILE_TYPE {
PNG = "PNG",
JPG = "JPG",
JPEG = "JPEG",
@@ -14,5 +25,9 @@ export enum IMAGE_FILE_TYPE {
export enum OBJECT_FILE_TYPE {
GLB = "GLB",
FBX = "FBX",
}
export enum CHARACTER_FILE_TYPE {
PMD = "PMD",
ZIP = "ZIP",
}
@@ -1,4 +1,4 @@
export enum UploaderState {
export enum UploaderStates {
ready,
uploadingAsset,
uploadingCover,
+1 -1
View File
@@ -12,5 +12,5 @@ export * from "./MediaFileAnimationTypes";
export * from "./QueryFilters";
export * from "./Tabs";
export * from "./toasts";
export * from "./UploaderState";
export * from "./UploaderStates";
export * from "./Visibility";
+10
View File
@@ -0,0 +1,10 @@
import { UploaderStates } from "~/enums";
export interface UploaderState {
status: UploaderStates;
errorMessage?: string;
}
export const initialUploaderState = {
status: UploaderStates.ready,
};
+1
View File
@@ -2,5 +2,6 @@ export * from "./Billing";
export * from "./BetaKeys";
export * from "./Job";
export * from "./JobState";
export * from "./UploaderState";
export * from "./Users";
export * from "./Weight";
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useState } from "react";
import { useSignals } from "@preact/signals-react/runtime";
import { faCirclePlus } from "@fortawesome/pro-solid-svg-icons";
import {
ANIMATION_FILE_TYPE,
AssetFilterOption,
FeatureFlags,
FilterEngineCategories,
@@ -267,7 +268,7 @@ export const AnimationTab = () => {
onClose={() => setOpen(false)}
onSuccess={fetchUserAnimations}
isOpen={open}
fileTypes={["GLB", "FBX", "VMD"]}
fileTypes={Object.values(ANIMATION_FILE_TYPE)}
title="Upload Animation"
options={{
fileSubtypes: [
@@ -3,18 +3,19 @@ import { useSignals } from "@preact/signals-react/runtime";
import { faCirclePlus } from "@fortawesome/pro-solid-svg-icons";
import {
AssetFilterOption,
AssetType,
CHARACTER_FILE_TYPE,
FeatureFlags,
FilterEngineCategories,
MediaFileAnimationType,
} from "~/enums";
import { FetchStatus } from "~/pages/PageEnigma/enums";
import { characterItems as demoCharacterItems } from "~/pages/PageEnigma/signals";
import {
Button,
FileWrapper,
FilterButtons,
Pagination,
SearchFilter,
UploadModal,
} from "~/components";
import {
ItemElements,
@@ -37,6 +38,8 @@ export const CharactersTab = () => {
FeatureFlags.SHOW_SEARCH_OBJECTS,
);
const [openUploadModal, setOpenUploadModal] = useState(false);
const [searchTermFeatured, setSearchTermFeatured] = useState("");
const [searchTermUser, setSearchTermUser] = useState("");
@@ -199,80 +202,90 @@ export const CharactersTab = () => {
return (
<>
<FileWrapper
onSuccess={fetchUserCharacters}
type={AssetType.CHARACTER}
render={(parentId) => (
<>
<TabTitle title="Characters" />
<div>
<FilterButtons
value={selectedFilter}
onClick={(button) => {
setSelectedFilter(Number(button));
setCurrentPage(0);
}}
/>
</div>
<div className="flex w-full flex-col gap-3 px-4">
<Button
className="file-picker-button py-3"
htmlFor={parentId}
icon={faCirclePlus}
variant="action"
>
Upload Character
</Button>
{showSearchObjectComponent && (
<SearchFilter
searchTerm={
selectedFilter === AssetFilterOption.FEATURED
? searchTermFeatured
: searchTermUser
}
onSearchChange={
selectedFilter === AssetFilterOption.FEATURED
? setSearchTermFeatured
: setSearchTermUser
}
key={selectedFilter}
placeholder={
selectedFilter === AssetFilterOption.FEATURED
? "Search featured characters"
: "Search my characters"
}
/>
)}
</div>
<div className="w-full grow overflow-y-auto rounded px-4 pb-4">
<ItemElements
busy={isFetching}
debug="characters tab"
currentPage={currentPage}
pageSize={pageSize}
items={
selectedFilter === AssetFilterOption.FEATURED
? searchTermFeatured
? featuredSearchResults ?? []
: displayedItems
: searchTermUser
? userSearchResults ?? []
: displayedItems
}
/>
</div>
{totalPages > 1 && (
<Pagination
className="-mt-4 px-4"
currentPage={currentPage}
totalPages={totalPages}
onPageChange={(newPage: number) => {
setCurrentPage(newPage);
}}
/>
)}
</>
<TabTitle title="Characters" />
<div>
<FilterButtons
value={selectedFilter}
onClick={(button) => {
setSelectedFilter(Number(button));
setCurrentPage(0);
}}
/>
</div>
<div className="flex w-full flex-col gap-3 px-4">
<Button
icon={faCirclePlus}
variant="action"
onClick={() => setOpenUploadModal(true)}
className="w-full py-3 text-sm font-medium"
>
Upload Characters
</Button>
{showSearchObjectComponent && (
<SearchFilter
searchTerm={
selectedFilter === AssetFilterOption.FEATURED
? searchTermFeatured
: searchTermUser
}
onSearchChange={
selectedFilter === AssetFilterOption.FEATURED
? setSearchTermFeatured
: setSearchTermUser
}
key={selectedFilter}
placeholder={
selectedFilter === AssetFilterOption.FEATURED
? "Search featured characters"
: "Search my characters"
}
/>
)}
</div>
<div className="w-full grow overflow-y-auto rounded px-4 pb-4">
<ItemElements
busy={isFetching}
debug="characters tab"
currentPage={currentPage}
pageSize={pageSize}
items={
selectedFilter === AssetFilterOption.FEATURED
? searchTermFeatured
? featuredSearchResults ?? []
: displayedItems
: searchTermUser
? userSearchResults ?? []
: displayedItems
}
/>
</div>
{totalPages > 1 && (
<Pagination
className="-mt-4 px-4"
currentPage={currentPage}
totalPages={totalPages}
onPageChange={(newPage: number) => {
setCurrentPage(newPage);
}}
/>
)}
<UploadModal
onClose={() => setOpenUploadModal(false)}
onSuccess={fetchUserCharacters}
isOpen={openUploadModal}
type={FilterEngineCategories.CHARACTER}
options={{
hasThumbnailUpload: true,
fileSubtypes: [
{ Mixamo: MediaFileAnimationType.Mixamo },
{ MikuMikuDance: MediaFileAnimationType.MikuMikuDance },
{ MoveAi: MediaFileAnimationType.MoveAi },
{ Rigify: MediaFileAnimationType.Rigify },
{ Rokoko: MediaFileAnimationType.Rokoko },
],
}}
fileTypes={Object.values(CHARACTER_FILE_TYPE)}
title="Upload Characters"
/>
</>
);
@@ -36,6 +36,8 @@ export const ExpressionTab = () => {
FeatureFlags.SHOW_SEARCH_OBJECTS,
);
const showUploadButton = usePosthogFeatureFlag(FeatureFlags.DEV_ONLY);
const [open, setOpen] = useState(false);
const [searchTermFeatured, setSearchTermFeatured] = useState("");
const [searchTermUser, setSearchTermUser] = useState("");
@@ -197,14 +199,16 @@ export const ExpressionTab = () => {
}}
/>
<div className="flex w-full flex-col gap-3 px-4">
<Button
icon={faCirclePlus}
variant="action"
onClick={() => setOpen(true)}
className="w-full py-3 text-sm font-medium"
>
Upload Expression (Dev Only)
</Button>
{showUploadButton && (
<Button
icon={faCirclePlus}
variant="action"
onClick={() => setOpen(true)}
className="w-full py-3 text-sm font-medium"
>
Upload Expression (Dev Only)
</Button>
)}
{showSearchObjectComponent && (
<SearchFilter
searchTerm={
@@ -4,7 +4,7 @@ import {
AssetFilterOption,
FeatureFlags,
FilterEngineCategories,
IMAGE_FILE_TYPE,
IMAGEPLANE_FILE_TYPE,
} from "~/enums";
import { FetchStatus } from "~/pages/PageEnigma/enums";
import {
@@ -246,9 +246,8 @@ export const ImagePlanesTab = () => {
onClose={() => setOpenUploadModal(false)}
onSuccess={fetchUserImages}
isOpen={openUploadModal}
fileTypes={Object.values(IMAGE_FILE_TYPE)}
fileTypes={Object.values(IMAGEPLANE_FILE_TYPE)}
title="Upload Image Panels"
type={FilterEngineCategories.IMAGE_PLANE}
/>
</>
);
@@ -4,9 +4,9 @@ import { useSignals } from "@preact/signals-react/runtime";
import { faCirclePlus } from "@fortawesome/pro-solid-svg-icons";
import {
AssetFilterOption,
AssetType,
FeatureFlags,
FilterEngineCategories,
OBJECT_FILE_TYPE,
} from "~/enums";
import { FetchStatus } from "~/pages/PageEnigma/enums";
import { shapeItems } from "~/pages/PageEnigma/signals";
@@ -17,10 +17,10 @@ import {
} from "~/pages/PageEnigma/comps/SidePanelTabs/sharedComps";
import {
Button,
FileWrapper,
FilterButtons,
SearchFilter,
Pagination,
UploadModal3D,
} from "~/components";
import {
fetchFeaturedMediaItems,
@@ -38,6 +38,8 @@ export const ObjectsTab = () => {
FeatureFlags.SHOW_SEARCH_OBJECTS,
);
const [openUploadModal, setOpenUploadModal] = useState(false);
const [searchTermFeatured, setSearchTermFeatured] = useState("");
const [searchTermUser, setSearchTermUser] = useState("");
@@ -188,78 +190,79 @@ export const ObjectsTab = () => {
]);
return (
<FileWrapper
onSuccess={fetchUserObjects}
type={AssetType.OBJECT}
render={(parentId) => (
<>
<TabTitle title="Objects" />
<FilterButtons
value={selectedFilter}
onClick={(button) => {
setSelectedFilter(Number(button));
setCurrentPage(0);
}}
<>
<TabTitle title="Objects" />
<FilterButtons
value={selectedFilter}
onClick={(button) => {
setSelectedFilter(Number(button));
setCurrentPage(0);
}}
/>
<div className="flex w-full flex-col gap-3 px-4">
<Button
icon={faCirclePlus}
variant="action"
onClick={() => setOpenUploadModal(true)}
className="w-full py-3 text-sm font-medium"
>
Upload Objects
</Button>
{showSearchObjectComponent && (
<SearchFilter
searchTerm={
selectedFilter === AssetFilterOption.FEATURED
? searchTermFeatured
: searchTermUser
}
onSearchChange={
selectedFilter === AssetFilterOption.FEATURED
? setSearchTermFeatured
: setSearchTermUser
}
key={selectedFilter}
placeholder={
selectedFilter === AssetFilterOption.FEATURED
? "Search featured objects"
: "Search my objects"
}
/>
<div className="flex w-full flex-col gap-3 px-4">
<Button
className="file-picker-button py-3"
htmlFor={parentId}
icon={faCirclePlus}
variant="action"
>
Upload Object
</Button>
{showSearchObjectComponent && (
<SearchFilter
searchTerm={
selectedFilter === AssetFilterOption.FEATURED
? searchTermFeatured
: searchTermUser
}
onSearchChange={
selectedFilter === AssetFilterOption.FEATURED
? setSearchTermFeatured
: setSearchTermUser
}
key={selectedFilter}
placeholder={
selectedFilter === AssetFilterOption.FEATURED
? "Search featured objects"
: "Search my objects"
}
/>
)}
</div>
<div className="w-full grow overflow-y-auto rounded px-4 pb-4">
<ItemElements
busy={isFetching}
debug="objects tab"
currentPage={currentPage}
pageSize={pageSize}
items={
selectedFilter === AssetFilterOption.FEATURED
? searchTermFeatured
? featuredSearchResults ?? []
: displayedItems
: searchTermUser
? userSearchResults ?? []
: displayedItems
}
/>
</div>
{totalPages > 1 && (
<Pagination
className="-mt-4 px-4"
currentPage={currentPage}
totalPages={totalPages}
onPageChange={(newPage: number) => {
setCurrentPage(newPage);
}}
/>
)}
</>
)}
</div>
<div className="w-full grow overflow-y-auto rounded px-4 pb-4">
<ItemElements
busy={isFetching}
debug="objects tab"
currentPage={currentPage}
pageSize={pageSize}
items={
selectedFilter === AssetFilterOption.FEATURED
? searchTermFeatured
? featuredSearchResults ?? []
: displayedItems
: searchTermUser
? userSearchResults ?? []
: displayedItems
}
/>
</div>
{totalPages > 1 && (
<Pagination
className="-mt-4 px-4"
currentPage={currentPage}
totalPages={totalPages}
onPageChange={(newPage: number) => {
setCurrentPage(newPage);
}}
/>
)}
/>
<UploadModal3D
onClose={() => setOpenUploadModal(false)}
onSuccess={fetchUserObjects}
isOpen={openUploadModal}
fileTypes={Object.values(OBJECT_FILE_TYPE)}
title="Upload Objects"
/>
</>
);
};
@@ -3,9 +3,9 @@ import { faCirclePlus } from "@fortawesome/pro-solid-svg-icons";
import {
AssetFilterOption,
CHARACTER_FILE_TYPE,
FeatureFlags,
FilterEngineCategories,
IMAGE_FILE_TYPE,
OBJECT_FILE_TYPE,
} from "~/enums";
import { FetchStatus } from "~/pages/PageEnigma/enums";
@@ -15,7 +15,7 @@ import {
FilterButtons,
Pagination,
SearchFilter,
UploadModal3DPreview,
UploadModal3D,
} from "~/components";
import {
@@ -111,7 +111,10 @@ export const SetsTab = () => {
const fetchFeaturedSetObjects = useCallback(
() =>
fetchFeaturedMediaItems({
filterEngineCategories: [FilterEngineCategories.OBJECT],
filterEngineCategories: [
FilterEngineCategories.OBJECT,
FilterEngineCategories.IMAGE_PLANE,
],
setState: (newState: FetchMediaItemStates) => {
setFeaturedFetch((curr) => ({
status: newState.status,
@@ -257,17 +260,15 @@ export const SetsTab = () => {
}}
/>
)}
<UploadModal3DPreview
<UploadModal3D
onClose={() => setOpenUploadModal(false)}
onSuccess={fetchUserSetObjects}
isOpen={openUploadModal}
fileTypes={[
...Object.values(OBJECT_FILE_TYPE),
"PMD",
...Object.values(IMAGE_FILE_TYPE),
...Object.values(CHARACTER_FILE_TYPE),
]}
title="Upload Set Objects"
type={FilterEngineCategories.OBJECT}
/>
</>
);