Compare commits

...
28 changed files with 872 additions and 344 deletions
@@ -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";
@@ -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>
);
};
@@ -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 ? (
@@ -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;
@@ -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>
@@ -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);
+50 -2
View File
@@ -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",
+2
View File
@@ -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",