Compare commits

..
27 changed files with 326 additions and 929 deletions
@@ -25,8 +25,6 @@ const F5_TTS_JOB_AVERAGE_SECONDS : u64 = 12;
const SEED_VC_JOB_AVERAGE_SECONDS : u64 = 60;
const IMAGE_GEN_API_AVERAGE_SECONDS: u64 = 70;
pub fn estimate_job_progress(job: &GenericInferenceJobStatus, maybe_args: Option<&PolymorphicInferenceArgs>) -> u8 {
match job.status {
// Jobs that haven't started
@@ -73,11 +71,10 @@ pub fn estimate_job_progress(job: &GenericInferenceJobStatus, maybe_args: Option
// TODO: Better estimate using video duration, params, etc.
InferenceCategory::Workflow => comfy_workflow_estimate(maybe_args, duration_seconds),
InferenceCategory::ImageGeneration => percent(duration_seconds, IMAGE_GEN_API_AVERAGE_SECONDS),
// NB: We don't run these job types anymore.
InferenceCategory::FormatConversion => 0,
InferenceCategory::Mocap => 0,
InferenceCategory::ImageGeneration => 0,
InferenceCategory::VideoFilter => 0,
InferenceCategory::ConvertBvhToWorkflow => 0,
InferenceCategory::DeprecatedField => 0, // TODO(bt,2024-07-16): Read job type instead.
@@ -73,6 +73,11 @@ export const PromptBox = () => {
selected: false,
icon: <FontAwesomeIcon icon={faSquare} className="h-4 w-4" />,
},
{
label: "16:9",
selected: false,
icon: <FontAwesomeIcon icon={faRectangleWide} className="h-4 w-4" />,
},
]);
const fileInputRef = useRef<HTMLInputElement>(null);
@@ -1,40 +1,42 @@
import { Transition } from "@headlessui/react";
import { twMerge } from "tailwind-merge";
import { Transition } from '@headlessui/react';
import { twMerge } from 'tailwind-merge';
import { H4 } from ".";
import { H4 } from '.';
interface LoadingDotsProps {
className?: string;
isShowing?: boolean;
}
};
interface LoadingDotsInnerProps {
className?: string;
isShowing?: boolean;
type?: "typing" | "bricks";
type?: 'typing'|'bricks';
message?: string;
}
};
export const LoadingDotsTyping = (props: LoadingDotsProps) => {
return <LoadingDots {...props} />;
};
export const LoadingDotsBricks = (props: LoadingDotsProps) => {
return <LoadingDots {...props} type="bricks" />;
};
export const LoadingDotsTyping = (props: LoadingDotsProps)=>{
return (
<LoadingDots {...props} />
)
}
export const LoadingDotsBricks = (props: LoadingDotsProps)=>{
return (
<LoadingDots {...props} type="bricks"/>
)
}
export function LoadingDots({
className,
isShowing = true,
type = "typing",
isShowing=true,
type = 'typing',
message,
}: LoadingDotsInnerProps) {
const classNames = twMerge(
"relative w-full h-full flex flex-col justify-center items-center bg-ui-background gap-6 z-50",
className,
);
}: LoadingDotsInnerProps){
const classNames = twMerge("w-full h-full flex flex-col justify-center items-center bg-ui-background gap-6", className);
return (
return(
<Transition
className={classNames}
show={isShowing}
enter="transition-opacity duration-150"
enterFrom="opacity-0"
@@ -43,11 +45,17 @@ export function LoadingDots({
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className={classNames}>
{type === "typing" && <div className="dot-typing"></div>}
{type === "bricks" && <div className="dot-bricks"></div>}
{message && <H4>{message}</H4>}
</div>
{ type==='typing' &&
<div className="dot-typing"></div>
}
{ type==='bricks' &&
<div className="dot-bricks"></div>
}
{
message &&
<H4>{message}</H4>
}
</Transition>
);
}
};
@@ -1,37 +1,29 @@
import { useState, useEffect } from "react";
import { useState } from "react";
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { Tooltip } from "../Tooltip"; // Adjust the path if necessary
interface Option {
value: string;
icon: IconDefinition;
text?: string;
tooltip?: string;
}
interface ButtonIconSelectProps {
options: Option[];
onOptionChange?: (value: string) => void;
selectedOption?: string;
}
export function ButtonIconSelect({
options,
onOptionChange,
selectedOption,
}: ButtonIconSelectProps) {
const [internalSelectedOption, setInternalSelectedOption] = useState<string>(
selectedOption || options[0].value,
const [selectedOption, setSelectedOption] = useState<string>(
options[0].value,
);
useEffect(() => {
setInternalSelectedOption(selectedOption || options[0].value);
}, [selectedOption, options]);
const handleOptionChange = (value: string) => {
setInternalSelectedOption(value);
setSelectedOption(value);
if (onOptionChange) {
onOptionChange(value);
}
@@ -39,50 +31,24 @@ export function ButtonIconSelect({
return (
<div className="flex space-x-1">
{options.map(({ value, icon, text, tooltip }) =>
tooltip ? (
<Tooltip
key={value}
content={tooltip}
position="bottom"
delay={300}
closeOnClick
>
<button
className={twMerge(
"flex h-9 items-center justify-center rounded-lg border text-sm outline-none transition-all duration-150 focus:outline-none",
text ? "h-auto w-auto gap-2 px-3 py-1.5" : "w-9",
internalSelectedOption === value
? "border-brand-primary bg-brand-primary/20"
: "border-transparent hover:bg-ui-panel/[0.4]",
)}
onClick={() => handleOptionChange(value)}
>
<FontAwesomeIcon icon={icon} />
{text && (
<span className="text-nowrap text-sm font-medium">{text}</span>
)}
</button>
</Tooltip>
) : (
<button
key={value}
className={twMerge(
`flex h-9 items-center justify-center rounded-lg border text-sm transition-all duration-150`,
text ? "h-auto w-auto gap-2 px-3 py-1.5" : "w-9",
internalSelectedOption === value
? "border-brand-primary bg-brand-primary/20"
: "border-transparent hover:bg-ui-panel/[0.4]",
)}
onClick={() => handleOptionChange(value)}
>
<FontAwesomeIcon icon={icon} />
{text && (
<span className="text-nowrap text-sm font-medium">{text}</span>
)}
</button>
),
)}
{options.map(({ value, icon, text }) => (
<button
key={value}
className={twMerge(
`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-brand-primary/20"
: "border-transparent hover:bg-ui-panel/[0.4]",
)}
onClick={() => handleOptionChange(value)}
>
<FontAwesomeIcon icon={icon} />
{text && (
<span className="text-nowrap text-sm font-medium">{text}</span>
)}
</button>
))}
</div>
);
}
@@ -13,7 +13,7 @@ import {
} from "@fortawesome/pro-solid-svg-icons";
import { Tooltip } from "~/components";
const DialogBackdrop = ({ className }: { className?: string }) => {
const DialogBackdrop = () => {
useEffect(() => {
disableHotkeyInput(DomLevels.DIALOGUE);
return () => {
@@ -31,7 +31,7 @@ const DialogBackdrop = ({ className }: { className?: string }) => {
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className={twMerge("fixed inset-0 bg-black/60", className)} />
<div className="fixed inset-0 bg-black/60" />
</Transition.Child>
);
};
@@ -43,7 +43,6 @@ export const TransitionDialogue = ({
onTitleIconClick,
onClose,
className,
backdropClassName,
width,
children,
childPadding = true,
@@ -57,7 +56,6 @@ export const TransitionDialogue = ({
titleIconClassName?: string;
onClose: () => void;
className?: string;
backdropClassName?: string;
width?: number;
children: ReactNode;
childPadding?: boolean;
@@ -66,7 +64,7 @@ export const TransitionDialogue = ({
return (
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-[70]" onClose={onClose}>
<DialogBackdrop className={backdropClassName} />
<DialogBackdrop />
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child
@@ -98,6 +98,7 @@ export const SceneTitleInput = ({ pageName }: Props) => {
className={twMerge(
"flex w-full items-center justify-center gap-1.5",
isSaving && "ml-3",
currentPage.value === Pages.STYLE ? "mr-0" : "mr-[70px]",
)}
>
{!showInput && (
@@ -11,6 +11,7 @@ import {
currPosition,
dragItem,
overTimeline,
sidePanelWidth,
timelineHeight,
assetModalVisibleDuringDrag,
reopenAfterDragSignal,
@@ -74,10 +75,10 @@ class DndAsset {
if (positionY < 69) {
return false;
}
if (positionY > pageHeight.value) {
if (positionY > pageHeight.value - timelineHeight.value) {
return false;
}
return positionX <= pageWidth.value;
return positionX <= pageWidth.value - sidePanelWidth.value - 84;
}
onPointerUp(event: PointerEvent) {
@@ -1,96 +0,0 @@
import * as THREE from "three";
export class InfiniteGridHelper extends THREE.Mesh {
constructor(
size1?: number,
size2?: number,
color?: THREE.Color,
distance?: number,
axes: string = "xzy",
) {
color = color || new THREE.Color("white");
size1 = size1 || 10;
size2 = size2 || 100;
distance = distance || 8000;
const planeAxes = axes.substr(0, 2);
const geometry = new THREE.PlaneGeometry(2, 2, 1, 1);
const material = new THREE.ShaderMaterial({
side: THREE.DoubleSide,
uniforms: {
uSize1: {
value: size1,
},
uSize2: {
value: size2,
},
uColor: {
value: color,
},
uDistance: {
value: distance,
},
uFadeStrength: {
value: 1.5,
},
uFadeStart: {
value: 0.2,
},
},
transparent: true,
vertexShader: `
varying vec3 worldPosition;
uniform float uDistance;
void main() {
vec3 pos = position.${axes} * uDistance;
pos.${planeAxes} += cameraPosition.${planeAxes};
worldPosition = pos;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
`,
fragmentShader: `
varying vec3 worldPosition;
uniform float uSize1;
uniform float uSize2;
uniform vec3 uColor;
uniform float uDistance;
uniform float uFadeStrength;
uniform float uFadeStart;
float getGrid(float size) {
vec2 r = worldPosition.${planeAxes} / size;
vec2 grid = abs(fract(r - 0.5) - 0.5) / (fwidth(r) * 1.5);
float line = min(grid.x, grid.y);
return 1.0 - min(line, 1.0);
}
void main() {
float distFromCamera = length(cameraPosition.${planeAxes} - worldPosition.${planeAxes}) / uDistance;
float fadeOut = 1.0 - smoothstep(uFadeStart, 1.0, distFromCamera);
fadeOut = pow(fadeOut, uFadeStrength);
float g1 = getGrid(uSize1);
float g2 = getGrid(uSize2);
float grid = mix(g2 * 0.3, g1, g1);
gl_FragColor = vec4(uColor.rgb, grid * fadeOut);
gl_FragColor.a = mix(0.5 * gl_FragColor.a, gl_FragColor.a, g2);
if (gl_FragColor.a <= 0.0) discard;
}
`,
extensions: {
derivatives: true,
},
});
super(geometry, material);
this.frustumCulled = false;
}
}
@@ -55,20 +55,12 @@ export class FKHelper {
}
// For each target bone, display a sphere to show the FK target and raycast intersect
const geometry = new THREE.SphereGeometry(0.015, 16, 16);
const geometry = new THREE.SphereGeometry(1, 16, 16);
const material = new THREE.MeshBasicMaterial({ color: 0xff0000, depthTest: false, transparent: true });
const sphere = new THREE.Mesh(geometry, material);
// FIX: Set the sphere scale to counter parent's world scale to remain constant in size
// TODO: Make a scale clamp
const parentScale = new THREE.Vector3();
child.getWorldScale(parentScale);
const sphereScale = new THREE.Vector3(1, 1, 1);
sphereScale.divide(parentScale);
sphere.scale.copy(sphereScale);
sphere.name = FKHelperSphereName;
const uniformScale = 1.5;
sphere.scale.set(uniformScale, uniformScale, uniformScale);
child.add(sphere);
this.targetBoneSpheres.push(sphere);
});
@@ -52,7 +52,6 @@ import { BufferType, EngineFrameBuffers } from "./VideoProcessor/engine_buffer";
import Stats from "three/examples/jsm/libs/stats.module.js";
import { CharacterAnimationEngine } from "./Engines/CharacterAnimationEngine";
import { cameras, selectedCameraId } from "~/pages/PageEnigma/signals/camera";
export type EditorInitializeConfig = {
sceneToken: string;
@@ -193,8 +192,6 @@ class Editor {
processingHasFailed: boolean;
stats: Stats;
cameras: Map<string, THREE.PerspectiveCamera> = new Map();
constructor() {
this.processingHasFailed = false;
console.log(
@@ -326,134 +323,8 @@ class Editor {
this.engineFrameBuffers.frameWorkerManager.type = imageFormat;
}
// Initialize cameras from signals
cameras.value.forEach((camData) => {
const camera = new THREE.PerspectiveCamera(
camData.fov,
this.getRenderDimensions().aspectRatio,
0.01,
200,
);
camera.position.set(
camData.position.x,
camData.position.y,
camData.position.z,
);
camera.rotation.set(
camData.rotation.x,
camData.rotation.y,
camData.rotation.z,
);
this.cameras.set(camData.id, camera);
});
// Set initial camera
this.camera = this.cameras.get("main") || null;
this.render_camera = this.camera;
// Subscribe to camera changes
this.setupCameraSignalHandlers();
// New Rendering Pipeline Engine Work
}
// Add helper method to convert focal length to FOV
focalLengthToFov(focalLength: number, sensorHeight: number = 24): number {
// Using the formula: FOV = 2 * arctan(sensorHeight / (2 * focalLength))
return 2 * Math.atan(sensorHeight / (2 * focalLength)) * (180 / Math.PI);
}
setupCameraSignalHandlers() {
// Handle camera selection changes
selectedCameraId.subscribe((id) => {
const newCamera = this.cameras.get(id);
if (newCamera) {
this.camera = newCamera;
this.render_camera = newCamera;
// Update camera properties in signals
const camData = cameras.value.find((c) => c.id === id);
if (camData) {
// Convert focal length to FOV
const fov = this.focalLengthToFov(camData.focalLength);
newCamera.fov = fov;
newCamera.updateProjectionMatrix();
this.renderScene(); // Force a render update
}
}
});
// Handle camera updates
cameras.subscribe((newCameras) => {
newCameras.forEach((camData) => {
let camera = this.cameras.get(camData.id);
// Create new camera if it doesn't exist
if (!camera) {
const fov = this.focalLengthToFov(camData.focalLength);
camera = new THREE.PerspectiveCamera(
fov,
this.getRenderDimensions().aspectRatio,
0.01,
200,
);
this.cameras.set(camData.id, camera);
}
// Update camera properties
camera.position.set(
camData.position.x,
camData.position.y,
camData.position.z,
);
camera.rotation.set(
camData.rotation.x,
camData.rotation.y,
camData.rotation.z,
);
// Update FOV based on focal length
const fov = this.focalLengthToFov(camData.focalLength);
camera.fov = fov;
camera.updateProjectionMatrix();
// Force a render update if this is the active camera
if (camData.id === selectedCameraId.value) {
this.renderScene();
}
});
// Clean up deleted cameras
const currentIds = new Set(newCameras.map((c) => c.id));
for (const [id] of this.cameras.entries()) {
if (!currentIds.has(id) && id !== "main") {
this.cameras.delete(id);
}
}
});
}
// Update camera position in signals when it changes
updateCameraPosition() {
if (this.camera && selectedCameraId.value) {
const pos = this.camera.position;
const rot = this.camera.rotation;
const camData = cameras.value.find(
(c) => c.id === selectedCameraId.value,
);
if (camData) {
cameras.value = cameras.value.map((c) =>
c.id === selectedCameraId.value
? {
...c,
position: { x: pos.x, y: pos.y, z: pos.z },
rotation: { x: rot.x, y: rot.y, z: rot.z },
}
: c,
);
}
}
}
getRenderDimensions() {
switch (this.render_camera_aspect_ratio) {
case CameraAspectRatio.HORIZONTAL_16_9: {
@@ -1288,21 +1159,6 @@ class Editor {
}
const delta_time = this.clock.getDelta();
// Update camera properties from signals before FreeCam update
if (selectedCameraId.value && this.camera) {
const camData = cameras.value.find(
(c) => c.id === selectedCameraId.value,
);
if (camData) {
const fov = this.focalLengthToFov(camData.focalLength);
if (this.camera.fov !== fov) {
this.camera.fov = fov;
this.camera.updateProjectionMatrix();
}
}
}
this.cameraViewControls?.update(5 * delta_time);
this.activeScene.shader_objects.forEach((shader) => {
shader.material.uniforms["time"].value += 0.5 * delta_time;
@@ -1,6 +1,5 @@
import { EventDispatcher, Quaternion, Vector3, Vector2 } from "three";
import * as THREE from "three";
import { cameras, selectedCameraId } from "~/pages/PageEnigma/signals/camera";
class FreeCam extends EventDispatcher {
object: THREE.PerspectiveCamera;
@@ -229,22 +228,6 @@ class FreeCam extends EventDispatcher {
this.object.translateX(this.moveVector.x * moveMulti);
this.object.translateY(this.moveVector.y * moveMulti);
this.object.translateZ(this.moveVector.z * moveMulti);
// Update camera position in signals
if (selectedCameraId.value) {
const pos = this.object.position;
const rot = this.object.rotation;
cameras.value = cameras.value.map((cam) =>
cam.id === selectedCameraId.value
? {
...cam,
position: { x: pos.x, y: pos.y, z: pos.z },
rotation: { x: rot.x, y: rot.y, z: rot.z },
}
: cam,
);
}
}
updateMovementVector() {
@@ -4,7 +4,6 @@ import {
hotkeysStatus,
outlinerState,
showObjectPanel,
assetModalVisible,
} from "../signals";
import {
OrbitControls,
@@ -16,7 +15,6 @@ import { SceneManager, SceneObject } from "./scene_manager_api";
import { FreeCam } from "./free_cam";
import Stats from "three/examples/jsm/libs/stats.module.js";
import { FKHelper } from "./KinHelpers/FKHelper";
import { selectedMode } from "../signals/selectedMode";
export enum KinMode {
FK,
@@ -59,7 +57,6 @@ export class MouseControls {
private kinMode: KinMode = KinMode.NONE;
private fkHelper: FKHelper;
private isBoneDragged: boolean = false;
private ignoreNextClick: boolean = false;
constructor(
camera: THREE.PerspectiveCamera,
@@ -86,7 +83,7 @@ export class MouseControls {
getAssetType: Function,
setSelected: Function,
isMovable: Function,
enable_stats: Function,
enable_stats: Function
) {
this.camera = camera;
this.camera_person_mode = camera_person_mode;
@@ -125,10 +122,6 @@ export class MouseControls {
onFKControlsDragging(dragging: boolean) {
this.isBoneDragged = dragging;
// FIX: Window dispatches a click event after FK dragging is complete
// This flag can be used to ignore that when FK is adjusted
this.ignoreNextClick = true;
}
clearFKVisuals() {
@@ -200,11 +193,7 @@ export class MouseControls {
}
onMouseDown(event: any) {
if (
(event.button === 0 || event.button === 1) &&
this.isMovable() &&
!this.isBoneDragged
) {
if ((event.button === 0 || event.button === 1) && this.isMovable() && !this.isBoneDragged) {
this.isMouseClicked = true;
}
}
@@ -279,25 +268,17 @@ export class MouseControls {
} else if (event.key === "t") {
// transform
this.control?.setMode("translate");
selectedMode.value = "move";
return;
} else if (event.key === "r" && !event.ctrlKey) {
// rotate
this.control?.setMode("rotate");
selectedMode.value = "rotate";
return;
} else if (event.key === "g") {
// scale
this.control?.setMode("scale");
selectedMode.value = "scale";
return;
} else if (event.key === "k") {
this.toggleFKMode();
} else if (event.key === "b") {
// Open asset modal
event.preventDefault();
assetModalVisible.value = true;
return;
}
if ((event.ctrlKey || event.metaKey) && !this.isProcessing) {
@@ -369,13 +350,6 @@ export class MouseControls {
if (this.camera == undefined) {
return;
}
// Ignore window clicks if FK is active and bone is being transformed
if (this.ignoreNextClick) {
this.ignoreNextClick = false;
return;
}
const camera_pos = new THREE.Vector3(
parseFloat(this.camera.position.x.toFixed(2)),
parseFloat(this.camera.position.y.toFixed(2)),
@@ -15,13 +15,10 @@ import { ClipGroup, ClipType } from "~/enums";
import { ClipUI } from "../clips/clip_ui";
import { GetCdnOrigin } from "~/api/GetCdnOrigin";
import { GetFrontendEnvironment } from "~/Classes/GetFrontendEnvironment";
import { gridVisibility } from "../signals/engine";
import { InfiniteGridHelper } from "./InfiniteGridHelper";
class Scene {
name: string;
gridHelper: InfiniteGridHelper | undefined;
groundPlane: THREE.Mesh | undefined;
gridHelper: THREE.GridHelper | undefined;
scene: THREE.Scene;
hot_items: THREE.Object3D[] | undefined;
@@ -68,7 +65,7 @@ class Scene {
this.message_mesh = undefined;
this.loading_placeholder = undefined;
this.message_font = undefined;
this.skybox = "m_1";
this.skybox = "Default";
// global names
this.camera_name = camera_name;
@@ -90,19 +87,6 @@ class Scene {
this.helper = new MMDAnimationHelper({ afterglow: 0.0 });
this.scene.userData["helper"] = this.helper;
// Subscribe to grid visibility changes
gridVisibility.subscribe((isVisible) => {
if (this.gridHelper) {
if (isVisible) {
if (!this.scene.children.includes(this.gridHelper)) {
this.scene.add(this.gridHelper);
}
} else {
this.scene.remove(this.gridHelper);
}
}
});
}
clear() {
@@ -373,9 +357,9 @@ class Scene {
const camera_position = new THREE.Vector3(0, 0.6, 1.5);
// TODO(bt,2025-02-10): Make the local dev assets more configurable or seedable.
const camera_id = GetFrontendEnvironment().getIsLocalDev()
? "m_4djqzq89mbn6sdbbqm25ks6drhr55k" // Local development
: "m_cxh4asqhapdz10j880755dg4yevshb"; // Production
const camera_id = GetFrontendEnvironment().getIsLocalDev() ?
"m_4djqzq89mbn6sdbbqm25ks6drhr55k" : // Local development
"m_cxh4asqhapdz10j880755dg4yevshb"; // Production
const camera_obj = await this.loadGlbWithPlaceholder(
camera_id,
@@ -785,20 +769,7 @@ class Scene {
_create_skybox() {
const loader = new THREE.CubeTextureLoader();
if (this.skybox == "Default") {
const texture = loader.load([
"/resources/skybox/day/px.png",
"/resources/skybox/day/nx.png",
"/resources/skybox/day/py.png",
"/resources/skybox/day/ny.png",
"/resources/skybox/day/pz.png",
"/resources/skybox/day/nz.png",
]);
this.scene.background = texture;
if (this.ambientLight) this.scene.remove(this.ambientLight);
if (this.directional_light) this.scene.add(this.directional_light);
if (this.hemisphereLight) this.scene.add(this.hemisphereLight);
} else if (this.skybox == "m_0") {
if (this.skybox == "m_0") {
const texture = loader.load([
"/resources/skybox/night/Night_Moon_Burst_Cam_2_LeftX.png",
"/resources/skybox/night/Night_Moon_Burst_Cam_3_Right-X.png",
@@ -812,17 +783,6 @@ class Scene {
if (this.directional_light) this.scene.add(this.directional_light);
if (this.hemisphereLight) this.scene.add(this.hemisphereLight);
} else if (this.skybox == "m_1") {
// Empty skybox - just a plain color
this.scene.background = new THREE.Color("#282828");
if (this.ambientLight) this.scene.remove(this.ambientLight);
if (this.directional_light) this.scene.add(this.directional_light);
if (this.hemisphereLight) this.scene.add(this.hemisphereLight);
} else if (this.skybox == "m_2") {
this.scene.background = new THREE.Color("#000000");
if (this.ambientLight) this.scene.add(this.ambientLight);
if (this.directional_light) this.scene.remove(this.directional_light);
if (this.hemisphereLight) this.scene.remove(this.hemisphereLight);
} else if (this.skybox == "m_3") {
const texture = loader.load([
"/resources/skybox/gray/Sky_AllSky_Overcast4_Low_Cam_2_LeftX.png",
"/resources/skybox/gray/Sky_AllSky_Overcast4_Low_Cam_3_Right-X.png",
@@ -835,15 +795,19 @@ class Scene {
if (this.ambientLight) this.scene.remove(this.ambientLight);
if (this.directional_light) this.scene.add(this.directional_light);
if (this.hemisphereLight) this.scene.add(this.hemisphereLight);
} else if (this.skybox == "m_2") {
this.scene.background = new THREE.Color("#000000");
if (this.ambientLight) this.scene.add(this.ambientLight);
if (this.directional_light) this.scene.remove(this.directional_light);
if (this.hemisphereLight) this.scene.remove(this.hemisphereLight);
} else {
// Default to empty skybox if no match
this.scene.background = new THREE.Color("#282828");
if (this.ambientLight) this.scene.remove(this.ambientLight);
if (this.directional_light) this.scene.add(this.directional_light);
if (this.hemisphereLight) this.scene.add(this.hemisphereLight);
}
console.log("Background creation..");
console.log("Backround creation..");
}
updateSkybox(media_id: string) {
@@ -884,52 +848,16 @@ class Scene {
}
_createGrid() {
// Create visual infinite grid
const size1 = 0.5; // Primary grid
const size2 = 2.5; // Secondary grid
const color = new THREE.Color(0x444444);
const distance = 80;
this.gridHelper = new InfiniteGridHelper(size1, size2, color, distance);
const size = 25;
const divisions = 50;
this.gridHelper = new THREE.GridHelper(
size,
divisions,
new THREE.Color("rgb(199,195,195)"),
new THREE.Color("rgb(161,157,157)"),
);
this.gridHelper.layers.set(1); // Enable default layer
this.scene.add(this.gridHelper);
// Create invisible ground plane for proper object placement
const planeGeometry = new THREE.PlaneGeometry(300, 300); // Same as original grid size
const planeMaterial = new THREE.MeshBasicMaterial({
visible: false,
side: THREE.DoubleSide,
});
this.groundPlane = new THREE.Mesh(planeGeometry, planeMaterial);
this.groundPlane.rotation.x = -Math.PI / 2;
this.groundPlane.name = ""; // Empty name to avoid selection
this.groundPlane.layers.set(0); // Put in layer 0 for placement raycasting
this.groundPlane.userData["selectable"] = false; // Mark as non-selectable
this.groundPlane.userData["placementOnly"] = false; // Don't include in interactable list
this.groundPlane.matrixAutoUpdate = false; // Optimize performance since it never moves
this.groundPlane.updateMatrix();
this.groundPlane.receiveShadow = false;
// Store the original raycast function
const originalRaycast = this.groundPlane.raycast;
// Override raycast to only work when the raycaster is checking layer 0
this.groundPlane.raycast = function (raycaster, intersects) {
if (raycaster.layers.test(this.layers)) {
return originalRaycast.call(this, raycaster, intersects);
}
};
this.scene.add(this.groundPlane);
}
// Update grid visibility based on the signal
updateGridVisibility() {
if (gridVisibility.value) {
if (!this.scene.children.includes(this.gridHelper)) {
this.scene.add(this.gridHelper);
}
} else {
this.scene.remove(this.gridHelper);
}
// Keep the ground plane regardless of grid visibility
}
}
@@ -1,8 +1,11 @@
import React, { useEffect } from "react";
import { useSignals } from "@preact/signals-react/runtime";
import { LoadingDots, Toaster, TopBar } from "~/components";
import { SidePanel } from "~/pages/PageEnigma/comps/SidePanel";
import { Timeline } from "./comps/Timeline";
import { Controls3D } from "./comps/Controls3D";
import { ControlsTopButtons } from "./comps/ControlsTopButtons";
import { ControlsVideo } from "./comps/ControlsVideo";
import { ControlPanelSceneObject } from "./comps/ControlPanelSceneObject";
import { PreviewEngineCamera } from "./comps/PreviewEngineCamera";
import { PreviewFrameImage } from "./comps/PreviewFrameImage";
@@ -23,8 +26,7 @@ import { SceneContainer } from "./comps/SceneContainer";
import { Outliner } from "./comps/Outliner";
import { CameraAspectRatio } from "./enums";
import { PromptBox } from "./comps/PromptBox";
import { OnboardingHelper } from "./comps/OnboardingHelper";
import { focalLengthDragging } from "./signals/camera";
import { AssetMenu } from "./comps/AssetMenu";
export const PageEditor = () => {
useSignals();
@@ -82,7 +84,6 @@ export const PageEditor = () => {
return (
<div className="w-screen">
<TopBar pageName="Edit Scene" />
<OnboardingHelper />
<div
className="relative flex w-screen"
style={{ height: "calc(100vh - 68px)" }}
@@ -102,15 +103,6 @@ export const PageEditor = () => {
<PreviewFrameImage />
</SceneContainer>
{/* Focal Length Display */}
{focalLengthDragging.value.isDragging && (
<div className="absolute left-1/2 top-24 -translate-x-1/2 transform">
<div className="glass rounded-xl px-6 py-3 text-center text-3xl font-bold text-white">
{focalLengthDragging.value.focalLength}mm
</div>
</div>
)}
{/* Top controls */}
<div
className="absolute left-0 top-0 w-full"
@@ -122,6 +114,8 @@ export const PageEditor = () => {
</div>
</div>
<AssetMenu />
{/* Bottom controls */}
<div
className="absolute bottom-0 left-0"
@@ -144,7 +138,7 @@ export const PageEditor = () => {
<PromptBox />
<LoadingDots
className="absolute left-0 top-0 z-50"
className="absolute left-0 top-0"
isShowing={editorLoader.value.isShowing}
type="bricks"
message={editorLoader.value.message}
@@ -6,10 +6,9 @@ import {
faLayerGroup,
faUser,
faSun,
faTree,
faCube,
faChevronRight,
faMountainCity,
faDog,
} from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Button, CloseButton, Input, Tooltip } from "~/components";
@@ -22,7 +21,6 @@ import {
demoCharacterItems,
assetModalVisibleDuringDrag,
reopenAfterDragSignal,
assetModalVisible,
} from "../../signals";
import { MediaItem } from "../../models";
import { useUserObjects, useFeaturedObjects } from "../SidePanelTabs/hooks";
@@ -74,32 +72,28 @@ const AllTabSection = ({
</div>
);
export const AssetModal = () => {
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(false);
const [reopenAfterAdd, setReopenAfterAdd] = useState(true);
const searchInputRef = useRef<HTMLInputElement>(null);
const handleClose = () => {
assetModalVisible.value = false;
};
// Update the signal when the preference changes
useEffect(() => {
reopenAfterDragSignal.value = reopenAfterAdd;
}, [reopenAfterAdd]);
useEffect(() => {
if (assetModalVisible.value) {
if (isOpen) {
// Small delay to ensure the modal is fully mounted
const timer = setTimeout(() => {
searchInputRef.current?.focus();
}, 100);
return () => clearTimeout(timer);
}
}, [assetModalVisible.value]);
}, [isOpen]);
// Fetch objects for different categories
const { userObjects: userCharacters } = useUserObjects({
@@ -112,6 +106,11 @@ export const AssetModal = () => {
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",
@@ -122,24 +121,9 @@ export const AssetModal = () => {
defaultErrorMessage: "Error fetching featured objects",
});
const { userObjects: userSets } = useUserObjects({
filterEngineCategories: [FilterEngineCategories.LOCATION],
defaultErrorMessage: "Error fetching user sets",
});
const { featuredObjects: featuredSets } = useFeaturedObjects({
filterEngineCategories: [FilterEngineCategories.LOCATION],
defaultErrorMessage: "Error fetching featured sets",
});
const { userObjects: userCreatures } = useUserObjects({
filterEngineCategories: [FilterEngineCategories.CREATURE],
defaultErrorMessage: "Error fetching user creatures",
});
const { featuredObjects: featuredCreatures } = useFeaturedObjects({
filterEngineCategories: [FilterEngineCategories.CREATURE],
defaultErrorMessage: "Error fetching featured creatures",
const { featuredObjects: featuredNature } = useFeaturedObjects({
filterEngineCategories: [FilterEngineCategories.SET_DRESSING],
defaultErrorMessage: "Error fetching featured nature assets",
});
const libraryTabs = [
@@ -170,32 +154,22 @@ export const AssetModal = () => {
? [...demoShapeItems.value, ...(featuredObjects ?? [])]
: (userObjects ?? []),
},
{
id: "sets",
label: "Sets",
icon: faMountainCity,
engineCategory: FilterEngineCategories.LOCATION,
items:
activeLibraryTab === "library"
? (featuredSets ?? [])
: (userSets ?? []),
},
{
id: "creatures",
label: "Creatures",
icon: faDog,
engineCategory: FilterEngineCategories.CREATURE,
items:
activeLibraryTab === "library"
? (featuredCreatures ?? [])
: (userCreatures ?? []),
},
{
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
@@ -267,10 +241,10 @@ export const AssetModal = () => {
if (reopenAfterAdd) {
// Small delay to allow the modal to close and reopen
setTimeout(() => {
handleClose();
onClose();
}, 100);
} else {
handleClose();
onClose();
}
};
@@ -278,8 +252,8 @@ export const AssetModal = () => {
return (
<TransitionDialogue
isOpen={assetModalVisible.value && assetModalVisibleDuringDrag.value}
onClose={handleClose}
isOpen={isOpen && assetModalVisibleDuringDrag.value}
onClose={onClose}
className="h-[640px] max-w-4xl"
childPadding={false}
>
@@ -355,7 +329,7 @@ export const AssetModal = () => {
/>
)}
</div>
<CloseButton onClick={handleClose} />
<CloseButton onClick={onClose} />
</div>
<div
className={twMerge(
@@ -1,15 +1,9 @@
import { TransitionDialogue } from "~/components/reusable/TransitionDialogue";
import { faPlus, faTrashAlt } from "@fortawesome/pro-solid-svg-icons";
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";
import {
updateCamera,
focalLengthDragging,
} from "~/pages/PageEnigma/signals/camera";
import { useState, useEffect } from "react";
import { twMerge } from "tailwind-merge";
interface ExtendedPopoverItem extends PopoverItem {
id: string;
@@ -40,45 +34,12 @@ export const CameraSettingsModal = ({
onDeleteCamera,
}: CameraSettingsModalProps) => {
const selectedCamera = cameras.find((cam) => cam.id === selectedCameraId);
const [isDragging, setIsDragging] = useState(false);
useEffect(() => {
const handlePointerUp = () => {
setIsDragging(false);
focalLengthDragging.value = {
isDragging: false,
focalLength: selectedCamera?.focalLength || 35,
};
};
document.addEventListener("pointerup", handlePointerUp);
return () => document.removeEventListener("pointerup", handlePointerUp);
}, [selectedCamera?.focalLength]);
const handlePointerDown = () => {
setIsDragging(true);
focalLengthDragging.value = {
isDragging: true,
focalLength: selectedCamera?.focalLength || 35,
};
};
const handleFocalLengthChange = (id: string, value: number) => {
focalLengthDragging.value = { isDragging: true, focalLength: value };
onCameraFocalLengthChange(id, value);
};
return (
<TransitionDialogue
isOpen={isOpen}
onClose={onClose}
className={twMerge(
"h-[500px] max-w-3xl transition-opacity duration-200",
isDragging ? "opacity-30 hover:opacity-20" : "opacity-100",
)}
backdropClassName={twMerge(
"transition-opacity duration-200",
isDragging ? "opacity-0" : "opacity-100",
)}
className="h-[500px] max-w-3xl"
childPadding={false}
>
<div className="grid h-full grid-cols-12 gap-3">
@@ -166,17 +127,14 @@ export const CameraSettingsModal = ({
<Label htmlFor="focal-length" className="text-sm opacity-70">
Focal Length
</Label>
<div
className="mt-1 flex items-center gap-4"
onPointerDown={handlePointerDown}
>
<div className="mt-1 flex items-center gap-4">
<SliderV2
min={10}
max={200}
value={selectedCamera?.focalLength || 35}
onChange={(value) =>
selectedCamera &&
handleFocalLengthChange(selectedCamera.id, value)
onCameraFocalLengthChange(selectedCamera.id, value)
}
step={1}
suffix="mm"
@@ -1,39 +1,15 @@
import {
faArrowsRotate,
faArrowsUpDownLeftRight,
faMagicWandSparkles,
faPlus,
faUpRightAndDownLeftFromCenter,
} from "@fortawesome/pro-solid-svg-icons";
import { Button, ButtonIconSelect, Tooltip } from "~/components";
import { ButtonIconSelect } from "~/components";
import { EngineContext } from "../../contexts/EngineContext";
import { useContext } from "react";
import { assetModalVisibleDuringDrag, assetModalVisible } from "../../signals";
import { AssetModal } from "../AssetMenu/AssetModal";
import { selectedMode } from "../../signals/selectedMode";
import { useSignals } from "@preact/signals-react/runtime";
export const Controls3D = () => {
useSignals();
const editorEngine = useContext(EngineContext);
const handleModeChange = (value: string) => {
selectedMode.value = value;
switch (value) {
case "move":
handleMoveArrows();
break;
case "rotate":
handleRotateArrows();
break;
case "scale":
handleZoomArrows();
break;
default:
console.log("Unknown option");
}
};
const handleMoveArrows = () => {
if (!editorEngine) {
return;
@@ -53,77 +29,40 @@ export const Controls3D = () => {
editorEngine.change_mode("scale");
};
const modes = [
{
value: "move",
icon: faArrowsUpDownLeftRight,
text: "Move",
tooltip: "Move (T)",
},
{
value: "rotate",
icon: faArrowsRotate,
text: "Rotate",
tooltip: "Rotate (R)",
},
{
value: "scale",
icon: faUpRightAndDownLeftFromCenter,
text: "Scale",
tooltip: "Scale (G)",
},
];
const handleOpenModal = () => {
assetModalVisibleDuringDrag.value = true;
assetModalVisible.value = true;
const handleModeChange = (value: string) => {
switch (value) {
case "move":
handleMoveArrows();
break;
case "rotate":
handleRotateArrows();
break;
case "scale":
handleZoomArrows();
break;
default:
console.log("Unknown option");
}
};
const modes = [
{ value: "move", icon: faArrowsUpDownLeftRight, text: "Move (T)" },
{ value: "rotate", icon: faArrowsRotate, text: "Rotate (R)" },
{ value: "scale", icon: faUpRightAndDownLeftFromCenter, text: "Scale (G)" },
];
return (
<div>
<div className="flex justify-center">
<div className="glass rounded-b-xl p-1.5 pr-2 text-white shadow-md">
<div className="flex items-center justify-center gap-2.5">
<div className="flex items-center gap-1">
<Tooltip
content="Add 3D asset to scene (B)"
position="bottom"
delay={100}
closeOnClick
>
<Button
icon={faPlus}
className="h-9 w-9 rounded-[10px] text-lg"
onClick={handleOpenModal}
/>
</Tooltip>
<Tooltip
content="Create 3D model from image"
position="bottom"
delay={300}
closeOnClick
>
<Button
icon={faMagicWandSparkles}
className="text-md h-9 w-9 rounded-[10px] bg-white/10"
variant="secondary"
disabled={true}
onClick={handleOpenModal}
/>
</Tooltip>
</div>
<span className="opacity-20">|</span>
<div className="rounded-b-lg border border-[#363636] bg-ui-controls p-1.5 text-white shadow-md">
<div className="flex items-center justify-center gap-3">
<ButtonIconSelect
options={modes}
onOptionChange={handleModeChange}
selectedOption={selectedMode.value}
/>
</div>
</div>
</div>
<AssetModal />
</div>
);
};
@@ -223,7 +223,7 @@ export const ControlsTopButtons = () => {
{
disabled:
!scene.value.isModified ||
scene.value.ownerToken ===
scene.value.ownerToken !==
authentication.userInfo.value?.user_token,
// save scene should be disabled if there are no changes
label: "Save scene",
@@ -1,6 +1,6 @@
import { Label } from "~/components";
export const ShortcutsGroup = (props: {
const ShortcutsGroup = (props: {
label: string;
children?: React.ReactNode;
}) => (
@@ -15,10 +15,7 @@ export const ShortcutsGroup = (props: {
</div>
);
export const Shortcut = (props: {
label: string;
children: React.ReactNode;
}) => (
const Shortcut = (props: { label: string; children: React.ReactNode }) => (
<div className="flex items-center gap-2 ps-4">
<span className="text-md w-[280px] font-medium opacity-80">
{props.label}
@@ -27,19 +24,19 @@ export const Shortcut = (props: {
</div>
);
export const Key = (props: { button: string }) => (
const Key = (props: { button: string }) => (
<div className="flex h-[30px] w-auto min-w-[30px] items-center justify-center rounded-md border-b-2 border-[#9E9EA6] bg-white px-2 text-center text-sm font-bold text-ui-background">
{props.button}
</div>
);
export const KeyGroup = (props: { children: React.ReactNode }) => (
const KeyGroup = (props: { children: React.ReactNode }) => (
<div className="flex gap-1">{props.children}</div>
);
export const Mouse = (props: {
const Mouse = (props: {
button: "left" | "middle" | "right";
label?: string;
label: string;
}) => (
<div className="flex items-center gap-2.5">
<img
@@ -51,7 +48,7 @@ export const Mouse = (props: {
</div>
);
export const Plus = () => <div className="text-xl font-medium">+</div>;
const Plus = () => <div className="text-xl font-medium">+</div>;
export const Help = () => {
return (
@@ -69,7 +66,7 @@ export const Help = () => {
</Shortcut> */}
<Shortcut label="Orbit View">
<KeyGroup>
<Mouse button="left" label="(Hold)" />
<Mouse button="middle" label="(Hold)" />
</KeyGroup>
</Shortcut>
<Shortcut label="Forward Backward">
@@ -97,23 +94,6 @@ export const Help = () => {
</Shortcut>
</ShortcutsGroup>
<ShortcutsGroup label="Interaction">
<Shortcut label="Select Object">
<Mouse button="left" label="(Click)" />
</Shortcut>
{/* <Shortcut label="Clear Selection">
<Key button="Esc" />
</Shortcut> */}
{/* <Shortcut label="Focus Selection">
<Key button="F" />
</Shortcut> */}
<Shortcut label="Delete Selection">
<Key button="Del" />
</Shortcut>
</ShortcutsGroup>
</div>
<div className="flex flex-col gap-8">
<ShortcutsGroup label="Shortcuts">
<Shortcut label="Transform">
<Key button="T" />
@@ -139,8 +119,68 @@ export const Help = () => {
<Plus></Plus>
<Key button="V" />
</Shortcut>
<Shortcut label="Open 3D Asset Modal">
<Key button="B" />
</ShortcutsGroup>
<ShortcutsGroup label="Interaction">
<Shortcut label="Select Object">
<Mouse button="left" label="(Click)" />
</Shortcut>
{/* <Shortcut label="Clear Selection">
<Key button="Esc" />
</Shortcut> */}
{/* <Shortcut label="Focus Selection">
<Key button="F" />
</Shortcut> */}
<Shortcut label="Delete Selection">
<Key button="Del" />
</Shortcut>
</ShortcutsGroup>
</div>
<div className="flex flex-col gap-8">
<ShortcutsGroup label="Timeline">
<Shortcut label="Select Clip">
<Mouse button="left" label="(Click)" />
</Shortcut>
<Shortcut label="Move Clip">
<Mouse button="left" label="(Drag)" />
</Shortcut>
<Shortcut label="Delete Selection">
<Key button="Del" />
</Shortcut>
<Shortcut label="Scroll">
<Mouse button="middle" label="(Scroll)" />
</Shortcut>
<Shortcut label="Side Scroll">
<Key button="Shift" />
<Plus />
<Mouse button="middle" label="(Scroll)" />
</Shortcut>
{/* <Shortcut label="Zoom">
<Key button="Ctrl/Cmd" />
<Plus />
<Mouse button="middle" label="(Scroll)" />
</Shortcut> */}
<Shortcut label="Add Keyframe to Selected Object">
<Key button="K" />
</Shortcut>
<Shortcut label="Select Keyframe">
<Mouse button="left" label="(Click)" />
</Shortcut>
<Shortcut label="Delete Selected Keyframe">
<Key button="Del" />
</Shortcut>
</ShortcutsGroup>
<ShortcutsGroup label="Side Panel">
<Shortcut label="Add Character">
<Mouse button="left" label="(Drag onto scene)" />
</Shortcut>
<Shortcut label="Add Animation">
<Mouse button="left" label="(Drag onto timeline)" />
</Shortcut>
<Shortcut label="Add Object">
<Mouse button="left" label="(Drag onto scene)" />
</Shortcut>
</ShortcutsGroup>
</div>
@@ -1,64 +0,0 @@
import { useState, useEffect } from "react";
import { Transition } from "@headlessui/react";
import { Key, KeyGroup, Mouse, Plus } from "./ControlsTopButtons/Help/Help";
export const OnboardingHelper = () => {
const [actionPerformed, setActionPerformed] = useState(false);
const [keyPressed, setKeyPressed] = useState(false);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (["W", "A", "S", "D"].includes(event.key.toUpperCase())) {
setTimeout(() => setKeyPressed(true), 200);
}
};
const handleAction = () => {
setTimeout(() => setActionPerformed(true), 200);
};
window.addEventListener("keydown", handleKeyDown);
window.addEventListener("mousedown", handleAction);
window.addEventListener("dragstart", handleAction);
return () => {
window.removeEventListener("keydown", handleKeyDown);
window.removeEventListener("mousedown", handleAction);
window.removeEventListener("dragstart", handleAction);
};
}, []);
return (
<Transition
show={!(actionPerformed && keyPressed)}
enter="transition-opacity duration-500"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition-opacity duration-500"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="glass pointer-events-none absolute left-1/2 top-1/2 z-10 flex -translate-x-1/2 -translate-y-1/2 transform items-center justify-center gap-2 rounded-2xl border-2 border-brand-primary !bg-brand-primary/10 px-5 py-4">
<div className="flex items-center">
<p className="pr-3 text-lg font-semibold">Hold</p>
<Mouse button="left" />
<p className="text-lg font-semibold">and drag</p>
</div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-2">
<Plus />
<KeyGroup>
<Key button="W" />
<Key button="A" />
<Key button="S" />
<Key button="D" />
</KeyGroup>
</div>
</div>
<p className="text-lg font-semibold">to move around the scene</p>
</div>
</Transition>
);
};
export default OnboardingHelper;
@@ -20,7 +20,6 @@ import {
faRectangleVertical,
faSquare,
faRectangle,
faTableCellsLarge,
} from "@fortawesome/pro-regular-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -32,21 +31,12 @@ import {
disableHotkeyInput,
DomLevels,
enableHotkeyInput,
gridVisibility,
setGridVisibility,
} from "~/pages/PageEnigma/signals";
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 type { LibraryItem } from "../LibraryModal/LibraryModal";
import {
cameras,
selectedCameraId,
addCamera,
deleteCamera,
updateCamera,
} from "~/pages/PageEnigma/signals/camera";
interface ReferenceImage {
id: string;
@@ -55,14 +45,18 @@ interface ReferenceImage {
mediaToken: string;
}
import { EngineApi } from "~/Classes/ApiManager/EngineApi";
import { scene } from "~/signals/scene";
import { UploaderStates } from "~/enums/UploaderStates";
import { CameraSettingsModal } from "../CameraSettingsModal";
interface ExtendedPopoverItem extends PopoverItem {
id: string;
focalLength: number;
}
export const PromptBox = () => {
useSignals();
const editorEngine = useContext(EngineContext);
const fileInputRef = useRef<HTMLInputElement>(null);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const [prompt, setPrompt] = useState("");
const [isEnqueueing, setisEnqueueing] = useState(false);
const [useSystemPrompt, setUseSystemPrompt] = useState(true);
@@ -86,14 +80,37 @@ export const PromptBox = () => {
selected: false,
icon: <FontAwesomeIcon icon={faSquare} className="h-4 w-4" />,
},
{
label: "16:9",
selected: false,
icon: <FontAwesomeIcon icon={faRectangleWide} className="h-4 w-4" />,
},
]);
const [cameraList, setCameraList] = useState<ExtendedPopoverItem[]>([
{
id: "main",
label: "Main View",
selected: true,
icon: <FontAwesomeIcon icon={faCamera} className="h-4 w-4" />,
focalLength: 35,
},
{
id: "cam2",
label: "Camera 2",
selected: false,
icon: <FontAwesomeIcon icon={faCamera} className="h-4 w-4" />,
focalLength: 35,
},
]);
const [selectedCameraId, setSelectedCameraId] = useState("main");
const fileInputRef = useRef<HTMLInputElement>(null);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const [isCameraSettingsOpen, setIsCameraSettingsOpen] = useState(false);
const [isLibraryModalOpen, setIsLibraryModalOpen] = useState(false);
const [selectedLibraryImages, setSelectedLibraryImages] = useState<string[]>(
[],
);
const [activeLibraryTab, setActiveLibraryTab] = useState("my-media");
const [isGridVisible, setIsGridVisible] = useState(true);
useEffect(() => {
if (textareaRef.current) {
@@ -121,47 +138,46 @@ export const PromptBox = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [cameraAspectRatio.value]);
useEffect(() => {
setGridVisibility(isGridVisible);
}, [isGridVisible]);
const handleCameraSelect = (selectedItem: PopoverItem) => {
const selectedCamera = cameras.value.find(
setCameraList((prev) =>
prev.map((item) => ({
...item,
selected: item.label === selectedItem.label,
})),
);
const selectedCamera = cameraList.find(
(cam) => cam.label === selectedItem.label,
);
if (selectedCamera) {
selectedCameraId.value = selectedCamera.id;
// Force update camera properties
updateCamera(selectedCamera.id, {
focalLength: selectedCamera.focalLength,
position: selectedCamera.position,
rotation: selectedCamera.rotation,
});
setSelectedCameraId(selectedCamera.id);
}
};
const handleAddCamera = () => {
const newIndex = cameras.value.length + 1;
const newIndex = cameraList.length + 1;
const newId = `cam${newIndex}`;
addCamera({
id: newId,
label: `Camera ${newIndex}`,
focalLength: 35,
fov: 70,
position: { x: 0, y: 2.5, z: -5 },
rotation: { x: 0, y: 0, z: 0 },
});
setCameraList((prev) => [
...prev,
{
id: newId,
label: `Camera ${newIndex}`,
selected: false,
icon: <FontAwesomeIcon icon={faCamera} className="h-4 w-4" />,
focalLength: 35,
},
]);
};
const handleCameraNameChange = (id: string, newName: string) => {
updateCamera(id, { label: newName });
setCameraList((prev) =>
prev.map((cam) => (cam.id === id ? { ...cam, label: newName } : cam)),
);
};
const handleCameraFocalLengthChange = (id: string, value: number) => {
const camera = cameras.value.find((cam) => cam.id === id);
if (camera) {
updateCamera(id, { focalLength: value });
}
setCameraList((prev) =>
prev.map((cam) => (cam.id === id ? { ...cam, focalLength: value } : cam)),
);
};
const handleAspectRatioSelect = (selectedItem: PopoverItem) => {
@@ -362,6 +378,8 @@ export const PromptBox = () => {
// Get the current aspect ratio icon based on the cameraAspectRatio signal
const getCurrentAspectRatioIcon = () => {
switch (cameraAspectRatio.value) {
case CameraAspectRatio.HORIZONTAL_16_9:
return faRectangleWide;
case CameraAspectRatio.HORIZONTAL_3_2:
return faRectangle;
case CameraAspectRatio.VERTICAL_2_3:
@@ -394,6 +412,31 @@ export const PromptBox = () => {
}
};
const handleDeleteCamera = (id: string) => {
// Don't allow deleting the main camera or if it's the last camera
if (id === "main" || cameraList.length <= 1) return;
// Update camera list
setCameraList((prev) => prev.filter((cam) => cam.id !== id));
// If we're deleting the selected camera, select the first remaining camera
if (id === selectedCameraId) {
const remainingCameras = cameraList.filter((cam) => cam.id !== id);
if (remainingCameras.length > 0) {
setSelectedCameraId(remainingCameras[0].id);
// Update selection state
setCameraList((prev) =>
prev
.filter((cam) => cam.id !== id)
.map((cam, idx) => ({
...cam,
selected: idx === 0,
})),
);
}
}
};
return (
<>
<div className="absolute bottom-4 left-1/2 flex -translate-x-1/2 flex-col gap-3">
@@ -526,15 +569,7 @@ export const PromptBox = () => {
closeOnClick={true}
>
<PopoverMenu
items={cameras.value.map((cam) => ({
id: cam.id,
label: cam.label,
selected: cam.id === selectedCameraId.value,
icon: (
<FontAwesomeIcon icon={faCamera} className="h-4 w-4" />
),
focalLength: cam.focalLength,
}))}
items={cameraList}
onSelect={handleCameraSelect}
onAdd={handleAddCamera}
triggerIcon={
@@ -565,19 +600,6 @@ export const PromptBox = () => {
onClick={() => setUseSystemPrompt(!useSystemPrompt)}
/>
</Tooltip>
<Tooltip
content={gridVisibility.value ? "Grid: ON" : "Grid: OFF"}
position="top"
className="z-50"
delay={200}
>
<ToggleButton
isActive={gridVisibility.value}
icon={faTableCellsLarge}
activeIcon={faTableCellsLarge}
onClick={() => setGridVisibility(!gridVisibility.value)}
/>
</Tooltip>
</div>
<div className="flex items-center gap-2">
<Tooltip
@@ -624,19 +646,13 @@ export const PromptBox = () => {
<CameraSettingsModal
isOpen={isCameraSettingsOpen}
onClose={() => setIsCameraSettingsOpen(false)}
cameras={cameras.value.map((cam) => ({
id: cam.id,
label: cam.label,
selected: cam.id === selectedCameraId.value,
icon: <FontAwesomeIcon icon={faCamera} className="h-4 w-4" />,
focalLength: cam.focalLength,
}))}
cameras={cameraList}
onCameraNameChange={handleCameraNameChange}
onCameraFocalLengthChange={handleCameraFocalLengthChange}
onAddCamera={handleAddCamera}
selectedCameraId={selectedCameraId.value}
onSelectCamera={(id: string) => (selectedCameraId.value = id)}
onDeleteCamera={deleteCamera}
selectedCameraId={selectedCameraId}
onSelectCamera={setSelectedCameraId}
onDeleteCamera={handleDeleteCamera}
/>
</div>
<LibraryModal
@@ -1,6 +1,8 @@
import { useSignals } from "@preact/signals-react/runtime";
import { twMerge } from "tailwind-merge";
import { cameraAspectRatio } from "~/pages/PageEnigma/signals";
import { EditorStates } from "~/pages/PageEnigma/enums";
import { editorState, cameraAspectRatio } from "~/pages/PageEnigma/signals";
import {
calcMatteWidth,
@@ -49,7 +51,7 @@ export const Letterbox = ({
<div
id="letterbox"
className={twMerge(
"user-select-none pointer-events-none absolute left-0 top-0 flex h-full w-full justify-between",
"absolute left-0 top-0 flex h-full w-full justify-between pointer-events-none",
matteOri === MatteOrientation.TOP_BOTTOM ? "flex-col" : null,
)}
>
@@ -71,14 +73,14 @@ const Matte = ({
if (matteOri === MatteOrientation.TOP_BOTTOM) {
return (
<div
className="pointer-events-none h-20 w-full bg-black/30 brightness-75"
className="h-20 w-full bg-black/30 brightness-75"
style={{ height: `${height}px` }}
/>
);
}
return (
<div
className="pointer-events-none h-full w-80 bg-black/30 brightness-75"
className="h-full w-80 bg-black/30 brightness-75"
style={{ width: `${width}px` }}
/>
);
@@ -1,5 +1,4 @@
import { signal } from "@preact/signals-react";
export const assetModalVisibleDuringDrag = signal(true);
export const reopenAfterDragSignal = signal(false);
export const assetModalVisible = signal(false);
export const reopenAfterDragSignal = signal(true);
@@ -1,59 +0,0 @@
import { signal } from "@preact/signals-react";
export interface Camera {
id: string;
label: string;
focalLength: number;
fov: number;
position: { x: number; y: number; z: number };
rotation: { x: number; y: number; z: number };
}
export const cameras = signal<Camera[]>([
{
id: "main",
label: "Main View",
focalLength: 17,
fov: 70,
position: { x: 2.5, y: 2.5, z: -2.5 },
rotation: { x: 0, y: 0, z: 0 },
},
{
id: "cam2",
label: "Camera 2",
focalLength: 35,
fov: 70,
position: { x: -2.5, y: 2.5, z: 2.5 },
rotation: { x: 0, y: 0, z: 0 },
},
]);
export const selectedCameraId = signal<string>("main");
export const addCamera = (camera: Camera) => {
cameras.value = [...cameras.value, camera];
};
export const updateCamera = (id: string, updates: Partial<Camera>) => {
cameras.value = cameras.value.map((cam) =>
cam.id === id ? { ...cam, ...updates } : cam,
);
};
export const deleteCamera = (id: string) => {
if (id === "main") return; // Prevent deleting main camera
cameras.value = cameras.value.filter((cam) => cam.id !== id);
if (selectedCameraId.value === id) {
selectedCameraId.value = "main";
}
};
export interface FocalLengthDragging {
isDragging: boolean;
focalLength: number;
}
export const focalLengthDragging = signal<FocalLengthDragging>({
isDragging: false,
focalLength: 35,
});
@@ -3,13 +3,6 @@ import { MediaItem } from "../../models";
import { AssetType } from "~/enums";
export const demoSkyboxItems = signal<MediaItem[]>([
{
version: 1,
media_id: "SKY::m_1",
type: AssetType.SKYBOX,
name: "Empty Skybox",
thumbnail: "/resources/skybox/empty.jpg",
},
{
version: 1,
media_id: "SKY::Default",
@@ -26,17 +19,18 @@ export const demoSkyboxItems = signal<MediaItem[]>([
},
{
version: 1,
media_id: "SKY::m_3",
media_id: "SKY::m_1",
type: AssetType.SKYBOX,
name: "Gray Skybox",
thumbnail:
"/resources/skybox/gray/Sky_AllSky_Overcast4_Low_Cam_2_LeftX.png",
"/resources/skybox/gray/Sky_AllSky_Overcast4_Low_Cam_0_FrontZ.png",
},
{
version: 1,
media_id: "SKY::m_2",
type: AssetType.SKYBOX,
name: "Black Skybox",
thumbnail: "/resources/skybox/black.jpg",
thumbnail:
"/resources/skybox/black.jpg",
},
]);
@@ -11,9 +11,3 @@ export const setCameraAspectRatio = (newAspectRatio: CameraAspectRatio) => {
cameraAspectRatio.value = newAspectRatio;
};
export const previewSrc = signal("");
export const gridVisibility = signal<boolean>(true);
export const setGridVisibility = (isVisible: boolean) => {
gridVisibility.value = isVisible;
};
@@ -1,3 +0,0 @@
import { signal } from "@preact/signals-react";
export const selectedMode = signal("move");