Compare commits

...
Author SHA1 Message Date
bflatastic 41c24e0655 focal length value to show on top when dragging slider 2025-04-11 02:02:09 +07:00
bflatastic 126f1b46d6 working focal length changing 2025-04-11 01:48:07 +07:00
7 changed files with 332 additions and 87 deletions
@@ -13,7 +13,7 @@ import {
} from "@fortawesome/pro-solid-svg-icons";
import { Tooltip } from "~/components";
const DialogBackdrop = () => {
const DialogBackdrop = ({ className }: { className?: string }) => {
useEffect(() => {
disableHotkeyInput(DomLevels.DIALOGUE);
return () => {
@@ -31,7 +31,7 @@ const DialogBackdrop = () => {
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-black/60" />
<div className={twMerge("fixed inset-0 bg-black/60", className)} />
</Transition.Child>
);
};
@@ -43,6 +43,7 @@ export const TransitionDialogue = ({
onTitleIconClick,
onClose,
className,
backdropClassName,
width,
children,
childPadding = true,
@@ -56,6 +57,7 @@ export const TransitionDialogue = ({
titleIconClassName?: string;
onClose: () => void;
className?: string;
backdropClassName?: string;
width?: number;
children: ReactNode;
childPadding?: boolean;
@@ -64,7 +66,7 @@ export const TransitionDialogue = ({
return (
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-[70]" onClose={onClose}>
<DialogBackdrop />
<DialogBackdrop className={backdropClassName} />
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child
@@ -52,6 +52,7 @@ 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;
@@ -192,6 +193,8 @@ class Editor {
processingHasFailed: boolean;
stats: Stats;
cameras: Map<string, THREE.PerspectiveCamera> = new Map();
constructor() {
this.processingHasFailed = false;
console.log(
@@ -323,8 +326,134 @@ class Editor {
this.engineFrameBuffers.frameWorkerManager.type = imageFormat;
}
// New Rendering Pipeline Engine Work
// 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();
}
// 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: {
@@ -1159,6 +1288,21 @@ 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,5 +1,6 @@
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;
@@ -228,6 +229,22 @@ 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() {
@@ -24,6 +24,7 @@ import { Outliner } from "./comps/Outliner";
import { CameraAspectRatio } from "./enums";
import { PromptBox } from "./comps/PromptBox";
import { OnboardingHelper } from "./comps/OnboardingHelper";
import { focalLengthDragging } from "./signals/camera";
export const PageEditor = () => {
useSignals();
@@ -101,6 +102,15 @@ 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"
@@ -1,9 +1,15 @@
import { TransitionDialogue } from "~/components/reusable/TransitionDialogue";
import { faPlus, faTrashAlt, faXmark } from "@fortawesome/pro-solid-svg-icons";
import { faPlus, faTrashAlt } 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;
@@ -34,12 +40,45 @@ 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="h-[500px] max-w-3xl"
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",
)}
childPadding={false}
>
<div className="grid h-full grid-cols-12 gap-3">
@@ -127,14 +166,17 @@ export const CameraSettingsModal = ({
<Label htmlFor="focal-length" className="text-sm opacity-70">
Focal Length
</Label>
<div className="mt-1 flex items-center gap-4">
<div
className="mt-1 flex items-center gap-4"
onPointerDown={handlePointerDown}
>
<SliderV2
min={10}
max={200}
value={selectedCamera?.focalLength || 35}
onChange={(value) =>
selectedCamera &&
onCameraFocalLengthChange(selectedCamera.id, value)
handleFocalLengthChange(selectedCamera.id, value)
}
step={1}
suffix="mm"
@@ -40,6 +40,13 @@ 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;
@@ -51,14 +58,11 @@ import { EngineApi } from "~/Classes/ApiManager/EngineApi";
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);
@@ -83,25 +87,6 @@ export const PromptBox = () => {
icon: <FontAwesomeIcon icon={faSquare} 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[]>(
@@ -141,45 +126,42 @@ export const PromptBox = () => {
}, [isGridVisible]);
const handleCameraSelect = (selectedItem: PopoverItem) => {
setCameraList((prev) =>
prev.map((item) => ({
...item,
selected: item.label === selectedItem.label,
})),
);
const selectedCamera = cameraList.find(
const selectedCamera = cameras.value.find(
(cam) => cam.label === selectedItem.label,
);
if (selectedCamera) {
setSelectedCameraId(selectedCamera.id);
selectedCameraId.value = selectedCamera.id;
// Force update camera properties
updateCamera(selectedCamera.id, {
focalLength: selectedCamera.focalLength,
position: selectedCamera.position,
rotation: selectedCamera.rotation,
});
}
};
const handleAddCamera = () => {
const newIndex = cameraList.length + 1;
const newIndex = cameras.value.length + 1;
const newId = `cam${newIndex}`;
setCameraList((prev) => [
...prev,
{
id: newId,
label: `Camera ${newIndex}`,
selected: false,
icon: <FontAwesomeIcon icon={faCamera} className="h-4 w-4" />,
focalLength: 35,
},
]);
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 },
});
};
const handleCameraNameChange = (id: string, newName: string) => {
setCameraList((prev) =>
prev.map((cam) => (cam.id === id ? { ...cam, label: newName } : cam)),
);
updateCamera(id, { label: newName });
};
const handleCameraFocalLengthChange = (id: string, value: number) => {
setCameraList((prev) =>
prev.map((cam) => (cam.id === id ? { ...cam, focalLength: value } : cam)),
);
const camera = cameras.value.find((cam) => cam.id === id);
if (camera) {
updateCamera(id, { focalLength: value });
}
};
const handleAspectRatioSelect = (selectedItem: PopoverItem) => {
@@ -412,31 +394,6 @@ 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">
@@ -569,7 +526,15 @@ export const PromptBox = () => {
closeOnClick={true}
>
<PopoverMenu
items={cameraList}
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,
}))}
onSelect={handleCameraSelect}
onAdd={handleAddCamera}
triggerIcon={
@@ -659,13 +624,19 @@ export const PromptBox = () => {
<CameraSettingsModal
isOpen={isCameraSettingsOpen}
onClose={() => setIsCameraSettingsOpen(false)}
cameras={cameraList}
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,
}))}
onCameraNameChange={handleCameraNameChange}
onCameraFocalLengthChange={handleCameraFocalLengthChange}
onAddCamera={handleAddCamera}
selectedCameraId={selectedCameraId}
onSelectCamera={setSelectedCameraId}
onDeleteCamera={handleDeleteCamera}
selectedCameraId={selectedCameraId.value}
onSelectCamera={(id: string) => (selectedCameraId.value = id)}
onDeleteCamera={deleteCamera}
/>
</div>
<LibraryModal
@@ -0,0 +1,59 @@
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,
});