mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-05 17:58:55 +00:00
Update PageEditor to use new 3D PromptBox
This commit is contained in:
1 parent
38c859b580
commit
9054e963d2
1 file changed
+266
-71
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect } from "react";
|
||||
import React, { useContext, useEffect } from "react";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { LoadingDots, TopBar } from "~/components";
|
||||
@@ -16,12 +16,17 @@ import {
|
||||
editorLoader,
|
||||
cameraAspectRatio,
|
||||
outlinerIsShowing,
|
||||
disableHotkeyInput,
|
||||
enableHotkeyInput,
|
||||
gridVisibility,
|
||||
setGridVisibility,
|
||||
} from "~/pages/PageEnigma/signals";
|
||||
import { EditorCanvas } from "./comps/EngineCanvases";
|
||||
import { SceneContainer } from "./comps/SceneContainer";
|
||||
import { Outliner } from "./comps/Outliner";
|
||||
import { CameraAspectRatio } from "./enums";
|
||||
import { PromptBox } from "./comps/PromptBox";
|
||||
import { PromptBox3D } from "@storyteller/ui-promptbox";
|
||||
import { PopoverItem } from "@storyteller/ui-popover";
|
||||
import { OnboardingHelper } from "./comps/OnboardingHelper";
|
||||
import { IsDesktopApp } from "@storyteller/tauri-utils";
|
||||
import { FocalLengthDisplay } from "./comps/FocalLengthDisplay/FocalLengthDisplay";
|
||||
@@ -29,10 +34,17 @@ import { DemoModal } from "@storyteller/ui-demo-modal";
|
||||
import { appTabId, setAppTabId } from "~/signals/appTab";
|
||||
import { KonvaCanvasContainer } from "../Page2d/KonvaCanvasContainer";
|
||||
import { KonvaRootComponent } from "../Page2d/KonvaRootComponent";
|
||||
import { addCamera, cameras, deleteCamera, focalLengthDragging, selectedCameraId, updateCamera } from "./signals/camera";
|
||||
import { isPromptBoxFocused } from "./signals/promptBox";
|
||||
import { uploadImage } from "~/components/reusable/UploadModalMedia/uploadImage";
|
||||
import { EngineContext } from "./contexts/EngineContext";
|
||||
import Queue, { QueueNames } from "./Queue";
|
||||
import { toEngineActions } from "./Queue/toEngineActions";
|
||||
import { UnionedDataTypes } from "./Queue/Queue";
|
||||
|
||||
export const PageEditor = () => {
|
||||
useSignals();
|
||||
|
||||
|
||||
const handleOverlayClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
event.stopPropagation();
|
||||
};
|
||||
@@ -75,93 +87,276 @@ export const PageEditor = () => {
|
||||
return scaleHeight;
|
||||
};
|
||||
|
||||
// These are callbacks required by promptbox
|
||||
const editorEngine = useContext(EngineContext);
|
||||
const handleCameraSelect = (selectedItem: PopoverItem) => {
|
||||
const selectedCamera = cameras.value.find(
|
||||
(cam) => cam.label === selectedItem.label,
|
||||
);
|
||||
if (selectedCamera && editorEngine) {
|
||||
selectedCameraId.value = selectedCamera.id;
|
||||
|
||||
// Show focal length display temporarily
|
||||
// TODO: Rename dragging to visible - BFlat
|
||||
focalLengthDragging.value = {
|
||||
isDragging: true,
|
||||
focalLength: selectedCamera.focalLength,
|
||||
};
|
||||
setTimeout(() => {
|
||||
focalLengthDragging.value = {
|
||||
isDragging: false,
|
||||
focalLength: selectedCamera.focalLength,
|
||||
};
|
||||
}, 1500);
|
||||
|
||||
// Update the main camera to match the selected camera's properties
|
||||
if (editorEngine.camera) {
|
||||
// First update position and lookAt
|
||||
editorEngine.camera.position.set(
|
||||
selectedCamera.position.x,
|
||||
selectedCamera.position.y,
|
||||
selectedCamera.position.z,
|
||||
);
|
||||
editorEngine.camera.lookAt(
|
||||
selectedCamera.lookAt.x,
|
||||
selectedCamera.lookAt.y,
|
||||
selectedCamera.lookAt.z,
|
||||
);
|
||||
|
||||
// Update FOV
|
||||
editorEngine.camera.fov = editorEngine.focalLengthToFov(
|
||||
selectedCamera.focalLength,
|
||||
);
|
||||
editorEngine.camera.updateProjectionMatrix();
|
||||
|
||||
// Reset and update camera controls
|
||||
if (editorEngine.cameraViewControls) {
|
||||
editorEngine.cameraViewControls.reset();
|
||||
editorEngine.cameraViewControls.update(0);
|
||||
}
|
||||
|
||||
// Force a render to update the view
|
||||
editorEngine.renderScene();
|
||||
|
||||
// Queue an update to ensure the engine processes the camera change
|
||||
// Queue.publish({
|
||||
// queueName: QueueNames.TO_ENGINE,
|
||||
// action: toEngineActions.CAMERA_CHANGED,
|
||||
// data: selectedCamera,
|
||||
// });
|
||||
}
|
||||
|
||||
// Force update camera properties in the state
|
||||
updateCamera(selectedCamera.id, {
|
||||
focalLength: selectedCamera.focalLength,
|
||||
position: selectedCamera.position,
|
||||
rotation: selectedCamera.rotation,
|
||||
lookAt: selectedCamera.lookAt,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddCamera = () => {
|
||||
// Check if we've reached the maximum number of cameras
|
||||
if (cameras.value.length >= 6) {
|
||||
console.warn("Maximum number of cameras (6) reached");
|
||||
return;
|
||||
}
|
||||
|
||||
const newIndex = cameras.value.length + 1;
|
||||
const newId = `cam${newIndex}`;
|
||||
|
||||
// This is for generating random orbital position for the new camera using spherical coordinates
|
||||
const radius = Math.random() * 5 + 7; // Distance from center: 7 to 12 units
|
||||
const theta = Math.random() * Math.PI * 2; // Azimuthal angle: 0 to 2π
|
||||
const phi = Math.PI / 3 + (Math.random() * Math.PI) / 6; // Polar angle: π/3 to π/2 (60° to 90° from horizontal)
|
||||
|
||||
// Convert spherical coordinates to Cartesian coordinates
|
||||
const randomX = radius * Math.sin(phi) * Math.cos(theta);
|
||||
const randomY = Math.abs(radius * Math.cos(phi)) + 2; // Ensure Y is positive and at least 2 units up
|
||||
const randomZ = radius * Math.sin(phi) * Math.sin(theta);
|
||||
|
||||
addCamera({
|
||||
id: newId,
|
||||
label: `Camera ${newIndex}`,
|
||||
focalLength: 24,
|
||||
position: {
|
||||
x: randomX,
|
||||
y: randomY,
|
||||
z: randomZ,
|
||||
},
|
||||
rotation: { x: 0, y: 0, z: 0 },
|
||||
lookAt: { x: 0, y: 0, z: 0 },
|
||||
});
|
||||
|
||||
// Switch to the newly created camera
|
||||
selectedCameraId.value = newId;
|
||||
|
||||
// Update the engine camera to match the new camera's properties
|
||||
if (editorEngine && editorEngine.camera) {
|
||||
editorEngine.camera.position.set(randomX, randomY, randomZ);
|
||||
editorEngine.camera.lookAt(0, 0, 0);
|
||||
editorEngine.camera.fov = editorEngine.focalLengthToFov(24);
|
||||
editorEngine.camera.updateProjectionMatrix();
|
||||
|
||||
// Reset and update camera controls
|
||||
if (editorEngine.cameraViewControls) {
|
||||
editorEngine.cameraViewControls.reset();
|
||||
editorEngine.cameraViewControls.update(0);
|
||||
}
|
||||
|
||||
// Force a render to update the view
|
||||
editorEngine.renderScene();
|
||||
|
||||
// Queue.publish({
|
||||
// queueName: QueueNames.TO_ENGINE,
|
||||
// action: toEngineActions.CAMERA_CHANGED,
|
||||
// data: {
|
||||
// id: newId,
|
||||
// label: `Camera ${newIndex}`,
|
||||
// focalLength: 24,
|
||||
// position: { x: randomX, y: randomY, z: randomZ },
|
||||
// rotation: { x: 0, y: 0, z: 0 },
|
||||
// lookAt: { x: 0, y: 0, z: 0 },
|
||||
// },
|
||||
// });
|
||||
}
|
||||
};
|
||||
|
||||
const handleCameraNameChange = (id: string, newName: string) => {
|
||||
updateCamera(id, { label: newName });
|
||||
};
|
||||
|
||||
const handleCameraFocalLengthChange = (id: string, value: number) => {
|
||||
const camera = cameras.value.find((cam) => cam.id === id);
|
||||
if (camera) {
|
||||
updateCamera(id, { focalLength: value });
|
||||
}
|
||||
};
|
||||
|
||||
const onAspectRatioSelect = (newRatio: UnionedDataTypes) => {
|
||||
// Publish the change to the engine
|
||||
Queue.publish({
|
||||
queueName: QueueNames.TO_ENGINE,
|
||||
action: toEngineActions.CHANGE_CAMERA_ASPECT_RATIO,
|
||||
data: newRatio,
|
||||
});
|
||||
}
|
||||
|
||||
const ContentFor3D = () => {
|
||||
return <> <OnboardingHelper />
|
||||
<div
|
||||
className="relative flex w-screen"
|
||||
style={{ height: "calc(100vh - 68px)" }}
|
||||
>
|
||||
{/* Engine section/side panel */}
|
||||
<div
|
||||
id="engine-n-panels-wrapper"
|
||||
className="flex"
|
||||
style={{
|
||||
height,
|
||||
}}
|
||||
className="relative flex w-screen"
|
||||
style={{ height: "calc(100vh - 68px)" }}
|
||||
>
|
||||
<div className="relative w-full overflow-hidden bg-transparent">
|
||||
<SceneContainer>
|
||||
<EditorCanvas />
|
||||
<PreviewFrameImage />
|
||||
</SceneContainer>
|
||||
{/* Engine section/side panel */}
|
||||
<div
|
||||
id="engine-n-panels-wrapper"
|
||||
className="flex"
|
||||
style={{
|
||||
height,
|
||||
}}
|
||||
>
|
||||
<div className="relative w-full overflow-hidden bg-transparent">
|
||||
<SceneContainer>
|
||||
<EditorCanvas />
|
||||
<PreviewFrameImage />
|
||||
</SceneContainer>
|
||||
|
||||
{/* Focal Length Display */}
|
||||
<FocalLengthDisplay />
|
||||
{/* Focal Length Display */}
|
||||
<FocalLengthDisplay />
|
||||
|
||||
{/* Pose Mode Selector */}
|
||||
<PoseModeSelector />
|
||||
{/* Pose Mode Selector */}
|
||||
<PoseModeSelector />
|
||||
|
||||
{/* Top controls */}
|
||||
<div
|
||||
className="absolute left-0 top-0 w-full"
|
||||
onClick={handleOverlayClick}
|
||||
>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<ControlsTopButtons />
|
||||
<Controls3D />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom controls */}
|
||||
<div
|
||||
className="absolute bottom-0 left-0"
|
||||
style={{
|
||||
width: pageWidth.value,
|
||||
}}
|
||||
onClick={handleOverlayClick}
|
||||
>
|
||||
{/* Top controls */}
|
||||
<div
|
||||
className="absolute bottom-0 mb-4 ml-4 flex origin-bottom-left flex-col gap-2"
|
||||
style={{ transform: `scale(${getScale()})` }}
|
||||
className="absolute left-0 top-0 w-full"
|
||||
onClick={handleOverlayClick}
|
||||
>
|
||||
<Outliner />
|
||||
<PreviewEngineCamera />
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<ControlsTopButtons />
|
||||
<Controls3D />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ControlPanelSceneObject />
|
||||
{/* Bottom controls */}
|
||||
<div
|
||||
className="absolute bottom-0 left-0"
|
||||
style={{
|
||||
width: pageWidth.value,
|
||||
}}
|
||||
onClick={handleOverlayClick}
|
||||
>
|
||||
<div
|
||||
className="absolute bottom-0 mb-4 ml-4 flex origin-bottom-left flex-col gap-2"
|
||||
style={{ transform: `scale(${getScale()})` }}
|
||||
>
|
||||
<Outliner />
|
||||
<PreviewEngineCamera />
|
||||
</div>
|
||||
|
||||
<ControlPanelSceneObject />
|
||||
</div>
|
||||
|
||||
<PromptBox3D
|
||||
cameras={cameras}
|
||||
cameraAspectRatio={cameraAspectRatio}
|
||||
disableHotkeyInput={disableHotkeyInput}
|
||||
enableHotkeyInput={enableHotkeyInput}
|
||||
gridVisibility={gridVisibility}
|
||||
setGridVisibility={setGridVisibility}
|
||||
selectedCameraId={selectedCameraId}
|
||||
deleteCamera={deleteCamera}
|
||||
focalLengthDragging={focalLengthDragging}
|
||||
isPromptBoxFocused={isPromptBoxFocused}
|
||||
uploadImage={uploadImage}
|
||||
handleCameraSelect={handleCameraSelect}
|
||||
handleAddCamera={handleAddCamera}
|
||||
handleCameraNameChange={handleCameraNameChange}
|
||||
handleCameraFocalLengthChange={handleCameraFocalLengthChange}
|
||||
onAspectRatioSelect={onAspectRatioSelect}
|
||||
setEnginePrompt={(prompt) => {
|
||||
if (!editorEngine) {
|
||||
return;
|
||||
}
|
||||
|
||||
editorEngine!.positive_prompt = prompt
|
||||
}}
|
||||
snapshotCurrentFrame={
|
||||
editorEngine?.snapShotOfCurrentFrame
|
||||
}
|
||||
/>
|
||||
|
||||
<LoadingDots
|
||||
className="absolute left-0 top-0 z-50"
|
||||
isShowing={editorLoader.value.isShowing}
|
||||
type="bricks"
|
||||
message={editorLoader.value.message}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<PromptBox />
|
||||
|
||||
<LoadingDots
|
||||
className="absolute left-0 top-0 z-50"
|
||||
isShowing={editorLoader.value.isShowing}
|
||||
type="bricks"
|
||||
message={editorLoader.value.message}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DemoModal
|
||||
title="Welcome to ArtCraft 3D"
|
||||
subTitle="Your 3D editor for digital art and design"
|
||||
description="Set up your scene by adding objects and start bringing your ideas to life!"
|
||||
videoSrc="/resources/videos/artcraft-3d-demo.mp4"
|
||||
buttonText="Sign in to OpenAI to get started"
|
||||
buttonOnClick={async () => {
|
||||
if (IsDesktopApp()) {
|
||||
await invoke("open_sora_login_command");
|
||||
} else {
|
||||
console.error("This is not the Desktop app.");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<DemoModal
|
||||
title="Welcome to ArtCraft 3D"
|
||||
subTitle="Your 3D editor for digital art and design"
|
||||
description="Set up your scene by adding objects and start bringing your ideas to life!"
|
||||
videoSrc="/resources/videos/artcraft-3d-demo.mp4"
|
||||
buttonText="Sign in to OpenAI to get started"
|
||||
buttonOnClick={async () => {
|
||||
if (IsDesktopApp()) {
|
||||
await invoke("open_sora_login_command");
|
||||
} else {
|
||||
console.error("This is not the Desktop app.");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
const ContentFor2D = () => {
|
||||
return <KonvaCanvasContainer>
|
||||
<KonvaRootComponent className="w-full h-full"/>
|
||||
<KonvaRootComponent className="w-full h-full" />
|
||||
</KonvaCanvasContainer>;
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user