mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-21 17:28:24 -04:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9eb1b8ebf | ||
|
|
7986991523 | ||
|
|
f4799a6244 | ||
|
|
66d5095f69 | ||
|
|
534b362cc0 | ||
|
|
94a5178bec | ||
|
|
26a6a65c92 | ||
|
|
032ebca3fb | ||
|
|
080f8da8a2 | ||
|
|
7230618ed5 |
@@ -38,7 +38,7 @@ export function ButtonIconSelect({
|
||||
`flex h-8 items-center justify-center rounded-lg border text-sm transition-all duration-150`,
|
||||
text ? "h-auto w-auto gap-2 px-2.5 py-1" : "w-8",
|
||||
selectedOption === value
|
||||
? "border-brand-primary bg-ui-panel/[0.3]"
|
||||
? "border-brand-primary bg-brand-primary/20"
|
||||
: "border-transparent hover:bg-ui-panel/[0.4]",
|
||||
)}
|
||||
onClick={() => handleOptionChange(value)}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { faXmark } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
interface CloseButtonProps {
|
||||
onClick: () => void;
|
||||
className?: string;
|
||||
size?: "sm" | "md" | "lg";
|
||||
}
|
||||
|
||||
export const CloseButton = ({
|
||||
onClick,
|
||||
className = "",
|
||||
size = "md",
|
||||
}: CloseButtonProps) => {
|
||||
const sizeClasses = {
|
||||
sm: "h-5 w-5 text-sm",
|
||||
md: "h-7 w-7 text-lg",
|
||||
lg: "h-9 w-9 text-xl",
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={twMerge(
|
||||
"flex items-center justify-center rounded-full bg-black/40 text-white/60 transition-all hover:bg-black/70 hover:text-white",
|
||||
sizeClasses[size],
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -39,7 +39,7 @@ export const InputVector = ({
|
||||
oldZ.current = z;
|
||||
|
||||
const inputCommonClasses =
|
||||
"relative h-6 rounded-r-lg bg-brand-secondary-900/35 p-2 text-sm text-white transition-all duration-100 ease-in-out outline-none -outline-offset-2 text-end w-full hover:cursor-e-resize hover:bg-brand-secondary-900";
|
||||
"relative h-6 rounded-r-lg bg-black/25 p-2 text-sm text-white transition-all duration-100 ease-in-out outline-none -outline-offset-2 text-end w-full hover:cursor-e-resize hover:bg-black/40";
|
||||
|
||||
const wrapperCommonClasses =
|
||||
"relative flex items-center before:inline-block before:h-6 before:bg-brand-primary before:text-white before:rounded-l-lg before:w-1.5 before:text-center before:justify-center before:items-center before:font-semibold before:flex before:align-middle before:leading-8 text-xs";
|
||||
|
||||
@@ -60,7 +60,7 @@ export const TabSelector: React.FC<TabSelectorProps> = ({
|
||||
)}
|
||||
>
|
||||
<Tab.Group selectedIndex={selectedIndex} onChange={handleTabChange}>
|
||||
<Tab.List className="glass relative inline-flex min-w-fit overflow-x-auto rounded-lg p-0.5 py-1">
|
||||
<Tab.List className="glass relative inline-flex min-w-fit overflow-x-auto rounded-lg p-0.5 py-1 !shadow-none">
|
||||
{/* Animated indicator */}
|
||||
<div
|
||||
className="absolute top-1 z-10 h-[calc(100%-8px)] rounded-md bg-brand-primary/30 transition-all duration-200 ease-in-out"
|
||||
|
||||
@@ -5,6 +5,7 @@ export * from "./ButtonDropdown";
|
||||
export * from "./ButtonIcon";
|
||||
export * from "./ButtonIconSelect";
|
||||
export * from "./ButtonLink";
|
||||
export * from "./CloseButton";
|
||||
export * from "./ConfirmationModal";
|
||||
export * from "./FileUploader";
|
||||
export * from "./FilterButtons";
|
||||
|
||||
@@ -59,7 +59,7 @@ export const TopBar = ({ pageName }: Props) => {
|
||||
icon={faImages}
|
||||
onClick={() => setIsLibraryModalOpen(true)}
|
||||
>
|
||||
My Library
|
||||
My Gallery
|
||||
</Button>
|
||||
<Activity />
|
||||
</div>
|
||||
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
overTimeline,
|
||||
sidePanelWidth,
|
||||
timelineHeight,
|
||||
assetModalVisibleDuringDrag,
|
||||
reopenAfterDragSignal,
|
||||
} from "~/pages/PageEnigma/signals";
|
||||
import { pageHeight, pageWidth } from "~/signals";
|
||||
import { addShape } from "~/pages/PageEnigma/signals/shape";
|
||||
@@ -25,6 +27,8 @@ class DndAsset {
|
||||
public initX = 0;
|
||||
public initY = 0;
|
||||
public notDropText = "";
|
||||
public isDragging: boolean = false;
|
||||
public dragThreshold: number = 5;
|
||||
|
||||
constructor() {
|
||||
this.onPointerMove = this.onPointerMove.bind(this);
|
||||
@@ -40,8 +44,10 @@ class DndAsset {
|
||||
};
|
||||
this.initX = event.pageX;
|
||||
this.initY = event.pageY;
|
||||
this.isDragging = false;
|
||||
canDrop.value = false;
|
||||
this.notDropText = "";
|
||||
assetModalVisibleDuringDrag.value = false;
|
||||
window.addEventListener("pointerup", this.onPointerUp);
|
||||
window.addEventListener("pointermove", this.onPointerMove);
|
||||
}
|
||||
@@ -54,6 +60,14 @@ class DndAsset {
|
||||
this.overElement = null;
|
||||
overTimeline.value = false;
|
||||
this.notDropText = "";
|
||||
if (reopenAfterDragSignal.value) {
|
||||
assetModalVisibleDuringDrag.value = true;
|
||||
} else {
|
||||
assetModalVisibleDuringDrag.value = true;
|
||||
setTimeout(() => {
|
||||
assetModalVisibleDuringDrag.value = false;
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +85,14 @@ class DndAsset {
|
||||
window.removeEventListener("pointerup", this.onPointerUp);
|
||||
window.removeEventListener("pointermove", this.onPointerMove);
|
||||
|
||||
if (!this.isDragging) {
|
||||
// It's a click, not a drag
|
||||
assetModalVisibleDuringDrag.value = true;
|
||||
dragItem.value = null;
|
||||
currPosition.value = { currX: 0, currY: 0 };
|
||||
return;
|
||||
}
|
||||
|
||||
if (dragItem.value) {
|
||||
const positionX = event.pageX;
|
||||
const positionY = event.pageY;
|
||||
@@ -142,6 +164,12 @@ class DndAsset {
|
||||
event.preventDefault();
|
||||
const deltaX = event.pageX - this.initX;
|
||||
const deltaY = event.pageY - this.initY;
|
||||
if (
|
||||
Math.abs(deltaX) > this.dragThreshold ||
|
||||
Math.abs(deltaY) > this.dragThreshold
|
||||
) {
|
||||
this.isDragging = true;
|
||||
}
|
||||
currPosition.value = {
|
||||
currX: this.initX + deltaX,
|
||||
currY: this.initY + deltaY,
|
||||
|
||||
@@ -922,12 +922,12 @@ class Editor {
|
||||
this.camera,
|
||||
);
|
||||
|
||||
this.outlinePass.edgeStrength = 6.0;
|
||||
this.outlinePass.edgeGlow = 0.2;
|
||||
this.outlinePass.edgeThickness = 1.0;
|
||||
this.outlinePass.edgeStrength = 5.0;
|
||||
this.outlinePass.edgeGlow = 0.1;
|
||||
this.outlinePass.edgeThickness = 1.2;
|
||||
this.outlinePass.pulsePeriod = 3;
|
||||
this.outlinePass.usePatternTexture = false;
|
||||
this.outlinePass.visibleEdgeColor.set(0xe66462);
|
||||
this.outlinePass.visibleEdgeColor.set(0x4b9fff);
|
||||
|
||||
this.composer.addPass(this.outlinePass);
|
||||
|
||||
@@ -992,27 +992,25 @@ class Editor {
|
||||
const width =
|
||||
this.render_camera_aspect_ratio === CameraAspectRatio.HORIZONTAL_16_9
|
||||
? 1024
|
||||
: this.render_camera_aspect_ratio ===
|
||||
meraAspectRatio.VERTICAL_9_16
|
||||
: this.render_camera_aspect_ratio === meraAspectRatio.VERTICAL_9_16
|
||||
? 576
|
||||
: this.render_camera_aspect_ratio ===
|
||||
meraAspectRatio.HORIZONTAL_3_2
|
||||
meraAspectRatio.HORIZONTAL_3_2
|
||||
? 900
|
||||
: this.render_camera_aspect_ratio ===
|
||||
meraAspectRatio.VERTICAL_2_3
|
||||
meraAspectRatio.VERTICAL_2_3
|
||||
? 600
|
||||
: 1000;
|
||||
const height =
|
||||
this.render_camera_aspect_ratio === CameraAspectRatio.HORIZONTAL_16_9
|
||||
? 576
|
||||
: this.render_camera_aspect_ratio ===
|
||||
meraAspectRatio.VERTICAL_9_16
|
||||
: this.render_camera_aspect_ratio === meraAspectRatio.VERTICAL_9_16
|
||||
? 1024
|
||||
: this.render_camera_aspect_ratio ===
|
||||
meraAspectRatio.HORIZONTAL_3_2
|
||||
meraAspectRatio.HORIZONTAL_3_2
|
||||
? 600
|
||||
: this.render_camera_aspect_ratio ===
|
||||
meraAspectRatio.VERTICAL_2_3
|
||||
meraAspectRatio.VERTICAL_2_3
|
||||
? 900
|
||||
: 1000;
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import { ControlsVideo } from "./comps/ControlsVideo";
|
||||
import { ControlPanelSceneObject } from "./comps/ControlPanelSceneObject";
|
||||
import { PreviewEngineCamera } from "./comps/PreviewEngineCamera";
|
||||
import { PreviewFrameImage } from "./comps/PreviewFrameImage";
|
||||
|
||||
import { pageHeight, pageWidth } from "~/signals";
|
||||
|
||||
import {
|
||||
@@ -24,10 +23,10 @@ import {
|
||||
} from "~/pages/PageEnigma/signals";
|
||||
import { EditorCanvas } from "./comps/EngineCanvases";
|
||||
import { SceneContainer } from "./comps/SceneContainer";
|
||||
import { AspectRatioMenu } from "./comps/AspectRatioMenu";
|
||||
import { Outliner } from "./comps/Outliner";
|
||||
import { CameraAspectRatio } from "./enums";
|
||||
import { PromptBox } from "./comps/PromptBox";
|
||||
import { AssetMenu } from "./comps/AssetMenu";
|
||||
|
||||
export const PageEditor = () => {
|
||||
useSignals();
|
||||
@@ -95,7 +94,6 @@ export const PageEditor = () => {
|
||||
className="flex"
|
||||
style={{
|
||||
height,
|
||||
width,
|
||||
}}
|
||||
>
|
||||
<div className="relative w-full overflow-hidden bg-transparent">
|
||||
@@ -116,14 +114,13 @@ export const PageEditor = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AssetMenu />
|
||||
|
||||
{/* Bottom controls */}
|
||||
<div
|
||||
className="absolute bottom-0 left-0"
|
||||
style={{
|
||||
width:
|
||||
pageWidth.value -
|
||||
(sidePanelVisible.value ? sidePanelWidth.value : 0) -
|
||||
84,
|
||||
width: pageWidth.value,
|
||||
}}
|
||||
onClick={handleOverlayClick}
|
||||
>
|
||||
@@ -134,10 +131,12 @@ export const PageEditor = () => {
|
||||
<Outliner />
|
||||
<PreviewEngineCamera />
|
||||
</div>
|
||||
<PromptBox />
|
||||
|
||||
<ControlPanelSceneObject />
|
||||
</div>
|
||||
|
||||
<PromptBox />
|
||||
|
||||
<LoadingDots
|
||||
className="absolute left-0 top-0"
|
||||
isShowing={editorLoader.value.isShowing}
|
||||
@@ -146,10 +145,11 @@ export const PageEditor = () => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Side panel */}
|
||||
<div onClick={handleOverlayClick}>
|
||||
{/* <div onClick={handleOverlayClick}>
|
||||
<SidePanel />
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
|
||||
{/* Timeline */}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { faCube, faMagicWandSparkles } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { Button, Tooltip } from "~/components";
|
||||
import { useState } from "react";
|
||||
import { AssetModal } from "./AssetModal";
|
||||
import { assetModalVisibleDuringDrag } from "../../signals";
|
||||
|
||||
export const AssetMenu = () => {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
const handleOpenModal = () => {
|
||||
assetModalVisibleDuringDrag.value = true;
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="glass absolute left-2 top-1/2 flex -translate-y-1/2 flex-col gap-1 rounded-lg p-1">
|
||||
<Tooltip content="Add 3D object to scene" position="right" delay={100}>
|
||||
<Button
|
||||
icon={faCube}
|
||||
className="h-12 w-12 text-lg"
|
||||
onClick={handleOpenModal}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
content="Create 3D model from image"
|
||||
position="right"
|
||||
delay={100}
|
||||
>
|
||||
<Button
|
||||
icon={faMagicWandSparkles}
|
||||
className="h-12 w-12 text-lg"
|
||||
variant="secondary"
|
||||
disabled={true}
|
||||
onClick={handleOpenModal}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<AssetModal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,363 @@
|
||||
import { TransitionDialogue } from "~/components/reusable/TransitionDialogue";
|
||||
import {
|
||||
faPlus,
|
||||
faSearch,
|
||||
faChevronLeft,
|
||||
faLayerGroup,
|
||||
faUser,
|
||||
faSun,
|
||||
faTree,
|
||||
faCube,
|
||||
faChevronRight,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Button, CloseButton, Input, Tooltip } from "~/components";
|
||||
import { TabSelector } from "~/components/reusable/TabSelector";
|
||||
import { useState, useEffect, useMemo, useRef } from "react";
|
||||
import { ItemElements } from "../SidePanelTabs/sharedComps/ItemElements";
|
||||
import {
|
||||
demoSkyboxItems,
|
||||
demoShapeItems,
|
||||
demoCharacterItems,
|
||||
assetModalVisibleDuringDrag,
|
||||
reopenAfterDragSignal,
|
||||
} from "../../signals";
|
||||
import { MediaItem } from "../../models";
|
||||
import { useUserObjects, useFeaturedObjects } from "../SidePanelTabs/hooks";
|
||||
import { FilterEngineCategories } from "~/enums";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
|
||||
interface AssetModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
type AssetTab = {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: typeof faLayerGroup;
|
||||
engineCategory?: FilterEngineCategories;
|
||||
items: MediaItem[];
|
||||
};
|
||||
|
||||
const AllTabSection = ({
|
||||
label,
|
||||
items,
|
||||
onViewAll,
|
||||
}: {
|
||||
label: string;
|
||||
items: MediaItem[];
|
||||
onViewAll: () => void;
|
||||
}) => (
|
||||
<div className="mb-0">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<h3 className="text-md ml-2 font-semibold opacity-90">{label}</h3>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="mr-3 flex items-center gap-1 px-2 py-1 text-xs"
|
||||
onClick={onViewAll}
|
||||
>
|
||||
View all
|
||||
<FontAwesomeIcon icon={faChevronRight} className="text-xs opacity-70" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="h-[170px]">
|
||||
<ItemElements
|
||||
items={items.slice(0, 4)}
|
||||
busy={false}
|
||||
debug={`all-tab-section-${label}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const AssetModal = ({ isOpen, onClose }: AssetModalProps) => {
|
||||
useSignals();
|
||||
const [activeLibraryTab, setActiveLibraryTab] = useState("library");
|
||||
const [activeAssetTab, setActiveAssetTab] = useState("all");
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [reopenAfterAdd, setReopenAfterAdd] = useState(true);
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Update the signal when the preference changes
|
||||
useEffect(() => {
|
||||
reopenAfterDragSignal.value = reopenAfterAdd;
|
||||
}, [reopenAfterAdd]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
// Small delay to ensure the modal is fully mounted
|
||||
const timer = setTimeout(() => {
|
||||
searchInputRef.current?.focus();
|
||||
}, 100);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
// Fetch objects for different categories
|
||||
const { userObjects: userCharacters } = useUserObjects({
|
||||
filterEngineCategories: [FilterEngineCategories.CHARACTER],
|
||||
defaultErrorMessage: "Error fetching user characters",
|
||||
});
|
||||
|
||||
const { userObjects: userObjects } = useUserObjects({
|
||||
filterEngineCategories: [FilterEngineCategories.OBJECT],
|
||||
defaultErrorMessage: "Error fetching user objects",
|
||||
});
|
||||
|
||||
const { userObjects: userNature } = useUserObjects({
|
||||
filterEngineCategories: [FilterEngineCategories.SET_DRESSING],
|
||||
defaultErrorMessage: "Error fetching user nature assets",
|
||||
});
|
||||
|
||||
const { featuredObjects: featuredCharacters } = useFeaturedObjects({
|
||||
filterEngineCategories: [FilterEngineCategories.CHARACTER],
|
||||
defaultErrorMessage: "Error fetching featured characters",
|
||||
});
|
||||
|
||||
const { featuredObjects: featuredObjects } = useFeaturedObjects({
|
||||
filterEngineCategories: [FilterEngineCategories.OBJECT],
|
||||
defaultErrorMessage: "Error fetching featured objects",
|
||||
});
|
||||
|
||||
const { featuredObjects: featuredNature } = useFeaturedObjects({
|
||||
filterEngineCategories: [FilterEngineCategories.SET_DRESSING],
|
||||
defaultErrorMessage: "Error fetching featured nature assets",
|
||||
});
|
||||
|
||||
const libraryTabs = [
|
||||
{ id: "library", label: "Library" },
|
||||
{ id: "mine", label: "Mine" },
|
||||
];
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const assetTabs: AssetTab[] = [
|
||||
{ id: "all", label: "All", icon: faLayerGroup, items: [] },
|
||||
{
|
||||
id: "character",
|
||||
label: "Character",
|
||||
icon: faUser,
|
||||
engineCategory: FilterEngineCategories.CHARACTER,
|
||||
items:
|
||||
activeLibraryTab === "library"
|
||||
? [...demoCharacterItems.value, ...(featuredCharacters ?? [])]
|
||||
: (userCharacters ?? []),
|
||||
},
|
||||
{
|
||||
id: "objects",
|
||||
label: "Objects",
|
||||
icon: faCube,
|
||||
engineCategory: FilterEngineCategories.OBJECT,
|
||||
items:
|
||||
activeLibraryTab === "library"
|
||||
? [...demoShapeItems.value, ...(featuredObjects ?? [])]
|
||||
: (userObjects ?? []),
|
||||
},
|
||||
{
|
||||
id: "skybox",
|
||||
label: "Skybox",
|
||||
icon: faSun,
|
||||
items: demoSkyboxItems.value,
|
||||
},
|
||||
{
|
||||
id: "nature",
|
||||
label: "Nature",
|
||||
icon: faTree,
|
||||
engineCategory: FilterEngineCategories.SET_DRESSING,
|
||||
items:
|
||||
activeLibraryTab === "library"
|
||||
? (featuredNature ?? [])
|
||||
: (userNature ?? []),
|
||||
},
|
||||
];
|
||||
|
||||
// Combine all items for the "All" tab
|
||||
const allItems = useMemo(() => {
|
||||
return assetTabs
|
||||
.filter((tab) => tab.id !== "all")
|
||||
.flatMap((tab) => tab.items);
|
||||
}, [assetTabs]);
|
||||
|
||||
// Update the items array for the "All" tab
|
||||
assetTabs[0].items = allItems;
|
||||
|
||||
const currentTab =
|
||||
assetTabs.find((tab) => tab.id === activeAssetTab) || assetTabs[0];
|
||||
|
||||
// Filter items based on search term
|
||||
const displayedItems = useMemo(() => {
|
||||
if (!searchTerm) {
|
||||
return currentTab.items;
|
||||
}
|
||||
|
||||
const searchLower = searchTerm.toLowerCase();
|
||||
return currentTab.items.filter(
|
||||
(item) =>
|
||||
item.name?.toLowerCase().includes(searchLower) ||
|
||||
item.description?.toLowerCase().includes(searchLower),
|
||||
);
|
||||
}, [currentTab.items, searchTerm]);
|
||||
|
||||
// Switch to "All" tab when searching
|
||||
useEffect(() => {
|
||||
if (searchTerm && activeAssetTab !== "all") {
|
||||
setActiveAssetTab("all");
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [searchTerm]);
|
||||
|
||||
// Clear search term when tab changes
|
||||
useEffect(() => {
|
||||
setSearchTerm("");
|
||||
}, [activeAssetTab]);
|
||||
|
||||
const renderContent = () => {
|
||||
if (activeAssetTab === "all" && !searchTerm) {
|
||||
return (
|
||||
<div className="h-full space-y-2 overflow-y-auto">
|
||||
{assetTabs.slice(1).map((tab) => (
|
||||
<AllTabSection
|
||||
key={tab.id}
|
||||
label={tab.label}
|
||||
items={tab.items}
|
||||
onViewAll={() => setActiveAssetTab(tab.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ItemElements
|
||||
items={displayedItems}
|
||||
busy={false}
|
||||
debug={`asset-modal-${currentTab.id}`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const handleAddAsset = () => {
|
||||
if (reopenAfterAdd) {
|
||||
// Small delay to allow the modal to close and reopen
|
||||
setTimeout(() => {
|
||||
onClose();
|
||||
}, 100);
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const clearSearch = () => setSearchTerm("");
|
||||
|
||||
return (
|
||||
<TransitionDialogue
|
||||
isOpen={isOpen && assetModalVisibleDuringDrag.value}
|
||||
onClose={onClose}
|
||||
className="h-[640px] max-w-4xl"
|
||||
childPadding={false}
|
||||
>
|
||||
<div className="grid h-full grid-cols-12 gap-3">
|
||||
<div className="relative col-span-3 flex h-full flex-col p-3 pt-2 after:absolute after:right-0 after:top-0 after:h-full after:w-px after:bg-gray-200 after:dark:bg-white/10">
|
||||
<div className="flex items-center justify-between gap-2.5 py-0.5">
|
||||
<h2 className="text-[18px] font-semibold opacity-80">3D Assets</h2>
|
||||
<Tooltip content="Upload model" position="top" delay={200}>
|
||||
<Button
|
||||
className="h-6 w-6 rounded-full border-none bg-transparent text-white/70 transition-colors hover:bg-transparent hover:text-white/100"
|
||||
onClick={handleAddAsset}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} className="text-xl" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<hr className="my-2 w-full border-white/10" />
|
||||
<div className="space-y-2">
|
||||
{assetTabs.map((tab) => (
|
||||
<Button
|
||||
key={tab.id}
|
||||
variant={activeAssetTab === tab.id ? "primary" : "secondary"}
|
||||
className={twMerge(
|
||||
"w-full justify-start rounded-xl border border-white/[2%] bg-white/[4%] px-3.5 py-2.5 text-left hover:bg-white/15",
|
||||
activeAssetTab === tab.id &&
|
||||
"border-brand-primary bg-brand-primary/10 hover:bg-brand-primary/10",
|
||||
)}
|
||||
onClick={() => setActiveAssetTab(tab.id)}
|
||||
>
|
||||
<FontAwesomeIcon icon={tab.icon} className="mr-2 opacity-70" />
|
||||
{tab.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-auto flex items-center gap-2 pt-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="reopen-after-add"
|
||||
checked={reopenAfterAdd}
|
||||
onChange={(e) => setReopenAfterAdd(e.target.checked)}
|
||||
className="h-4 w-4 rounded-lg border-gray-300 bg-gray-700 text-brand-primary focus:ring-brand-primary"
|
||||
/>
|
||||
<label htmlFor="reopen-after-add" className="text-sm text-white/70">
|
||||
Reopen after adding
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-9 p-3 pb-0 ps-0 pt-2">
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="h-full">
|
||||
<div className="flex items-center gap-4">
|
||||
<TabSelector
|
||||
tabs={libraryTabs}
|
||||
activeTab={activeLibraryTab}
|
||||
onTabChange={setActiveLibraryTab}
|
||||
className="w-auto"
|
||||
/>
|
||||
<div className="relative grow">
|
||||
<Input
|
||||
ref={searchInputRef}
|
||||
placeholder="Search"
|
||||
className="grow"
|
||||
inputClassName="pr-2.5"
|
||||
icon={faSearch}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
iconClassName="text-white/60"
|
||||
/>
|
||||
{searchTerm && (
|
||||
<CloseButton
|
||||
onClick={clearSearch}
|
||||
className="absolute right-2.5 top-1/2 h-4 w-4 -translate-y-1/2 bg-white/10 text-[10px] hover:bg-white/20"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<CloseButton onClick={onClose} />
|
||||
</div>
|
||||
<div
|
||||
className={twMerge(
|
||||
"overflow-auto-y mt-4 h-[574px]",
|
||||
activeAssetTab !== "all" && "h-[552px]",
|
||||
)}
|
||||
>
|
||||
{activeAssetTab !== "all" && !searchTerm && (
|
||||
<div className="mb-2 flex items-center font-semibold">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="flex items-center gap-2 border-none bg-transparent px-3 py-1.5 text-sm text-white/70 hover:bg-transparent hover:text-white/100"
|
||||
onClick={() => setActiveAssetTab("all")}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faChevronLeft}
|
||||
className="text-sm font-semibold opacity-70"
|
||||
/>
|
||||
</Button>
|
||||
{currentTab.label}
|
||||
</div>
|
||||
)}
|
||||
{renderContent()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TransitionDialogue>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./AssetMenu";
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
import { TransitionDialogue } from "~/components/reusable/TransitionDialogue";
|
||||
import { faPlus, faTrashAlt, faXmark } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { PopoverItem } from "~/components/reusable/Popover";
|
||||
import { Button, Input, Label, Tooltip } from "~/components";
|
||||
import { SliderV2 } from "~/components/reusable/SliderV2/SliderV2";
|
||||
|
||||
interface ExtendedPopoverItem extends PopoverItem {
|
||||
id: string;
|
||||
focalLength: number;
|
||||
}
|
||||
|
||||
interface CameraSettingsModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
cameras: ExtendedPopoverItem[];
|
||||
onCameraNameChange: (id: string, newName: string) => void;
|
||||
onCameraFocalLengthChange: (id: string, value: number) => void;
|
||||
onAddCamera: () => void;
|
||||
selectedCameraId: string;
|
||||
onSelectCamera: (id: string) => void;
|
||||
onDeleteCamera: (id: string) => void;
|
||||
}
|
||||
|
||||
export const CameraSettingsModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
cameras,
|
||||
onCameraNameChange,
|
||||
onCameraFocalLengthChange,
|
||||
onAddCamera,
|
||||
selectedCameraId,
|
||||
onSelectCamera,
|
||||
onDeleteCamera,
|
||||
}: CameraSettingsModalProps) => {
|
||||
const selectedCamera = cameras.find((cam) => cam.id === selectedCameraId);
|
||||
|
||||
return (
|
||||
<TransitionDialogue
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
className="h-[500px] max-w-3xl"
|
||||
childPadding={false}
|
||||
>
|
||||
<div className="grid h-full grid-cols-12 gap-3">
|
||||
<div className="relative col-span-4 p-3 pt-2 after:absolute after:right-0 after:top-0 after:h-full after:w-px after:bg-gray-200 after:dark:bg-white/10">
|
||||
<div className="flex items-center justify-between gap-2.5 py-0.5">
|
||||
<h2 className="text-[18px] font-semibold opacity-80">Camera</h2>
|
||||
<Tooltip content="Add camera" position="top" delay={200}>
|
||||
<button
|
||||
className="h-6 w-6 rounded-full text-white/70 transition-colors hover:text-white/100"
|
||||
onClick={onAddCamera}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} className="text-xl" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<hr className="my-2 w-full border-white/10" />
|
||||
<div className="space-y-1">
|
||||
{cameras.map((camera) => (
|
||||
<button
|
||||
key={camera.id}
|
||||
className={`w-full rounded-lg p-2 text-left transition-colors duration-100 hover:bg-[#63636B]/40 ${
|
||||
camera.id === selectedCameraId ? "bg-[#63636B]/40" : ""
|
||||
}`}
|
||||
onClick={() => onSelectCamera(camera.id)}
|
||||
>
|
||||
<div className="flex items-center gap-2.5 text-sm">
|
||||
{camera.icon}
|
||||
{camera.label}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-8 p-3 ps-0 pt-2">
|
||||
<div className="flex h-full flex-col">
|
||||
<div>
|
||||
<div className="flex items-center justify-between gap-2.5 py-0.5 opacity-100">
|
||||
<h2 className="text-[18px] font-semibold">
|
||||
{selectedCamera?.label || "Camera"}
|
||||
</h2>
|
||||
<Tooltip
|
||||
content={
|
||||
selectedCamera?.id === "main"
|
||||
? "Cannot delete the main camera"
|
||||
: "Delete camera"
|
||||
}
|
||||
position="top"
|
||||
delay={200}
|
||||
>
|
||||
<button
|
||||
className={`h-6 w-6 rounded-lg transition-colors ${
|
||||
selectedCamera?.id === "main"
|
||||
? "cursor-not-allowed text-white/30"
|
||||
: "text-white/60 hover:text-white/100"
|
||||
}`}
|
||||
onClick={() =>
|
||||
selectedCamera &&
|
||||
selectedCamera.id !== "main" &&
|
||||
onDeleteCamera(selectedCamera.id)
|
||||
}
|
||||
disabled={selectedCamera?.id === "main"}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrashAlt} className="text-lg" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<hr className="my-2 w-full border-white/10" />
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="camera-name" className="text-sm opacity-70">
|
||||
Name
|
||||
</Label>
|
||||
<Input
|
||||
id="camera-name"
|
||||
type="text"
|
||||
value={selectedCamera?.label || ""}
|
||||
onChange={(e) =>
|
||||
selectedCamera &&
|
||||
onCameraNameChange(selectedCamera.id, e.target.value)
|
||||
}
|
||||
className="text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="focal-length" className="text-sm opacity-70">
|
||||
Focal Length
|
||||
</Label>
|
||||
<div className="mt-1 flex items-center gap-4">
|
||||
<SliderV2
|
||||
min={10}
|
||||
max={200}
|
||||
value={selectedCamera?.focalLength || 35}
|
||||
onChange={(value) =>
|
||||
selectedCamera &&
|
||||
onCameraFocalLengthChange(selectedCamera.id, value)
|
||||
}
|
||||
step={1}
|
||||
suffix="mm"
|
||||
showDecrement={true}
|
||||
showIncrement={true}
|
||||
className="w-full"
|
||||
/>
|
||||
<span className="min-w-[60px] text-sm">
|
||||
{selectedCamera?.focalLength || 35}mm
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-auto flex justify-end pt-4">
|
||||
<Button onClick={onClose}>Done</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TransitionDialogue>
|
||||
);
|
||||
};
|
||||
|
||||
+7
-48
@@ -15,11 +15,6 @@ import {
|
||||
} from "../../signals";
|
||||
import { EngineContext } from "~/pages/PageEnigma/contexts/EngineContext";
|
||||
import { Button, H5, InputVector } from "~/components";
|
||||
|
||||
import { QueueNames } from "../../Queue/QueueNames";
|
||||
import Queue from "~/pages/PageEnigma/Queue/Queue";
|
||||
import { toTimelineActions } from "../../Queue/toTimelineActions";
|
||||
import { QueueKeyframe } from "~/pages/PageEnigma/models";
|
||||
import { editorState } from "~/pages/PageEnigma/signals/engine";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { EditorStates } from "~/pages/PageEnigma/enums";
|
||||
@@ -71,9 +66,6 @@ export const ControlPanelSceneObject = () => {
|
||||
};
|
||||
|
||||
const currentSceneObject = currentObject.value;
|
||||
const position = currentSceneObject?.objectVectors?.position;
|
||||
const rotation = currentSceneObject?.objectVectors?.rotation;
|
||||
const scale = currentSceneObject?.objectVectors?.scale;
|
||||
|
||||
function localToEngine(xyz: Record<string, string>) {
|
||||
return {
|
||||
@@ -167,42 +159,6 @@ export const ControlPanelSceneObject = () => {
|
||||
setLocalScale(xyz);
|
||||
};
|
||||
|
||||
const handleOnAddKeyFrame = () => {
|
||||
if (currentSceneObject) {
|
||||
if (
|
||||
position == null ||
|
||||
rotation == null ||
|
||||
scale == null ||
|
||||
currentSceneObject == null
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const key in editorEngine?.timeline.characters) {
|
||||
const element = editorEngine?.timeline.characters[key];
|
||||
if (key == currentSceneObject.object_uuid) {
|
||||
currentSceneObject.group = element;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Queue.publish({
|
||||
queueName: QueueNames.TO_TIMELINE,
|
||||
action: toTimelineActions.ADD_KEYFRAME,
|
||||
data: {
|
||||
group: currentSceneObject.group,
|
||||
object_uuid: currentSceneObject.object_uuid,
|
||||
object_name: currentSceneObject.object_name,
|
||||
version: 1,
|
||||
|
||||
position: currentSceneObject.objectVectors.position,
|
||||
rotation: currentSceneObject.objectVectors.rotation,
|
||||
scale: currentSceneObject.objectVectors.scale,
|
||||
} as QueueKeyframe,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteObject = () => {
|
||||
editorEngine?.deleteObject(currentSceneObject.object_uuid);
|
||||
};
|
||||
@@ -216,7 +172,7 @@ export const ControlPanelSceneObject = () => {
|
||||
<Transition
|
||||
show={isShowing.value}
|
||||
className={twMerge(
|
||||
"absolute bottom-0 right-0 mb-2 mr-2 flex h-fit w-56 origin-bottom-right flex-col gap-2 rounded-lg border border-ui-panel-border bg-ui-panel/95 p-3.5 text-white shadow-lg",
|
||||
"glass absolute bottom-0 right-0 mb-2 mr-2 flex h-fit w-56 origin-bottom-right flex-col gap-2 rounded-lg border border-ui-panel-border p-3.5 text-white shadow-lg",
|
||||
)}
|
||||
enter="transition-opacity duration-150"
|
||||
enterFrom="opacity-0"
|
||||
@@ -311,14 +267,17 @@ export const ControlPanelSceneObject = () => {
|
||||
</Transition>
|
||||
|
||||
<div className="mt-0.5 flex gap-1.5">
|
||||
<Button variant="action" className="grow" onClick={handleOnAddKeyFrame}>
|
||||
{/* <Button variant="action" className="grow" onClick={handleOnAddKeyFrame}>
|
||||
Add Keyframe (K)
|
||||
</Button>
|
||||
</Button> */}
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon={faTrash}
|
||||
onClick={handleDeleteObject}
|
||||
/>
|
||||
className="w-full"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</Transition>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { TransitionDialogue } from "~/components/reusable/TransitionDialogue";
|
||||
import { faCheck, faXmark } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Button, LoadingSpinner } from "~/components";
|
||||
import { Button, LoadingSpinner, CloseButton } from "~/components";
|
||||
import { TabSelector } from "~/components/reusable/TabSelector";
|
||||
import { useState, useEffect, Fragment } from "react";
|
||||
import { LibraryModalApi } from "./LibraryModalApi";
|
||||
@@ -156,10 +156,10 @@ export const LibraryModal = ({
|
||||
childPadding={false}
|
||||
>
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="border-b border-white/10 p-4">
|
||||
<div className="border-b border-white/10 p-4 py-3">
|
||||
<div className="grid grid-cols-3 items-center">
|
||||
<h2 className="text-xl font-semibold">
|
||||
{mode === "select" ? "Select Images" : "My Library"}
|
||||
{mode === "select" ? "Select Images" : "My Gallery"}
|
||||
</h2>
|
||||
<div className="flex items-center justify-center">
|
||||
<TabSelector
|
||||
@@ -170,12 +170,7 @@ export const LibraryModal = ({
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="flex h-7 w-7 items-center justify-center rounded-full bg-black/40 text-white/60 transition-all hover:bg-black/70 hover:text-white"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} className="text-lg" />
|
||||
</button>
|
||||
<CloseButton onClick={onClose} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -326,12 +321,10 @@ export const LibraryModal = ({
|
||||
className="relative h-[90vh] w-[80vw] rounded-xl bg-[#2C2C2C]"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<button
|
||||
<CloseButton
|
||||
onClick={handleCloseLightbox}
|
||||
className="absolute right-4 top-4 z-10 flex h-7 w-7 items-center justify-center rounded-full bg-black/40 text-white/60 transition-all hover:bg-black/70 hover:text-white"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} className="text-lg" />
|
||||
</button>
|
||||
className="absolute right-4 top-4 z-10"
|
||||
/>
|
||||
<div className="grid h-full grid-cols-3 gap-6">
|
||||
<div className="col-span-2 flex h-full items-center justify-center overflow-hidden bg-black/40">
|
||||
{!lightboxImage.fullImage ? (
|
||||
|
||||
+1
-1
@@ -71,7 +71,7 @@ export const PreviewEngineCamera = () => {
|
||||
<div
|
||||
id="preview-engine-camera"
|
||||
className={twMerge(
|
||||
"origin-bottom-left shadow-lg",
|
||||
"hidden origin-bottom-left shadow-lg", //hidden right now with css
|
||||
editorState.value === EditorStates.PREVIEW
|
||||
? "invisible h-0 w-0"
|
||||
: "visible",
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
import { TransitionDialogue } from "~/components/reusable/TransitionDialogue";
|
||||
import { faPlus, faTrashAlt, faXmark } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { PopoverItem } from "~/components/reusable/Popover";
|
||||
import { Button, Input, Label, Tooltip } from "~/components";
|
||||
import { SliderV2 } from "~/components/reusable/SliderV2/SliderV2";
|
||||
|
||||
interface ExtendedPopoverItem extends PopoverItem {
|
||||
id: string;
|
||||
focalLength: number;
|
||||
}
|
||||
|
||||
interface CameraSettingsModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
cameras: ExtendedPopoverItem[];
|
||||
onCameraNameChange: (id: string, newName: string) => void;
|
||||
onCameraFocalLengthChange: (id: string, value: number) => void;
|
||||
onAddCamera: () => void;
|
||||
selectedCameraId: string;
|
||||
onSelectCamera: (id: string) => void;
|
||||
onDeleteCamera: (id: string) => void;
|
||||
}
|
||||
|
||||
export const CameraSettingsModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
cameras,
|
||||
onCameraNameChange,
|
||||
onCameraFocalLengthChange,
|
||||
onAddCamera,
|
||||
selectedCameraId,
|
||||
onSelectCamera,
|
||||
onDeleteCamera,
|
||||
}: CameraSettingsModalProps) => {
|
||||
const selectedCamera = cameras.find((cam) => cam.id === selectedCameraId);
|
||||
|
||||
return (
|
||||
<TransitionDialogue
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
className="h-[500px] max-w-3xl"
|
||||
childPadding={false}
|
||||
>
|
||||
<div className="grid h-full grid-cols-12 gap-3">
|
||||
<div className="relative col-span-4 p-3 pt-2 after:absolute after:right-0 after:top-0 after:h-full after:w-px after:bg-gray-200 after:dark:bg-white/10">
|
||||
<div className="flex items-center justify-between gap-2.5 py-0.5">
|
||||
<h2 className="text-[18px] font-semibold opacity-80">Camera</h2>
|
||||
<Tooltip content="Add camera" position="top" delay={200}>
|
||||
<button
|
||||
className="h-6 w-6 rounded-full text-white/70 transition-colors hover:text-white/100"
|
||||
onClick={onAddCamera}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} className="text-xl" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<hr className="my-2 w-full border-white/10" />
|
||||
<div className="space-y-1">
|
||||
{cameras.map((camera) => (
|
||||
<button
|
||||
key={camera.id}
|
||||
className={`w-full rounded-lg p-2 text-left transition-colors duration-100 hover:bg-[#63636B]/40 ${
|
||||
camera.id === selectedCameraId ? "bg-[#63636B]/40" : ""
|
||||
}`}
|
||||
onClick={() => onSelectCamera(camera.id)}
|
||||
>
|
||||
<div className="flex items-center gap-2.5 text-sm">
|
||||
{camera.icon}
|
||||
{camera.label}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-8 p-3 ps-0 pt-2">
|
||||
<div className="flex h-full flex-col">
|
||||
<div>
|
||||
<div className="flex items-center justify-between gap-2.5 py-0.5 opacity-100">
|
||||
<h2 className="text-[18px] font-semibold">
|
||||
{selectedCamera?.label || "Camera"}
|
||||
</h2>
|
||||
<Tooltip
|
||||
content={
|
||||
selectedCamera?.id === "main"
|
||||
? "Cannot delete the main camera"
|
||||
: "Delete camera"
|
||||
}
|
||||
position="top"
|
||||
delay={200}
|
||||
>
|
||||
<button
|
||||
className={`h-6 w-6 rounded-lg transition-colors ${
|
||||
selectedCamera?.id === "main"
|
||||
? "cursor-not-allowed text-white/30"
|
||||
: "text-white/60 hover:text-white/100"
|
||||
}`}
|
||||
onClick={() =>
|
||||
selectedCamera &&
|
||||
selectedCamera.id !== "main" &&
|
||||
onDeleteCamera(selectedCamera.id)
|
||||
}
|
||||
disabled={selectedCamera?.id === "main"}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrashAlt} className="text-lg" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<hr className="my-2 w-full border-white/10" />
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="camera-name" className="text-sm opacity-70">
|
||||
Name
|
||||
</Label>
|
||||
<Input
|
||||
id="camera-name"
|
||||
type="text"
|
||||
value={selectedCamera?.label || ""}
|
||||
onChange={(e) =>
|
||||
selectedCamera &&
|
||||
onCameraNameChange(selectedCamera.id, e.target.value)
|
||||
}
|
||||
className="text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="focal-length" className="text-sm opacity-70">
|
||||
Focal Length
|
||||
</Label>
|
||||
<div className="mt-1 flex items-center gap-4">
|
||||
<SliderV2
|
||||
min={10}
|
||||
max={200}
|
||||
value={selectedCamera?.focalLength || 35}
|
||||
onChange={(value) =>
|
||||
selectedCamera &&
|
||||
onCameraFocalLengthChange(selectedCamera.id, value)
|
||||
}
|
||||
step={1}
|
||||
suffix="mm"
|
||||
showDecrement={true}
|
||||
showIncrement={true}
|
||||
className="w-full"
|
||||
/>
|
||||
<span className="min-w-[60px] text-sm">
|
||||
{selectedCamera?.focalLength || 35}mm
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-auto flex justify-end pt-4">
|
||||
<Button onClick={onClose}>Done</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TransitionDialogue>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { useState, useRef, useEffect, useContext } from "react";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { EngineContext } from "~/pages/PageEnigma/contexts/EngineContext";
|
||||
import { useContext } from "react";
|
||||
import { uploadImage } from "~/components/reusable/UploadModalMedia/uploadImage";
|
||||
import {
|
||||
faPlus,
|
||||
@@ -13,8 +12,6 @@ import {
|
||||
faDownload,
|
||||
faSpinnerThird,
|
||||
faTimes,
|
||||
faCheck,
|
||||
faSpellCheck,
|
||||
faMessageCheck,
|
||||
faMessageXmark,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
@@ -39,7 +36,6 @@ import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
|
||||
import Queue from "~/pages/PageEnigma/Queue/Queue";
|
||||
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
|
||||
import { LibraryModal } from "../LibraryModal/LibraryModal";
|
||||
import { BucketConfig } from "../../../../api/BucketConfig";
|
||||
import type { LibraryItem } from "../LibraryModal/LibraryModal";
|
||||
|
||||
interface ReferenceImage {
|
||||
@@ -51,7 +47,7 @@ interface ReferenceImage {
|
||||
import { EngineApi } from "~/Classes/ApiManager/EngineApi";
|
||||
import { scene } from "~/signals/scene";
|
||||
import { UploaderStates } from "~/enums/UploaderStates";
|
||||
import { CameraSettingsModal } from "./CameraSettingsModal";
|
||||
import { CameraSettingsModal } from "../CameraSettingsModal";
|
||||
|
||||
interface ExtendedPopoverItem extends PopoverItem {
|
||||
id: string;
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import { useCallback, useContext } from "react";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { pageHeight, pageWidth } from "~/signals";
|
||||
import {
|
||||
editorLetterBox,
|
||||
timelineHeight,
|
||||
sidePanelWidth,
|
||||
sidePanelVisible,
|
||||
} from "~/pages/PageEnigma/signals";
|
||||
import { editorLetterBox, timelineHeight } from "~/pages/PageEnigma/signals";
|
||||
import { EngineContext } from "~/pages/PageEnigma/contexts/EngineContext";
|
||||
import { Letterbox } from "./Letterbox";
|
||||
|
||||
@@ -14,8 +9,7 @@ export const SceneContainer = ({ children }: { children: React.ReactNode }) => {
|
||||
useSignals();
|
||||
const editorEngine = useContext(EngineContext);
|
||||
|
||||
const containerWidth =
|
||||
pageWidth.value - (sidePanelVisible.value ? sidePanelWidth.value : 0) - 84;
|
||||
const containerWidth = pageWidth.value;
|
||||
|
||||
const containerHeight = pageHeight.value - timelineHeight.value - 64;
|
||||
|
||||
|
||||
+27
-14
@@ -1,9 +1,15 @@
|
||||
import { MediaItem } from "~/pages/PageEnigma/models";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import DndAsset from "~/pages/PageEnigma/DragAndDrop/DndAsset";
|
||||
import { SyntheticEvent } from "react";
|
||||
import { Badge } from "~/components";
|
||||
import { AssetType } from "~/enums";
|
||||
import { useState } from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faHandPointer } from "@fortawesome/pro-regular-svg-icons";
|
||||
import {
|
||||
faArrowPointer,
|
||||
faUpDownLeftRight,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
interface Props {
|
||||
debug?: string;
|
||||
@@ -35,12 +41,11 @@ const patchExpressionObjectType = (mediaType: string) => {
|
||||
|
||||
export const ItemElement = ({ item }: Props) => {
|
||||
useSignals();
|
||||
const defaultThumb = `/resources/images/default-covers/${(item.imageIndex || 0) % 24}.webp`;
|
||||
const thumbnail = item.thumbnail ? item.thumbnail : defaultThumb;
|
||||
const [imageError, setImageError] = useState(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="group relative w-full cursor-pointer select-none overflow-hidden rounded-lg transition-all duration-200 hover:brightness-110"
|
||||
className="group relative w-full cursor-pointer select-none overflow-hidden transition-all duration-200"
|
||||
onPointerDown={(event) => DndAsset.onPointerDown(event, item)}
|
||||
>
|
||||
{item.media_type && (
|
||||
@@ -56,16 +61,24 @@ export const ItemElement = ({ item }: Props) => {
|
||||
/>
|
||||
)}
|
||||
|
||||
<img
|
||||
crossOrigin="anonymous"
|
||||
src={thumbnail}
|
||||
onError={(e: SyntheticEvent<HTMLImageElement>) => {
|
||||
e.currentTarget.src = defaultThumb;
|
||||
}}
|
||||
alt={item.name}
|
||||
className="pointer-events-none aspect-[4.5/5] w-full select-none bg-gradient-to-b from-[#CCCCCC] to-[#A0A0A0] object-cover object-center"
|
||||
/>
|
||||
<div className="pointer-events-none w-full select-none truncate bg-brand-secondary-950 px-2 py-1 text-center text-[12px] transition-all duration-200 group-hover:bg-brand-secondary-800">
|
||||
<div className="pointer-events-none relative aspect-[16/12] w-full select-none overflow-hidden rounded-xl border-[3px] border-white/5 bg-brand-secondary-600 object-cover object-center transition-all group-hover:border-brand-primary">
|
||||
{item.thumbnail && !imageError && (
|
||||
<img
|
||||
crossOrigin="anonymous"
|
||||
referrerPolicy="no-referrer"
|
||||
src={item.thumbnail}
|
||||
alt={item.name}
|
||||
className="h-full w-full object-cover object-center"
|
||||
onError={() => setImageError(true)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="text-shadow-md absolute inset-0 flex items-center justify-center bg-brand-primary-950/50 text-[13px] font-medium text-white opacity-0 transition-opacity duration-200 group-hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faUpDownLeftRight} className="mr-1.5" />
|
||||
Drag to Scene
|
||||
</div>
|
||||
</div>
|
||||
<div className="pointer-events-none w-full select-none truncate py-1.5 text-start text-[13px] text-white/80 transition-all duration-200">
|
||||
{item.name || item.media_id}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+95
-32
@@ -1,17 +1,18 @@
|
||||
import { ItemElement } from "./ItemElement";
|
||||
import { MediaItem } from "~/pages/PageEnigma/models";
|
||||
import { dndSidePanelWidth, sidePanelWidth } from "~/pages/PageEnigma/signals";
|
||||
import { H4, P, LoadingDots } from "~/components";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faEmptySet } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { AutoSizer, Grid } from "react-virtualized";
|
||||
import "react-virtualized/styles.css";
|
||||
|
||||
interface Props {
|
||||
busy?: boolean;
|
||||
className?: string;
|
||||
debug?: string;
|
||||
currentPage?: number;
|
||||
pageSize?: number;
|
||||
items: MediaItem[];
|
||||
onLoadMore?: () => void;
|
||||
hasMore?: boolean;
|
||||
}
|
||||
|
||||
export const ItemElements = ({
|
||||
@@ -19,40 +20,76 @@ export const ItemElements = ({
|
||||
className,
|
||||
debug,
|
||||
items,
|
||||
currentPage,
|
||||
pageSize = 20,
|
||||
onLoadMore,
|
||||
hasMore,
|
||||
}: Props) => {
|
||||
const displayWidth =
|
||||
dndSidePanelWidth.value > -1
|
||||
? dndSidePanelWidth.value
|
||||
: sidePanelWidth.value;
|
||||
const GRID_GAP = 12;
|
||||
const ASPECT_RATIO = 16 / 12;
|
||||
const TEXT_HEIGHT = 32;
|
||||
const COLUMN_COUNT = 4;
|
||||
|
||||
const displayItems =
|
||||
currentPage !== undefined
|
||||
? items.slice(currentPage * pageSize, (currentPage + 1) * pageSize)
|
||||
: items;
|
||||
const rowCount = Math.ceil(items.length / COLUMN_COUNT);
|
||||
|
||||
function getGridColumnsClass(displayWidth: number): string {
|
||||
if (displayWidth <= 280) {
|
||||
return "grid-cols-2";
|
||||
} else if (displayWidth <= 360) {
|
||||
return "grid-cols-3";
|
||||
} else if (displayWidth <= 440) {
|
||||
return "grid-cols-4";
|
||||
} else {
|
||||
return "grid-cols-4";
|
||||
const cellRenderer = ({
|
||||
columnIndex,
|
||||
rowIndex,
|
||||
key,
|
||||
style,
|
||||
}: {
|
||||
columnIndex: number;
|
||||
rowIndex: number;
|
||||
key: string;
|
||||
style: React.CSSProperties;
|
||||
}) => {
|
||||
const index = rowIndex * COLUMN_COUNT + columnIndex;
|
||||
if (index >= items.length) return null;
|
||||
|
||||
// Calculate dimensions for the container
|
||||
const containerWidth = (style.width as number) - GRID_GAP;
|
||||
const containerHeight = (style.height as number) - GRID_GAP;
|
||||
|
||||
const adjustedStyle = {
|
||||
...style,
|
||||
left: `${(style.left as number) + GRID_GAP / 2}px`,
|
||||
top: `${(style.top as number) + GRID_GAP / 2}px`,
|
||||
width: `${containerWidth}px`,
|
||||
height: `${containerHeight}px`,
|
||||
padding: 0,
|
||||
};
|
||||
|
||||
return (
|
||||
<div key={key} style={adjustedStyle}>
|
||||
<ItemElement debug={debug} item={items[index]} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const handleScroll = ({
|
||||
clientHeight,
|
||||
scrollHeight,
|
||||
scrollTop,
|
||||
}: {
|
||||
clientHeight: number;
|
||||
scrollHeight: number;
|
||||
scrollTop: number;
|
||||
}) => {
|
||||
if (!onLoadMore || !hasMore || busy) return;
|
||||
|
||||
const threshold = 100; // pixels from bottom
|
||||
const bottom = scrollHeight - clientHeight - scrollTop;
|
||||
if (bottom < threshold) {
|
||||
onLoadMore();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const gridColumnsClass = getGridColumnsClass(displayWidth);
|
||||
|
||||
if (busy) {
|
||||
if (busy && items.length === 0) {
|
||||
return (
|
||||
<div className="flex h-full w-full">
|
||||
<LoadingDots className="bg-transparent" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (items.length === 0 && !busy) {
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center text-center">
|
||||
@@ -65,13 +102,39 @@ export const ItemElements = ({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`grid ${gridColumnsClass} gap-2.5 ${className ? " " + className : ""}`}
|
||||
>
|
||||
{displayItems.map((item, index) => (
|
||||
<ItemElement debug={debug} key={index} item={item} />
|
||||
))}
|
||||
<div className={`h-full w-full ${className || ""}`}>
|
||||
<AutoSizer className="w-full">
|
||||
{({ height, width }) => {
|
||||
// Calculate cell width accounting for gaps
|
||||
const cellWidth = Math.floor(
|
||||
(width - GRID_GAP * (COLUMN_COUNT + 1)) / COLUMN_COUNT,
|
||||
);
|
||||
const imageHeight = Math.floor(cellWidth / ASPECT_RATIO);
|
||||
const cellHeight = imageHeight + TEXT_HEIGHT + GRID_GAP;
|
||||
|
||||
return (
|
||||
<Grid
|
||||
cellRenderer={cellRenderer}
|
||||
columnCount={COLUMN_COUNT}
|
||||
columnWidth={cellWidth + GRID_GAP}
|
||||
height={height - GRID_GAP}
|
||||
rowCount={rowCount}
|
||||
rowHeight={cellHeight}
|
||||
width={width}
|
||||
style={{ outline: "none" }}
|
||||
overscanRowCount={2}
|
||||
onScroll={handleScroll}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</AutoSizer>
|
||||
{busy && hasMore && (
|
||||
<div className="flex w-full justify-center py-4">
|
||||
<LoadingDots className="bg-transparent" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { faCirclePlus } from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
import { usePosthogFeatureFlag } from "~/hooks/usePosthogFeatureFlag";
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
import {
|
||||
Button,
|
||||
FilterButtons,
|
||||
Pagination,
|
||||
SearchFilter,
|
||||
UploadModal3D,
|
||||
} from "~/components";
|
||||
@@ -33,6 +32,7 @@ import {
|
||||
} from "../hooks";
|
||||
|
||||
const filterEngineCategories = [FilterEngineCategories.LOCATION];
|
||||
const PAGE_SIZE = 42; // Load 2 pages worth of items at a time
|
||||
|
||||
export const SetsTab = () => {
|
||||
const showSearchObjectComponent = usePosthogFeatureFlag(
|
||||
@@ -40,6 +40,7 @@ export const SetsTab = () => {
|
||||
);
|
||||
|
||||
const [openUploadModal, setOpenUploadModal] = useState(false);
|
||||
const [loadedItemCount, setLoadedItemCount] = useState(PAGE_SIZE);
|
||||
|
||||
const { userObjects, userFetchStatus, fetchUserObjects } = useUserObjects({
|
||||
filterEngineCategories: filterEngineCategories,
|
||||
@@ -78,15 +79,11 @@ export const SetsTab = () => {
|
||||
const displayedItems =
|
||||
selectedFilter === AssetFilterOption.FEATURED
|
||||
? searchTermForFeaturedObjects
|
||||
? featuredObjectsSearchResults ?? []
|
||||
: featuredObjects ?? []
|
||||
? (featuredObjectsSearchResults ?? [])
|
||||
: (featuredObjects ?? [])
|
||||
: searchTermForUserObjects
|
||||
? userObjectsSearchResults ?? []
|
||||
: userObjects ?? [];
|
||||
|
||||
const [currentPage, setCurrentPage] = useState<number>(0);
|
||||
const pageSize = 21;
|
||||
const totalPages = Math.ceil(displayedItems.length / pageSize);
|
||||
? (userObjectsSearchResults ?? [])
|
||||
: (userObjects ?? []);
|
||||
|
||||
const isFetching = isAnyStatusFetching([
|
||||
userFetchStatus,
|
||||
@@ -95,17 +92,17 @@ export const SetsTab = () => {
|
||||
userObjectsSearchFetchStatus,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (searchTermForUserObjects.length > 0) {
|
||||
setCurrentPage(0);
|
||||
const handleLoadMore = useCallback(() => {
|
||||
if (!isFetching && loadedItemCount < displayedItems.length) {
|
||||
setLoadedItemCount((prev) =>
|
||||
Math.min(prev + PAGE_SIZE, displayedItems.length),
|
||||
);
|
||||
}
|
||||
}, [searchTermForUserObjects]);
|
||||
}, [isFetching, loadedItemCount, displayedItems.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (searchTermForFeaturedObjects.length > 0) {
|
||||
setCurrentPage(0);
|
||||
}
|
||||
}, [searchTermForFeaturedObjects]);
|
||||
setLoadedItemCount(PAGE_SIZE);
|
||||
}, [searchTermForUserObjects, searchTermForFeaturedObjects, selectedFilter]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -152,21 +149,11 @@ export const SetsTab = () => {
|
||||
<ItemElements
|
||||
busy={isFetching}
|
||||
debug="sets tab"
|
||||
currentPage={currentPage}
|
||||
pageSize={pageSize}
|
||||
items={displayedItems}
|
||||
items={displayedItems.slice(0, loadedItemCount)}
|
||||
onLoadMore={handleLoadMore}
|
||||
hasMore={loadedItemCount < displayedItems.length}
|
||||
/>
|
||||
</div>
|
||||
{totalPages > 1 && (
|
||||
<Pagination
|
||||
className="-mt-4 px-4"
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={(newPage: number) => {
|
||||
setCurrentPage(newPage);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<UploadModal3D
|
||||
onClose={() => setOpenUploadModal(false)}
|
||||
onSuccess={fetchUserObjects}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import { signal } from "@preact/signals-react";
|
||||
|
||||
export const assetModalVisibleDuringDrag = signal(true);
|
||||
export const reopenAfterDragSignal = signal(true);
|
||||
@@ -18,3 +18,4 @@ export * from "./stylizeTab";
|
||||
export * from "./timing";
|
||||
export * from "./timeline";
|
||||
export * from "./error";
|
||||
export * from "./assetModal";
|
||||
|
||||
@@ -40,7 +40,7 @@ export const outlinerState = {
|
||||
// locked: false,
|
||||
// },
|
||||
|
||||
export const outlinerIsShowing = signal(true);
|
||||
export const outlinerIsShowing = signal(false);
|
||||
|
||||
export const selectItem = (
|
||||
id: string,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Tab } from "~/pages/PageEnigma/models";
|
||||
import { pageHeight } from "~/signals";
|
||||
import { AssetFilterOption } from "~/enums";
|
||||
|
||||
export const sidePanelVisible = signal(true);
|
||||
export const sidePanelVisible = signal(false);
|
||||
|
||||
export const dndSidePanelWidth = signal(-1);
|
||||
|
||||
|
||||
Generated
+50
-2
@@ -25,6 +25,7 @@
|
||||
"@remix-run/serve": "^2.8.1",
|
||||
"@tweenjs/tween.js": "^18.6.4",
|
||||
"@types/lodash": "^4.17.6",
|
||||
"@types/react-virtualized": "^9.22.2",
|
||||
"@zip.js/zip.js": "^2.7.51",
|
||||
"ammo.js": "^0.0.10",
|
||||
"ccapture.js": "github:aceslowman/ccapture.js#npm-fix",
|
||||
@@ -44,6 +45,7 @@
|
||||
"react-drag-drop-files": "^2.3.10",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-image-crop": "^11.0.5",
|
||||
"react-virtualized": "^9.22.6",
|
||||
"tailwind-merge": "^2.2.2",
|
||||
"terser": "^5.30.3",
|
||||
"three": "^0.162.0",
|
||||
@@ -4589,14 +4591,12 @@
|
||||
"version": "15.7.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
|
||||
"integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.3.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz",
|
||||
"integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
@@ -4633,6 +4633,15 @@
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-virtualized": {
|
||||
"version": "9.22.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-virtualized/-/react-virtualized-9.22.2.tgz",
|
||||
"integrity": "sha512-0Eg/ME3OHYWGxs+/n4VelfYrhXssireZaa1Uqj5SEkTpSaBu5ctFGOCVxcOqpGXRiEdrk/7uho9tlZaryCIjHA==",
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/resolve": {
|
||||
"version": "1.20.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.6.tgz",
|
||||
@@ -7195,6 +7204,14 @@
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
|
||||
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/co": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
||||
@@ -7996,6 +8013,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dom-helpers": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
|
||||
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.8.7",
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/domexception": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz",
|
||||
@@ -18824,6 +18850,11 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-lifecycles-compat": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
|
||||
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
|
||||
},
|
||||
"node_modules/react-refresh": {
|
||||
"version": "0.14.2",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
|
||||
@@ -18874,6 +18905,23 @@
|
||||
"react": "^16.3.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-virtualized": {
|
||||
"version": "9.22.6",
|
||||
"resolved": "https://registry.npmjs.org/react-virtualized/-/react-virtualized-9.22.6.tgz",
|
||||
"integrity": "sha512-U5j7KuUQt3AaMatlMJ0UJddqSiX+Km0YJxSqbAzIiGw5EmNz0khMyqP2hzgu4+QUtm+QPIrxzUX4raJxmVJnHg==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.7.2",
|
||||
"clsx": "^1.0.4",
|
||||
"dom-helpers": "^5.1.3",
|
||||
"loose-envify": "^1.4.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"react-lifecycles-compat": "^3.0.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.3.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.3.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/read-cache": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"@remix-run/serve": "^2.8.1",
|
||||
"@tweenjs/tween.js": "^18.6.4",
|
||||
"@types/lodash": "^4.17.6",
|
||||
"@types/react-virtualized": "^9.22.2",
|
||||
"@zip.js/zip.js": "^2.7.51",
|
||||
"ammo.js": "^0.0.10",
|
||||
"ccapture.js": "github:aceslowman/ccapture.js#npm-fix",
|
||||
@@ -56,6 +57,7 @@
|
||||
"react-drag-drop-files": "^2.3.10",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-image-crop": "^11.0.5",
|
||||
"react-virtualized": "^9.22.6",
|
||||
"tailwind-merge": "^2.2.2",
|
||||
"terser": "^5.30.3",
|
||||
"three": "^0.162.0",
|
||||
|
||||
Reference in New Issue
Block a user