diff --git a/frontend/apps/editor3d/app/public/resources/memes/doge.jpg b/frontend/apps/editor3d/app/public/resources/memes/doge.jpg new file mode 100644 index 0000000000..aadbba17df Binary files /dev/null and b/frontend/apps/editor3d/app/public/resources/memes/doge.jpg differ diff --git a/frontend/apps/editor3d/app/public/resources/memes/grimace.jpg b/frontend/apps/editor3d/app/public/resources/memes/grimace.jpg new file mode 100644 index 0000000000..5dba23f510 Binary files /dev/null and b/frontend/apps/editor3d/app/public/resources/memes/grimace.jpg differ diff --git a/frontend/apps/editor3d/app/public/resources/memes/jd_vance.jpg b/frontend/apps/editor3d/app/public/resources/memes/jd_vance.jpg new file mode 100644 index 0000000000..b32376fc27 Binary files /dev/null and b/frontend/apps/editor3d/app/public/resources/memes/jd_vance.jpg differ diff --git a/frontend/apps/editor3d/app/public/resources/memes/lil_jd.jpg b/frontend/apps/editor3d/app/public/resources/memes/lil_jd.jpg new file mode 100644 index 0000000000..01d26978da Binary files /dev/null and b/frontend/apps/editor3d/app/public/resources/memes/lil_jd.jpg differ diff --git a/frontend/apps/editor3d/app/public/resources/memes/roblox.jpg b/frontend/apps/editor3d/app/public/resources/memes/roblox.jpg new file mode 100644 index 0000000000..595a0234be Binary files /dev/null and b/frontend/apps/editor3d/app/public/resources/memes/roblox.jpg differ diff --git a/frontend/apps/editor3d/app/public/resources/memes/toothless.jpg b/frontend/apps/editor3d/app/public/resources/memes/toothless.jpg new file mode 100644 index 0000000000..1eb4fa52b9 Binary files /dev/null and b/frontend/apps/editor3d/app/public/resources/memes/toothless.jpg differ diff --git a/frontend/apps/editor3d/app/src/components/reusable/UploadModal3D/UploadFiles3D.tsx b/frontend/apps/editor3d/app/src/components/reusable/UploadModal3D/UploadFiles3D.tsx index d4aac54682..a4dd7dc55d 100644 --- a/frontend/apps/editor3d/app/src/components/reusable/UploadModal3D/UploadFiles3D.tsx +++ b/frontend/apps/editor3d/app/src/components/reusable/UploadModal3D/UploadFiles3D.tsx @@ -82,6 +82,18 @@ export const UploadFiles3D = ({ }); }; + useEffect(() => { + // Reset subtype when fileSubtypes set changes (e.g., toggling character) + if (!fileSubtypes || fileSubtypes.length === 0) { + setSubtype(undefined); + return; + } + const nextDefault = Object.values(fileSubtypes[0])[0] as + | MediaFileAnimationType + | undefined; + setSubtype(nextDefault); + }, [fileSubtypes]); + useEffect(() => { if (canvasRef.current && assetFile.value) { loadPreviewOnCanvas({ diff --git a/frontend/apps/editor3d/app/src/components/reusable/UploadModal3D/UploadModal3D.tsx b/frontend/apps/editor3d/app/src/components/reusable/UploadModal3D/UploadModal3D.tsx index 15be5f9f2e..5d7750bc5f 100644 --- a/frontend/apps/editor3d/app/src/components/reusable/UploadModal3D/UploadModal3D.tsx +++ b/frontend/apps/editor3d/app/src/components/reusable/UploadModal3D/UploadModal3D.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { LoadingDots } from "@storyteller/ui-loading"; import { Modal } from "@storyteller/ui-modal"; import { UploadAssetError, UploadSuccess } from "@storyteller/ui-upload-modal"; @@ -8,6 +8,7 @@ import { FilterEngineCategories, UploaderStates, OBJECT_FILE_TYPE, + MediaFileAnimationType, } from "../../../enums"; import { IconDefinition } from "@fortawesome/fontawesome-svg-core"; import { @@ -34,9 +35,26 @@ export function UploadModal3D(props: Props) { const { isOpen, onClose, onSuccess, title, titleIcon, options } = props; const [uploaderState, setUploaderState] = useState(initialUploaderState); + const [isCharacter, setIsCharacter] = useState(false); - // Category fixed to OBJECT - const selectedCategory = FilterEngineCategories.OBJECT; + const selectedCategory = isCharacter + ? FilterEngineCategories.CHARACTER + : FilterEngineCategories.OBJECT; + + const characterAnimationOptions = useMemo(() => { + if (!isCharacter) return undefined; + const values = Object.values(MediaFileAnimationType); + const sorted = values.sort((a, b) => + a === MediaFileAnimationType.MixamoArKit + ? -1 + : b === MediaFileAnimationType.MixamoArKit + ? 1 + : a.localeCompare(b), + ); + const toLabel = (v: string) => + v.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()); + return sorted.map((v) => ({ [toLabel(v)]: v })); + }, [isCharacter]); const updateUploaderState = (newState: UploaderState) => { setUploaderState(newState); @@ -49,6 +67,7 @@ export function UploadModal3D(props: Props) { useEffect(() => { if (isOpen) { resetModalState(); + setIsCharacter(false); } }, [isOpen]); @@ -69,11 +88,23 @@ export function UploadModal3D(props: Props) { case UploaderStates.ready: return (
+
+ setIsCharacter(e.target.checked)} + /> + +
diff --git a/frontend/apps/editor3d/app/src/components/reusable/UploadModal3D/utilities/uploadAsset.ts b/frontend/apps/editor3d/app/src/components/reusable/UploadModal3D/utilities/uploadAsset.ts index 163b74681f..b56cce37e6 100644 --- a/frontend/apps/editor3d/app/src/components/reusable/UploadModal3D/utilities/uploadAsset.ts +++ b/frontend/apps/editor3d/app/src/components/reusable/UploadModal3D/utilities/uploadAsset.ts @@ -33,6 +33,7 @@ export const uploadAsset = async ({ fileName: file.name, engine_category: engineCategory, maybe_title: title, + maybe_animation_type: animationType, uuid: uuidv4(), }); } diff --git a/frontend/apps/editor3d/app/src/pages/PageEnigma/PageEditor.tsx b/frontend/apps/editor3d/app/src/pages/PageEnigma/PageEditor.tsx index c8018cdcf4..9968bb9ace 100644 --- a/frontend/apps/editor3d/app/src/pages/PageEnigma/PageEditor.tsx +++ b/frontend/apps/editor3d/app/src/pages/PageEnigma/PageEditor.tsx @@ -55,6 +55,7 @@ import { } from "~/components/signaled/TopBar/TopBar"; import { uploadPlaneFromMediaToken } from "~/components/reusable/UploadModalMedia/uploadPlane"; import { addObject } from "./signals/objectGroup/addObject"; +import { addCharacter } from "./signals/characterGroups/addCharacter"; import { AssetType, AUTH_STATUS } from "~/enums"; import { v4 as uuidv4 } from "uuid"; import { MediaItem } from "~/pages/PageEnigma/models"; @@ -77,7 +78,7 @@ import { useTabStore } from "../Stores/TabState"; import PageEdit from "../PageEdit/PageEdit"; import { ImageModel } from "@storyteller/model-list"; -const PAGE_ID : ModelPage = ModelPage.Stage3D; +const PAGE_ID: ModelPage = ModelPage.Stage3D; export const PageEditor = () => { useSignals(); @@ -103,7 +104,8 @@ export const PageEditor = () => { }; }, []); - const selectedImageModel : ImageModel | undefined = getSelectedImageModel(PAGE_ID); + const selectedImageModel: ImageModel | undefined = + getSelectedImageModel(PAGE_ID); const height = dndTimelineHeight.value > -1 @@ -305,11 +307,12 @@ export const PageEditor = () => { try { if (item.mediaClass === "dimensional") { // Handle 3D models + const isCharacter = item.assetType === "character"; const mediaItem: MediaItem = { version: 1, - type: AssetType.OBJECT, + type: isCharacter ? AssetType.CHARACTER : AssetType.OBJECT, media_id: item.id || uuidv4(), - name: item.label || "3D Object", + name: item.label || (isCharacter ? "Character" : "3D Object"), ...(worldPosition && { position: { x: worldPosition.x, @@ -318,7 +321,11 @@ export const PageEditor = () => { }, }), }; - addObject(mediaItem); + if (isCharacter) { + addCharacter(mediaItem); + } else { + addObject(mediaItem); + } } else { // Handle images (create image planes) const mediaItem: MediaItem = { diff --git a/frontend/apps/editor3d/app/src/pages/PageEnigma/comps/AssetMenu/AssetModal.tsx b/frontend/apps/editor3d/app/src/pages/PageEnigma/comps/AssetMenu/AssetModal.tsx index e8cd102cff..2376d33cf7 100644 --- a/frontend/apps/editor3d/app/src/pages/PageEnigma/comps/AssetMenu/AssetModal.tsx +++ b/frontend/apps/editor3d/app/src/pages/PageEnigma/comps/AssetMenu/AssetModal.tsx @@ -10,6 +10,7 @@ import { faChevronRight, faMountainCity, faDog, + faFaceGrinStars, faUpFromLine, } from "@fortawesome/pro-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -29,6 +30,7 @@ import { demoSkyboxItems, demoShapeItems, demoCharacterItems, + demoMemeItems, assetModalVisibleDuringDrag, reopenAfterDragSignal, assetModalVisible, @@ -246,10 +248,10 @@ export const AssetModal = () => { featuredImagePlanesFetchStatus, ]); - const libraryTabs = [ - { id: "library", label: "Library" }, - { id: "mine", label: "Mine" }, - ]; + // const libraryTabs = [ + // { id: "library", label: "Library" }, + // { id: "mine", label: "Mine" }, + // ]; const assetTabs = useMemo( () => [ @@ -276,6 +278,14 @@ export const AssetModal = () => { ? [...demoShapeItems.value, ...(featuredObjects ?? [])] : (userObjects ?? []), }, + { + id: "memes", + label: "Memes", + labelSingle: "Meme", + icon: faFaceGrinStars, + engineCategory: FilterEngineCategories.CHARACTER, + items: activeLibraryTab === "library" ? demoMemeItems.value : [], + }, { id: "sets", label: "Sets", @@ -323,6 +333,7 @@ export const AssetModal = () => { demoCharacterItems.value, demoShapeItems.value, demoSkyboxItems.value, + demoMemeItems.value, featuredCharacters, featuredCreatures, featuredObjects, @@ -426,19 +437,19 @@ export const AssetModal = () => { ); }; - const handleAddAsset = () => { - handleClose(); - setIsUploadModalOpen(true); - setSelectedCategory(null); - setIsSelectVisible(true); - }; + // const handleAddAsset = () => { + // handleClose(); + // setIsUploadModalOpen(true); + // setSelectedCategory(null); + // setIsSelectVisible(true); + // }; - const handleAddSpecificAsset = (category: FilterEngineCategories) => { - handleClose(); - setIsUploadModalOpen(true); - setSelectedCategory(category); - setIsSelectVisible(false); - }; + // const handleAddSpecificAsset = (category: FilterEngineCategories) => { + // handleClose(); + // setIsUploadModalOpen(true); + // setSelectedCategory(category); + // setIsSelectVisible(false); + // }; const handleUploadSuccess = (category: FilterEngineCategories) => { if (reopenAfterDragSignal.value) { diff --git a/frontend/apps/editor3d/app/src/pages/PageEnigma/signals/demoAssets/demoMemeItems.ts b/frontend/apps/editor3d/app/src/pages/PageEnigma/signals/demoAssets/demoMemeItems.ts new file mode 100644 index 0000000000..6301f2ed7b --- /dev/null +++ b/frontend/apps/editor3d/app/src/pages/PageEnigma/signals/demoAssets/demoMemeItems.ts @@ -0,0 +1,54 @@ +import { signal } from "@preact/signals-core"; +import { MediaItem } from "~/pages/PageEnigma/models"; +import { AssetType } from "~/enums"; + +export const demoMemeItems = signal([ + { + version: 1, + media_id: "m_yb02j1b0dn6ashn14ap2e2vnvy1jhz", + media_type: "glb", + type: AssetType.CHARACTER, + name: "Toothless", + thumbnail: "/resources/memes/toothless.jpg", + }, + { + version: 1, + media_id: "m_jgrzn7f66dvvw90qqttjn3m0dk8gef", + media_type: "glb", + type: AssetType.CHARACTER, + name: "Doge", + thumbnail: "/resources/memes/doge.jpg", + }, + { + version: 1, + media_id: "m_h8gk5m59252nbza07p08dyn1fnhnsf", + media_type: "glb", + type: AssetType.CHARACTER, + name: "Grimace", + thumbnail: "/resources/memes/grimace.jpg", + }, + { + version: 1, + media_id: "m_pzjn9583k1hqcetcfv1a2y04hkaspq", + media_type: "glb", + type: AssetType.CHARACTER, + name: "Roblox", + thumbnail: "/resources/memes/roblox.jpg", + }, + { + version: 1, + media_id: "m_xh0aj0wmdhvyf0pv5jj1wj3ap7p1rf", + media_type: "glb", + type: AssetType.CHARACTER, + name: "JD Vance", + thumbnail: "/resources/memes/jd_vance.jpg", + }, + { + version: 1, + media_id: "m_v8mkb44wy4r1ed7g5z8bq8n6bynerg", + media_type: "glb", + type: AssetType.CHARACTER, + name: "Lil JD", + thumbnail: "/resources/memes/lil_jd.jpg", + }, +]); diff --git a/frontend/apps/editor3d/app/src/pages/PageEnigma/signals/demoAssets/demoSkyboxItems.ts b/frontend/apps/editor3d/app/src/pages/PageEnigma/signals/demoAssets/demoSkyboxItems.ts index 190001c0a2..f169137af2 100644 --- a/frontend/apps/editor3d/app/src/pages/PageEnigma/signals/demoAssets/demoSkyboxItems.ts +++ b/frontend/apps/editor3d/app/src/pages/PageEnigma/signals/demoAssets/demoSkyboxItems.ts @@ -8,7 +8,7 @@ export const demoSkyboxItems = signal([ media_id: "SKY::m_1", type: AssetType.SKYBOX, name: "Empty Skybox", - thumbnail: "/resources/skybox/empty.jpg", + thumbnail: "/resources/placeholders/placeholder.png", }, { version: 1, diff --git a/frontend/apps/editor3d/app/src/pages/PageEnigma/signals/demoAssets/index.ts b/frontend/apps/editor3d/app/src/pages/PageEnigma/signals/demoAssets/index.ts index 49868d6b0b..35b89c4f1a 100644 --- a/frontend/apps/editor3d/app/src/pages/PageEnigma/signals/demoAssets/index.ts +++ b/frontend/apps/editor3d/app/src/pages/PageEnigma/signals/demoAssets/index.ts @@ -3,3 +3,4 @@ export * from "./demoAssets"; export * from "./demoAudioItems"; export * from "./demoCharacterItems"; export * from "./demoSkyboxItems"; +export * from "./demoMemeItems"; diff --git a/frontend/libs/components/gallery-modal/src/lib/gallery-modal.tsx b/frontend/libs/components/gallery-modal/src/lib/gallery-modal.tsx index 8d19724958..26e7c52fc1 100644 --- a/frontend/libs/components/gallery-modal/src/lib/gallery-modal.tsx +++ b/frontend/libs/components/gallery-modal/src/lib/gallery-modal.tsx @@ -50,6 +50,7 @@ export interface GalleryItem { fullImage?: string | null; createdAt: string; mediaClass?: string; + assetType?: string; } interface GroupedItems { @@ -256,6 +257,16 @@ export const GalleryModal = React.memo( (item.filter_media_classes ? item.filter_media_classes[0] : "image"), + // assetType for 3D assets so drop handlers can decide between character vs object + assetType: + item.media_class === "dimensional" + ? item.maybe_animation_type || + item.origin_product_category === "character" || + (item.origin && + item.origin.product_category === "character") + ? "character" + : "object" + : undefined, })); setAllItems((prev) => (reset ? newItems : [...prev, ...newItems])); // Pagination logic