mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-04 21:38:07 -05:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9eb1b8ebf | ||
|
|
7986991523 | ||
|
|
f4799a6244 | ||
|
|
66d5095f69 | ||
|
|
534b362cc0 | ||
|
|
94a5178bec | ||
|
|
26a6a65c92 | ||
|
|
032ebca3fb | ||
|
|
080f8da8a2 | ||
|
|
7230618ed5 |
+1
-4
@@ -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(
|
||||
|
||||
+4
-46
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+1
-1
@@ -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");
|
||||
Reference in New Issue
Block a user