mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-21 01:12:40 -04:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41c24e0655 | ||
|
|
126f1b46d6 |
@@ -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"
|
||||
|
||||
+46
-4
@@ -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,
|
||||
});
|
||||
Reference in New Issue
Block a user