Merge pull request #154 from storytold/bug/load-characters

Bug/load characters
This commit is contained in:
ArEnSc authored and GitHub committed 2024-04-12 23:18:44 -04:00
commit 46c8bdc7ec
36 files changed
+926 -719

No files matched your search

@@ -10,15 +10,18 @@ import {
} from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { deleteInferenceJob, inferenceJobs } from "~/pages/PageEnigma/store/inferenceJobs";
import {
deleteInferenceJob,
inferenceJobs,
} from "~/pages/PageEnigma/store/inferenceJobs";
import { InferenceJob } from "~/pages/PageEnigma/models";
import { ButtonDialogue } from "../ButtonDialogue";
import { ButtonIcon, H4,H6,P } from "~/components";
import { ButtonIcon, H4, H6, P } from "~/components";
import { JobState } from "~/hooks/useInferenceJobManager/useInferenceJobManager";
const printJobType = (jobType : string)=>{
switch (jobType){
const printJobType = (jobType: string) => {
switch (jobType) {
case "generic":
return "Text to Speech";
default:
@@ -26,23 +29,23 @@ const printJobType = (jobType : string)=>{
}
};
const printJobStatus = (jobStatus : string)=>{
switch (jobStatus){
const printJobStatus = (jobStatus: string) => {
switch (jobStatus) {
case JobState.PENDING:
return "Adding to the queue.";
case JobState.STARTED:
return "Processing, please wait...";
case JobState.COMPLETE_SUCCESS:
return "Completed! See Audio Panel for the result. file."
return "Completed! See Audio Panel for the result. file.";
default:
return "Unknown Status";
}
};
const JobStatusIcon = ({jobStatus} : {jobStatus:string})=>{
switch (jobStatus){
case JobState.PENDING:{
return(
const JobStatusIcon = ({ jobStatus }: { jobStatus: string }) => {
switch (jobStatus) {
case JobState.PENDING: {
return (
<FontAwesomeIcon
className="col-span-1 animate-spin"
icon={faSpinner}
@@ -50,17 +53,17 @@ const JobStatusIcon = ({jobStatus} : {jobStatus:string})=>{
/>
);
}
case JobState.STARTED:{
return(
<FontAwesomeIcon
case JobState.STARTED: {
return (
<FontAwesomeIcon
className="col-span-1 animate-spin"
icon={faLoader}
size="2xl"
/>
);
}
case JobState.COMPLETE_SUCCESS:{
return(
case JobState.COMPLETE_SUCCESS: {
return (
<FontAwesomeIcon
className="col-span-1"
icon={faCircleCheck}
@@ -68,8 +71,8 @@ const JobStatusIcon = ({jobStatus} : {jobStatus:string})=>{
/>
);
}
default:{
return(
default: {
return (
<FontAwesomeIcon
className="col-span-1"
icon={faCircleExclamation}
@@ -80,62 +83,56 @@ const JobStatusIcon = ({jobStatus} : {jobStatus:string})=>{
}
};
const InferenceItem = ({job}:{job:InferenceJob}) => {
// console.log(job);
return(
<li
className="grid grid-cols-12 gap-2 items-center bg-brand-secondary rounded-md py-2 px-4"
>
const InferenceItem = ({ job }: { job: InferenceJob }) => {
return (
<li className="grid grid-cols-12 items-center gap-2 rounded-md bg-brand-secondary px-4 py-2">
<JobStatusIcon jobStatus={job.job_status} />
<div className="col-span-10 flex flex-col">
<H4>{printJobType(job.job_type)}</H4>
<H6>{printJobStatus(job.job_status)} </H6>
<H4>{printJobType(job.job_type)}</H4>
<H6>{printJobStatus(job.job_status)} </H6>
</div>
{job.job_status === JobState.COMPLETE_SUCCESS &&
{job.job_status === JobState.COMPLETE_SUCCESS && (
<div className="col-span-1 flex justify-end">
<ButtonIcon
icon={faSquareXmark}
size="2xl"
onClick={()=>{
onClick={() => {
deleteInferenceJob(job);
}}
/>
</div>
}
)}
</li>
);
};
export const DialogueInference = () =>{
export const DialogueInference = () => {
useSignals();
const buttonIcon = inferenceJobs.value.length > 0
? faClipboardList : faClipboard;
return(
const buttonIcon =
inferenceJobs.value.length > 0 ? faClipboardList : faClipboard;
return (
<ButtonDialogue
title="My Jobs"
dialogProps={{
className:"w-1/2 max-w-full"
className: "w-1/2 max-w-full",
}}
buttonProps={{
icon: buttonIcon,
variant: "secondary",
label: "My Jobs",
className:( inferenceJobs.value.length === 0 ? "invisible" : "visible")
}}
>
{inferenceJobs.value.length > 0 &&
className: inferenceJobs.value.length === 0 ? "invisible" : "visible",
}}>
{inferenceJobs.value.length > 0 && (
<ul className="flex flex-col gap-2">
{inferenceJobs.value.map(job=>
{inferenceJobs.value.map((job) => (
<InferenceItem key={job.job_id} job={job} />
)}
))}
</ul>
}
{
inferenceJobs.value.length === 0 &&
)}
{inferenceJobs.value.length === 0 && (
<P>You currently have no running proccesses.</P>
}
)}
</ButtonDialogue>
);
}
};
+4 -14
View File
@@ -1,7 +1,7 @@
import React, { useCallback, UIEvent } from "react";
import React, { useCallback } from "react";
import { useMouseEventsTimeline } from "~/pages/PageEnigma/comps/Timeline/utils/useMouseEventsTimeline";
import {
currentScroll,
timelineScrollX,
currentTime,
dndTimelineHeight,
overTimeline,
@@ -29,7 +29,7 @@ export const LowerPanel = ({ children, onStyle }: LowerPanelPropsI) => {
(event: React.PointerEvent<HTMLDivElement>) => {
if (event.button === 0) {
const newTime = Math.round(
(event.clientX + currentScroll.value - 92) / 4 / scale.value,
(event.clientX + timelineScrollX.value - 92) / 4 / scale.value,
);
if (newTime < 0) {
return;
@@ -45,10 +45,6 @@ export const LowerPanel = ({ children, onStyle }: LowerPanelPropsI) => {
[],
);
const onScroll = useCallback((event: UIEvent<HTMLDivElement>) => {
currentScroll.value = event.currentTarget.scrollLeft;
}, []);
return (
<>
{!onStyle && (
@@ -59,19 +55,13 @@ export const LowerPanel = ({ children, onStyle }: LowerPanelPropsI) => {
/>
)}
<div
className={[
"absolute bottom-0",
"w-screen",
onStyle ? "overflow-x-auto overflow-y-hidden" : "overflow-auto",
"bg-ui-panel",
].join(" ")}
className={["absolute bottom-0", "w-screen", "bg-ui-panel"].join(" ")}
style={{ height: onStyle ? 80 : displayHeight }}
onPointerOver={() => {
overTimeline.value = true;
}}
onPointerLeave={() => (overTimeline.value = false)}
onPointerDown={onTimelineClick}
onScroll={onScroll}
>
{children}
</div>
+5 -11
View File
@@ -1,5 +1,4 @@
import React, { useContext } from "react";
import { useParams } from "@remix-run/react";
import { LoadingBar, LoadingDots } from "~/components";
import { SidePanel } from "~/modules/SidePanel";
@@ -57,8 +56,7 @@ export const PageEditor = ({ setPage }: Props) => {
<TopBar pageName="Edit Scene" />
<div
className="relative flex w-screen"
style={{ height: "calc(100vh - 68px)" }}
>
style={{ height: "calc(100vh - 68px)" }}>
{/* Engine section/side panel */}
<div
id="engine-n-panels-wrapper"
@@ -66,23 +64,20 @@ export const PageEditor = ({ setPage }: Props) => {
style={{
height,
width,
}}
>
}}>
<div className="relative w-full overflow-hidden bg-transparent">
<div
className={
appUiState.viewMode === APPUI_VIEW_MODES.SIDE_BY_SIDE
? "invisible"
: ""
}
>
}>
<canvas id="video-scene" width="1280px" height="720px" />
{/* Top controls */}
<div
className="absolute left-0 top-0 w-full"
onClick={handleOverlayClick}
>
onClick={handleOverlayClick}>
<div className="grid grid-cols-3 gap-4">
<ControlsTopButtons />
<Controls3D setPage={setPage} />
@@ -98,8 +93,7 @@ export const PageEditor = ({ setPage }: Props) => {
(sidePanelVisible.value ? sidePanelWidth.value : 0) -
84,
}}
onClick={handleOverlayClick}
>
onClick={handleOverlayClick}>
<PreviewEngineCamera />
<ControlsVideo />
<ControlPanelSceneObject />
+10 -8
View File
@@ -7,7 +7,6 @@ import { PageEditor } from "~/pages/PageEnigma/PageEditor";
import { PageStyling } from "~/pages/PageEnigma/PageStyling";
import { useParams } from "@remix-run/react";
export const PageEnigmaComponent = () => {
useSignals();
@@ -15,16 +14,19 @@ export const PageEnigmaComponent = () => {
const editor = useContext(EngineContext);
const [page, setPage] = useState("edit");
const params = useParams();
useEffect(() => {
if (editor && editor.can_initialize && dispatchAppUiState !== null) {
console.log("initializing Editor");
const sceneToken = params["sceneToken"]
editor.initialize({
dispatchAppUiState
},sceneToken);
const sceneToken = params["sceneToken"];
editor.initialize(
{
dispatchAppUiState,
},
sceneToken,
);
}
}, [editor, dispatchAppUiState]);
+56 -18
View File
@@ -5,15 +5,23 @@ import {
faArrowsRotate,
faFilm,
} from "@fortawesome/pro-solid-svg-icons";
import { Button,LoadingBar } from "~/components";
import { Button, LoadingBar } from "~/components";
import { StyleSelection } from "~/pages/PageEnigma/comps/StyleSelection";
import { LowerPanel } from "~/modules/LowerPanel";
import { TimerGrid } from "~/pages/PageEnigma/comps/TimerGrid/TimerGrid";
import { TimerGrid } from "~/pages/PageEnigma/comps/Timeline/TimerGrid";
import { Scrubber } from "~/pages/PageEnigma/comps/Timeline/Scrubber";
import { PreviewImages } from "~/pages/PageEnigma/comps/PreviewImages";
import { TopBar } from "~/modules/TopBar";
import { useContext } from "react";
import React, { UIEvent, useCallback, useContext } from "react";
import { EngineContext } from "~/contexts/EngineContext";
import {
currentTime,
filmLength,
scale,
timelineScrollX,
} from "~/pages/PageEnigma/store";
import Queue from "~/pages/PageEnigma/Queue/Queue";
import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
interface Props {
setPage: (page: string) => void;
@@ -37,6 +45,30 @@ export const PageStyling = ({ setPage }: Props) => {
setPage("edit");
};
const onScroll = useCallback((event: UIEvent<HTMLDivElement>) => {
timelineScrollX.value = event.currentTarget.scrollLeft;
}, []);
const onTimelineClick = useCallback(
(event: React.PointerEvent<HTMLDivElement>) => {
if (event.button === 0) {
const newTime = Math.round(
(event.clientX + timelineScrollX.value - 92) / 4 / scale.value,
);
if (newTime < 0) {
return;
}
currentTime.value = newTime;
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.UPDATE_TIME,
data: { currentTime: Math.round(newTime) },
});
}
},
[],
);
return (
<div className="w-screen">
<TopBar pageName="Stylization" />
@@ -56,8 +88,7 @@ export const PageStyling = ({ setPage }: Props) => {
<Button
icon={faArrowsRotate}
variant="action"
onClick={generateFrame}
>
onClick={generateFrame}>
Update Preview
</Button>
<Button icon={faFilm} variant="primary" onClick={generateMovie}>
@@ -68,21 +99,28 @@ export const PageStyling = ({ setPage }: Props) => {
</div>
<div className="fixed bottom-0 left-0 w-full">
<div className="relative flex h-[80px] w-full gap-5 border-t border-t-action-600 bg-ui-panel">
<LowerPanel onStyle>
<TimerGrid />
<Scrubber />
</LowerPanel>
<div className="w-screen overflow-x-auto" onScroll={onScroll}>
<div
style={{
width: filmLength.value * 60 * 4 * scale.value,
height: 60,
}}
onClick={onTimelineClick}>
<TimerGrid />
<Scrubber />
</div>
</div>
</div>
</div>
<LoadingBar
id="editor-loading-bar"
wrapperClassName="absolute top-0 left-0"
innerWrapperClassName="max-w-screen-sm"
isShowing={appUiState.showEditorLoadingBar.isShowing}
message={appUiState.showEditorLoadingBar.message}
label={appUiState.showEditorLoadingBar.label}
progress={appUiState.showEditorLoadingBar.progress}
/>
id="editor-loading-bar"
wrapperClassName="absolute top-0 left-0"
innerWrapperClassName="max-w-screen-sm"
isShowing={appUiState.showEditorLoadingBar.isShowing}
message={appUiState.showEditorLoadingBar.message}
label={appUiState.showEditorLoadingBar.label}
progress={appUiState.showEditorLoadingBar.progress}
/>
</div>
);
};
@@ -7,8 +7,8 @@ import {
} from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { AppUiContext } from "../../../../contexts/AppUiContext";
import { EngineContext } from "../../../../contexts/EngineContext";
import { AppUiContext } from "~/contexts/AppUiContext";
import { EngineContext } from "~/contexts/EngineContext";
import { Button, H5, InputVector } from "~/components";
import { XYZ } from "../../datastructures/common";
@@ -33,6 +33,9 @@ export const ControlPanelSceneObject = () => {
useEffect(() => {
// TODO this causes a subtle bug because it renders way too many times.
if (!appUiState.controlPanel.currentSceneObject) {
return;
}
const vectors = appUiState.controlPanel.currentSceneObject.objectVectors;
editorEngine?.setSelectedObject(
@@ -176,8 +179,7 @@ export const ControlPanelSceneObject = () => {
enterTo="opacity-100"
leave="transition-opacity duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
leaveTo="opacity-0">
<div className="flex justify-between">
<div className="flex items-center gap-2">
<FontAwesomeIcon icon={faCube} />
@@ -227,8 +229,7 @@ export const ControlPanelSceneObject = () => {
<Button
variant="secondary"
className="grow"
onClick={handleOnAddKeyFrame}
>
onClick={handleOnAddKeyFrame}>
Add Keyframe (K)
</Button>
<Button
@@ -1,13 +1,13 @@
import { useContext } from "react";
import { Button, } from "~/components";
import { Button } from "~/components";
import { ClipGroup } from "~/pages/PageEnigma/models/track";
import { APPUI_ACTION_TYPES } from "~/reducers";
import { AppUiContext } from "~/contexts/AppUiContext";
import { EngineContext } from "~/contexts/EngineContext";
export const TestFeaturesButtons = ({debug}:{debug:boolean})=>{
export const TestFeaturesButtons = ({ debug }: { debug: boolean }) => {
if (!debug) return null;
const [appUiState, dispatchAppUiState] = useContext(AppUiContext);
const editorEngine = useContext(EngineContext);
@@ -34,10 +34,10 @@ export const TestFeaturesButtons = ({debug}:{debug:boolean})=>{
}
};
const testTTSPanel = () =>{
const testTTSPanel = () => {
dispatchAppUiState({
type: APPUI_ACTION_TYPES.OPEN_DIALOGUE_TTS
})
type: APPUI_ACTION_TYPES.OPEN_DIALOGUE_TTS,
});
};
const handleButtonCameraView = () => {
@@ -45,7 +45,7 @@ export const TestFeaturesButtons = ({debug}:{debug:boolean})=>{
};
const testStylizeRequest = () => {
// editorEngine?.testStylizeRequest();
console.log('editorEnging does not have testStylizeRequest')
console.log("editorEnging does not have testStylizeRequest");
};
const handleButtonRender = () => {
editorEngine?.generateVideo();
@@ -53,7 +53,7 @@ export const TestFeaturesButtons = ({debug}:{debug:boolean})=>{
const handleButtonTakeFrame = () => {
// editorEngine?.take_timeline_cam_clip();
console.log('editorEnging does not have take_timeline_cam_clip')
console.log("editorEnging does not have take_timeline_cam_clip");
};
const handleButtonSingleFrame = () => {
@@ -64,39 +64,45 @@ export const TestFeaturesButtons = ({debug}:{debug:boolean})=>{
};
const smallButtons = "text-xs p-2 h-6 ";
const tertiaryColor = "bg-brand-tertiary hover:bg-brand-teriary-400 focus-visible:outline-brand-tertiary ";
return(
const tertiaryColor =
"bg-brand-tertiary hover:bg-brand-teriary-400 focus-visible:outline-brand-tertiary ";
return (
<>
<div className="flex gap-1">
<Button variant="secondary" onClick={handleButtonCameraView} className={smallButtons}>
<Button
variant="secondary"
onClick={handleButtonCameraView}
className={smallButtons}>
Toggle Camera View
</Button>
<Button
onClick={testObjectPanel}
className={smallButtons}
>
<Button onClick={testObjectPanel} className={smallButtons}>
Object Panel
</Button>
<Button
onClick={testTTSPanel}
className={tertiaryColor + smallButtons}
>
<Button onClick={testTTSPanel} className={tertiaryColor + smallButtons}>
TTS Panel
</Button>
</div>
<div className="flex gap-1">
<Button onClick={handleButtonSingleFrame} className={smallButtons}>Render Single Frame</Button>
<Button onClick={handleButtonTakeFrame} className={smallButtons}>Take Frame</Button>
<Button onClick={handleButtonRender} className={smallButtons}>Render</Button>
<Button onClick={handleButtonSingleFrame} className={smallButtons}>
Render Single Frame
</Button>
<Button onClick={handleButtonTakeFrame} className={smallButtons}>
Take Frame
</Button>
<Button onClick={handleButtonRender} className={smallButtons}>
Render
</Button>
<Button
onClick={testStylizeRequest}
className={"bg-brand-tertiary hover:bg-brand-teriary-400 focus-visible:outline-brand-tertiary " +smallButtons}
style={{ zIndex: 9001 }}
>
className={
"hover:bg-brand-teriary-400 bg-brand-tertiary focus-visible:outline-brand-tertiary " +
smallButtons
}
style={{ zIndex: 9001 }}>
Test Stylize
</Button>
</div>
</>
)
}
);
};
+4 -14
View File
@@ -1,10 +1,5 @@
import { TrackClips } from "~/pages/PageEnigma/comps/Timeline/TrackClips";
import {
audioGroup,
fullWidth,
toggleAudioMute,
updateAudio,
} from "~/pages/PageEnigma/store";
import { audioGroup, fullWidth, updateAudio } from "~/pages/PageEnigma/store";
import { ClipGroup } from "~/pages/PageEnigma/models";
export const Audio = () => {
@@ -12,21 +7,16 @@ export const Audio = () => {
return (
<div
className="bg-global_audio-groupBg block rounded-lg pb-5 pl-2 pr-4"
style={{ width: fullWidth.value + 90 }}
className="block rounded-r-lg bg-global_audio-groupBg pb-5 pr-4"
style={{ width: fullWidth.value + 16 }}
>
<div className="prevent-select mb-5 pt-2 text-xs font-medium text-white">
Global Audio
</div>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-4 pt-4">
<TrackClips
id={audioGroup.value.id}
clips={clips}
title="Global Audio TrackClips"
updateClip={updateAudio}
group={ClipGroup.GLOBAL_AUDIO}
muted={audioGroup.value.muted}
toggleMute={toggleAudioMute}
/>
</div>
</div>
@@ -9,11 +9,10 @@ export const Camera = () => {
return (
<div
className="block rounded-lg bg-camera-groupBg pb-5 pl-2 pr-4"
style={{ width: fullWidth.value + 90 }}
className="block rounded-r-lg bg-camera-groupBg pb-5 pr-4"
style={{ width: fullWidth.value + 16 }}
>
<div className="mb-5 pt-2 text-xs font-medium text-white">Camera</div>
<div className="flex flex-col gap-4">
<div className="pt-4">
<TrackKeyFrames
id={cameraGroup.value.id}
keyframes={keyframes}
@@ -1,13 +1,12 @@
import { useCallback, useMemo } from "react";
import { TrackClips } from "~/pages/PageEnigma/comps/Timeline/TrackClips";
import {
characterGroups,
fullWidth,
toggleLipSyncMute,
updateCharacters,
} from "~/pages/PageEnigma/store";
import { TrackKeyFrames } from "~/pages/PageEnigma/comps/Timeline/TrackKeyFrames";
import { ClipGroup, ClipType } from "~/pages/PageEnigma/models";
import { CharacterTrack, ClipGroup, ClipType } from "~/pages/PageEnigma/models";
function buildUpdaters(
updateCharacters: (options: {
@@ -37,35 +36,23 @@ function buildUpdaters(
return { updateClipLipSync, updateClipPosition, updateClipAnimations };
}
interface Props {
characterId: string;
character: CharacterTrack;
}
export const Character = ({ characterId }: Props) => {
const character = characterGroups.value.find((row) => (row.id = characterId));
export const Character = ({ character }: Props) => {
const { updateClipLipSync, updateClipPosition, updateClipAnimations } =
useMemo(() => buildUpdaters(updateCharacters), []);
const toggleCharacterLipSyncMute = useCallback(() => {
toggleLipSyncMute(character?.id ?? "");
}, [character?.id]);
if (!character) {
return false;
}
const { animationClips, positionKeyframes, lipSyncClips } = character;
return (
<div
className="block rounded-lg bg-character-groupBg pb-5 pl-2 pr-4"
style={{ width: fullWidth.value + 90 }}
className="block rounded-r-lg bg-character-groupBg pb-5 pr-4"
style={{ width: fullWidth.value + 16 }}
>
<div className="prevent-select mb-5 pt-2 text-xs font-medium text-white">
Character
</div>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-4 pt-4">
<TrackClips
id={character.id}
id={character.object_uuid}
clips={animationClips}
title="Animation"
updateClip={updateClipAnimations}
@@ -73,19 +60,17 @@ export const Character = ({ characterId }: Props) => {
type={ClipType.ANIMATION}
/>
<TrackKeyFrames
id={character.id}
id={character.object_uuid}
keyframes={positionKeyframes}
title="Character Position/Rotation"
updateKeyframe={updateClipPosition}
group={ClipGroup.CHARACTER}
/>
<TrackClips
id={character.id}
id={character.object_uuid}
clips={lipSyncClips}
title="Lipsync Audio TrackClips"
updateClip={updateClipLipSync}
muted={character.muted}
toggleMute={toggleCharacterLipSyncMute}
group={ClipGroup.CHARACTER}
type={ClipType.AUDIO}
/>
@@ -1,4 +1,4 @@
import { characterGroups } from "~/pages/PageEnigma/store";
import { characterGroup } from "~/pages/PageEnigma/store";
import { Character } from "~/pages/PageEnigma/comps/Timeline/Character";
import { useSignals } from "@preact/signals-react/runtime";
@@ -6,8 +6,10 @@ export const Characters = () => {
useSignals();
return (
<>
{characterGroups.value.map((character) => (
<Character key={character.id} characterId={character.id} />
{characterGroup.value.characters.map((character) => (
<div key={character.object_uuid} className="pb-4 pr-4">
<Character character={character} />
</div>
))}
</>
);
@@ -1,17 +1,17 @@
import { objectGroup } from "~/pages/PageEnigma/store";
import { useSignals } from "@preact/signals-react/runtime";
import { ObjectTrack } from "~/pages/PageEnigma/comps/Timeline/ObjectTrack";
import { ObjectTrackComponent } from "~/pages/PageEnigma/comps/Timeline/ObjectTrack";
export const ObjectGroups = () => {
useSignals();
return (
<>
{objectGroup.value.objects.length > 0 && (
<div className="p-4">
<ObjectTrack />
{objectGroup.value.objects.map((object) => (
<div key={object.object_uuid} className="pb-4 pr-4">
<ObjectTrackComponent object={object} />
</div>
)}
))}
</>
);
};
@@ -1,27 +1,26 @@
import { fullWidth, objectGroup, updateObject } from "~/pages/PageEnigma/store";
import { fullWidth, updateObject } from "~/pages/PageEnigma/store";
import { TrackKeyFrames } from "~/pages/PageEnigma/comps/Timeline/TrackKeyFrames";
import { ClipGroup } from "~/pages/PageEnigma/models";
import { ClipGroup, ObjectTrack } from "~/pages/PageEnigma/models";
export const ObjectTrack = () => {
interface Props {
object: ObjectTrack;
}
export const ObjectTrackComponent = ({ object }: Props) => {
return (
<div
className="bg-object-groupBg block rounded-lg pb-5 pl-2 pr-4"
style={{ width: fullWidth.value + 90 }}
className="mr-4 block rounded-r-lg bg-object-groupBg pb-5 pr-4"
style={{ width: fullWidth.value + 32 }}
>
<div className="prevent-select mb-5 pt-2 text-xs font-medium text-white">
Objects
<div key={object.object_uuid} className="pt-4">
<TrackKeyFrames
id={object.object_uuid}
keyframes={object.keyframes}
title={`${object.name} Position/Rotation`}
updateKeyframe={updateObject}
group={ClipGroup.OBJECT}
/>
</div>
{objectGroup.value.objects.map((object) => (
<div key={object.object_uuid} className="flex flex-col gap-4">
<TrackKeyFrames
id={object.object_uuid}
keyframes={object.keyframes}
title={`${object.name} Position/Rotation`}
updateKeyframe={updateObject}
group={ClipGroup.OBJECT}
/>
</div>
))}
</div>
);
};
@@ -0,0 +1,89 @@
import {
audioGroup,
characterGroup,
objectGroup,
timelineScrollY,
toggleAudioMute,
toggleLipSyncMute,
} from "~/pages/PageEnigma/store";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faVolume, faVolumeSlash } from "@fortawesome/pro-solid-svg-icons";
import { useSignals } from "@preact/signals-react/runtime";
export const RowHeaders = () => {
useSignals();
return (
<div className="relative">
<div
className="absolute mt-2"
style={{ top: timelineScrollY.value * -1 - 8 }}
>
{characterGroup.value.characters.map((character) => (
<div className="mb-4 ml-6" key={character.object_uuid}>
<div className="relative block h-[224px] w-[64px] overflow-x-hidden rounded-l-lg bg-character-groupBg">
<div className="absolute left-2 top-2 text-xs font-medium text-white">
Character
</div>
<div className="absolute">
<button
className="text-md absolute text-white transition-colors duration-100 hover:text-white/80"
style={{ top: 174, left: 34 }}
onClick={() => toggleLipSyncMute(character.object_uuid)}
>
{character.muted ? (
<FontAwesomeIcon
icon={faVolumeSlash}
className="text-brand-primary transition-colors duration-100 hover:text-brand-primary/80"
/>
) : (
<FontAwesomeIcon icon={faVolume} />
)}
</button>
</div>
</div>
</div>
))}
<div className="mb-4 ml-6">
<div className="relative block h-[88px] w-[64px] rounded-l-lg bg-camera-groupBg">
<div className="absolute left-2 top-2 text-xs font-medium text-white">
Camera
</div>
</div>
</div>
<div className="mb-4 ml-6">
<div className="relative block h-[88px] w-[64px] rounded-l-lg bg-global_audio-groupBg">
<div className="absolute left-2 top-2 text-xs font-medium text-white">
Global Audio
</div>
<div className="absolute">
<button
className="text-md absolute text-white transition-colors duration-100 hover:text-white/80"
style={{ top: 40, left: 34 }}
onClick={() => toggleAudioMute()}
>
{audioGroup.value.muted ? (
<FontAwesomeIcon
icon={faVolumeSlash}
className="text-brand-primary transition-colors duration-100 hover:text-brand-primary/80"
/>
) : (
<FontAwesomeIcon icon={faVolume} />
)}
</button>
</div>
</div>
</div>
{objectGroup.value.objects.map((obj) => (
<div key={obj.object_uuid} className="mb-4 ml-6">
<div className="relative block h-[88px] w-[64px] rounded-l-lg bg-object-groupBg">
<div className="absolute left-2 top-2 text-xs font-medium text-white">
Object
</div>
</div>
</div>
))}
</div>
</div>
);
};
@@ -1,4 +1,9 @@
import { currentTime, fullHeight, scale } from "~/pages/PageEnigma/store";
import {
timelineScrollX,
currentTime,
fullHeight,
scale,
} from "~/pages/PageEnigma/store";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faSortDown } from "@fortawesome/pro-solid-svg-icons";
import { useMouseEventsScrubber } from "~/pages/PageEnigma/comps/Timeline/utils/useMouseEventsScrubber";
@@ -9,10 +14,16 @@ export const Scrubber = () => {
const { onPointerDown, time } = useMouseEventsScrubber();
const displayTime = time === -1 ? currentTime.value : time;
if (displayTime * 4 * scale.value + 84 - timelineScrollX.value < 84) {
return null;
}
return (
<div
className="absolute flex cursor-ew-resize flex-col items-center text-brand-primary"
style={{ top: 8, left: displayTime * 4 * scale.value + 84 }}
style={{
top: 8,
left: displayTime * 4 * scale.value + 84 - timelineScrollX.value,
}}
onPointerDown={onPointerDown}
>
<div>
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from "react";
import { UIEvent, useCallback, useEffect, useState } from "react";
import { LowerPanel } from "~/modules/LowerPanel";
import { Camera } from "./Camera";
@@ -10,15 +10,21 @@ import {
deleteCharacterClip,
selectedItem,
ignoreDelete,
timelineScrollX,
timelineScrollY,
filmLength,
scale,
} from "~/pages/PageEnigma/store";
import { useQueueHandler } from "~/pages/PageEnigma/comps/Timeline/utils/useQueueHandler";
import { useSignals } from "@preact/signals-react/runtime";
import { TimerGrid } from "~/pages/PageEnigma/comps/TimerGrid/TimerGrid";
import { TimerGrid } from "~/pages/PageEnigma/comps/Timeline/TimerGrid";
import { Scrubber } from "~/pages/PageEnigma/comps/Timeline/Scrubber";
import { Characters } from "~/pages/PageEnigma/comps/Timeline/Characters";
import { ObjectGroups } from "~/pages/PageEnigma/comps/Timeline/ObjectGroups";
import useUpdateKeyframe from "~/pages/PageEnigma/contexts/TrackContext/utils/useUpdateKeyframe";
import { Clip, Keyframe } from "~/pages/PageEnigma/models";
import { RowHeaders } from "~/pages/PageEnigma/comps/Timeline/RowHeaders";
import { pageWidth } from "~/store";
function getItemType(item: Clip | Keyframe | null) {
if (!item) {
@@ -32,6 +38,11 @@ export const Timeline = () => {
const [dialogOpen, setDialogOpen] = useState(false);
const { deleteKeyframe } = useUpdateKeyframe();
const onScroll = useCallback((event: UIEvent<HTMLDivElement>) => {
timelineScrollX.value = event.currentTarget.scrollLeft;
timelineScrollY.value = event.currentTarget.scrollTop;
}, []);
// implement the code to handle incoming messages from the Engine
useQueueHandler();
@@ -75,17 +86,39 @@ export const Timeline = () => {
<>
<LowerPanel>
<TimerGrid />
<div className="p-4">
<Characters />
<div className="flex">
<div
className="mt-2 overflow-hidden"
style={{
width: 88,
height: timelineHeight.value - 54,
}}
>
<RowHeaders />
</div>
<div
className="mt-2 overflow-auto"
onScroll={onScroll}
style={{
width: pageWidth.value - 90,
height: timelineHeight.value - 54,
}}
>
<div
style={{ width: filmLength.value * 60 * 4 * scale.value + 32 }}
>
<Characters />
<div className="pb-4 pr-8">
<Camera />
</div>
<div className="pb-4 pr-8">
<Audio />
</div>
<ObjectGroups />
</div>
</div>
<Scrubber />
</div>
<div className="p-4">
<Camera />
</div>
<div className="p-4">
<Audio />
</div>
<ObjectGroups />
<Scrubber />
</LowerPanel>
<ConfirmationModal
title="Delete Clip"
@@ -0,0 +1,78 @@
import {
timelineScrollX,
filmLength,
fullHeight,
scale,
} from "~/pages/PageEnigma/store";
import { Fragment } from "react";
import { useSignals } from "@preact/signals-react/runtime";
export const TimerGrid = () => {
useSignals();
const sectionWidth = 60 * 4 * scale.value;
return (
<div
className={[
"prevent-select ml-[88px] mt-4",
"relative flex h-7 overflow-hidden",
"border-t border-t-ui-panel-border",
"text-xs text-white opacity-75",
].join(" ")}
>
<div className="absolute" style={{ left: timelineScrollX.value * -1 }}>
{Array(filmLength.value)
.fill(0)
.map((_, index) => (
<Fragment key={index}>
<div
className="absolute ps-1 pt-2"
style={{ left: index * sectionWidth + 4 }}
>
00:{index < 10 ? "0" + index.toString() : index.toString()}
</div>
<div
className="absolute block bg-ui-divider"
style={{
width: 1,
left: index * sectionWidth,
height: fullHeight.value,
}}
/>
{Array(9)
.fill(0)
.map((_, ind) => (
<div
key={ind}
className="absolute block h-2 bg-ui-divider"
style={{
width: 1,
top: 0,
left:
index * sectionWidth + (sectionWidth / 10) * (ind + 1),
}}
/>
))}
</Fragment>
))}
<div
className="absolute pt-2"
style={{ left: filmLength.value * sectionWidth + 4 }}
>
00:
{filmLength.value < 10
? "0" + filmLength.value.toString()
: filmLength.value.toString()}
</div>
<div
className="absolute block h-full bg-ui-divider"
style={{
width: 1,
left: filmLength.value * sectionWidth,
height: fullHeight.value,
}}
/>
</div>
</div>
);
};
@@ -23,8 +23,6 @@ interface Props {
title: string;
group: ClipGroup;
type?: ClipType;
toggleMute?: () => void;
muted?: boolean;
updateClip: (options: { id: string; length: number; offset: number }) => void;
}
@@ -56,9 +54,7 @@ function getCanBuild({
export const TrackClips = ({
id,
clips,
toggleMute,
updateClip,
muted,
title,
group,
type,
@@ -81,7 +77,7 @@ export const TrackClips = ({
return;
}
// Now check the the clip fits
// Now check if the clip fits
const position = track.getBoundingClientRect();
const clipOffset = (event.clientX - position.x) / 4 / scale.value;
@@ -114,49 +110,31 @@ export const TrackClips = ({
}
return (
<div className="pl-16">
<div
id={`track-${trackType}-${id}`}
className={`relative mt-4 block h-9 w-full rounded-lg bg-${group}-unselected`}
onPointerOver={onPointerOver}
onPointerLeave={onPointerLeave}
onPointerMove={onPointerMove}
>
{clips.map((clip, index) => (
<TrackClip
key={clip.clip_uuid}
min={
index > 0 ? clips[index - 1].offset + clips[index - 1].length : 0
}
max={
index < clips.length - 1
? clips[index + 1].offset
: filmLength.value * 60
}
group={group}
updateClip={updateClip}
clip={clip}
/>
))}
<div className="prevent-select absolute ps-2 pt-1 text-xs font-medium text-white">
{title}
</div>
{!!toggleMute && (
<button
className="text-md absolute text-white transition-colors duration-100 hover:text-white/80"
style={{ top: 6, left: -28 }}
onClick={toggleMute}
>
{muted ? (
<FontAwesomeIcon
icon={faVolumeSlash}
className="text-brand-primary transition-colors duration-100 hover:text-brand-primary/80"
/>
) : (
<FontAwesomeIcon icon={faVolume} />
)}
</button>
)}
<div
id={`track-${trackType}-${id}`}
className={`relative mt-4 block h-9 w-full rounded-lg bg-${group}-unselected`}
onPointerOver={onPointerOver}
onPointerLeave={onPointerLeave}
onPointerMove={onPointerMove}
>
{clips.map((clip, index) => (
<TrackClip
key={clip.clip_uuid}
min={
index > 0 ? clips[index - 1].offset + clips[index - 1].length : 0
}
max={
index < clips.length - 1
? clips[index + 1].offset
: filmLength.value * 60
}
group={group}
updateClip={updateClip}
clip={clip}
/>
))}
<div className="prevent-select absolute ps-2 pt-1 text-xs font-medium text-white">
{title}
</div>
</div>
);
@@ -24,7 +24,7 @@ export const TrackKeyFrames = ({
group,
}: Props) => {
return (
<div className="pl-16">
<div>
<div
id={`track-${group}-${id}`}
className={`relative mt-4 block h-9 w-full rounded-lg bg-${group}-unselected`}
@@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useState } from "react";
import {
currentScroll,
timelineScrollX,
currentTime,
filmLength,
scale,
@@ -26,7 +26,7 @@ export const useMouseEventsScrubber = () => {
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.UPDATE_TIME,
data: { currentTime: Math.round(time) + currentScroll.value },
data: { currentTime: Math.round(time) + timelineScrollX.value },
});
}
};
@@ -46,7 +46,7 @@ export const useMouseEventsScrubber = () => {
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.UPDATE_TIME,
data: { currentTime: delta + currentScroll.value },
data: { currentTime: delta + timelineScrollX.value },
});
}
return delta;
@@ -40,31 +40,34 @@ export function useQueueHandler() {
const { addKeyframe, clearExistingData, deleteObjectOrCharacter } =
useContext(TrackContext);
const handleFromEngineActions = useCallback(({ action, data }: Arguments) => {
console.log("FROM ENGINE", action, data);
switch (action) {
case fromEngineActions.UPDATE_TIME:
currentTime.value = (data as UpdateTime).currentTime;
break;
case fromEngineActions.UPDATE_TIME_LINE:
clearExistingData();
(data as ClipUI[]).forEach((item) => {
LOADING_FUNCTIONS[item.group](item);
});
break;
case fromEngineActions.UPDATE_CHARACTER_ID: {
addNewCharacter(data as MediaItem);
break;
const handleFromEngineActions = useCallback(
({ action, data }: Arguments) => {
console.log("FROM ENGINE", action, data);
switch (action) {
case fromEngineActions.UPDATE_TIME:
currentTime.value = (data as UpdateTime).currentTime;
break;
case fromEngineActions.UPDATE_TIME_LINE:
clearExistingData();
(data as ClipUI[]).forEach((item) => {
LOADING_FUNCTIONS[item.group](item);
});
break;
case fromEngineActions.UPDATE_CHARACTER_ID: {
addNewCharacter(data as MediaItem);
break;
}
case fromEngineActions.DELETE_OBJECT: {
// this could be an object or character
deleteObjectOrCharacter(data as MediaItem);
break;
}
default:
throw new Error(`Unknown action ${action}`);
}
case fromEngineActions.DELETE_OBJECT: {
// this could be an object or character
deleteObjectOrCharacter(data as MediaItem);
break;
}
default:
throw new Error(`Unknown action ${action}`);
}
}, []);
},
[clearExistingData, deleteObjectOrCharacter],
);
const handleToTimelineActions = useCallback(
({ action, data }: Arguments) => {
@@ -1,55 +0,0 @@
import { filmLength, fullHeight, scale } from "~/pages/PageEnigma/store";
import { Fragment } from "react";
export const TimerGrid = () => {
const sectionWidth = 60 * 4 * scale.value;
return (
<div
className={[
"prevent-select mt-4",
"flex h-3",
"border-t border-t-ui-panel-border",
"text-xs text-white opacity-75",
].join(" ")}
>
{Array(filmLength.value)
.fill(0)
.map((_, index) => (
<Fragment key={index}>
<div
className="absolute ps-1 pt-1"
style={{ left: index * sectionWidth + 92 }}
>
00:{index < 10 ? "0" + index.toString() : index.toString()}
</div>
<div
className="absolute block h-full bg-ui-divider"
style={{
width: 1,
left: index * sectionWidth + 88,
height: fullHeight.value,
}}
/>
</Fragment>
))}
<div
className="absolute"
style={{ left: filmLength.value * sectionWidth + 92 }}
>
00:
{filmLength.value < 10
? "0" + filmLength.value.toString()
: filmLength.value.toString()}
</div>
<div
className="absolute block h-full bg-ui-divider"
style={{
width: 1,
left: filmLength.value * sectionWidth + 88,
height: fullHeight.value,
}}
/>
</div>
);
};
@@ -12,7 +12,7 @@ import {
dropId,
dropOffset,
addObject,
characterGroups,
characterGroup,
cameraGroup,
audioGroup,
objectGroup,
@@ -75,7 +75,10 @@ export const TrackProvider = ({ children }: Props) => {
}, [endDrag]);
const clearExistingData = useCallback(() => {
characterGroups.value = [];
characterGroup.value = {
id: "ChG1",
characters: [],
};
cameraGroup.value = {
id: "CG1",
keyframes: [],
+25 -24
View File
@@ -219,11 +219,12 @@ class Editor {
}
isEmpty(value: string) {
return (value == null || (typeof value === "string" && value.trim().length === 0));
return (
value == null || (typeof value === "string" && value.trim().length === 0)
);
}
initialize(config: any, sceneToken) {
//setup reactland Callbacks
this.dispatchAppUiState = config.dispatchAppUiState;
@@ -358,7 +359,7 @@ class Editor {
this.updateLoop();
if (this.isEmpty(sceneToken) == false) {
this.loadScene(sceneToken)
this.loadScene(sceneToken);
}
this.dispatchAppUiState({
@@ -384,7 +385,7 @@ class Editor {
console.log(result);
}
public async testTestTimelineEvents() { }
public async testTestTimelineEvents() {}
public async loadScene(scene_media_token: string) {
this.dispatchAppUiState({
@@ -427,7 +428,7 @@ class Editor {
// TO UPDATE selected objects in the scene might want to add to the scene ...
async setSelectedObject(position: XYZ, rotation: XYZ, scale: XYZ) {
if (this.selected != undefined || this.selected != null) {
//console.log(`triggering setSelectedObject`)
console.log(`triggering setSelectedObject`);
this.selected.position.x = position.x;
this.selected.position.y = position.y;
this.selected.position.z = position.z;
@@ -528,6 +529,7 @@ class Editor {
this.removeTransformControls();
this.selected = this.cam_obj;
this.dispatchAppUiState({
type: APPUI_ACTION_TYPES.SHOW_CONTROLPANELS_SCENEOBJECT,
});
@@ -574,7 +576,7 @@ class Editor {
payload: {
showEditorLoadingBar: {
progress: progress,
message: message
message: message,
},
},
});
@@ -719,7 +721,9 @@ class Editor {
this.rawRenderer.setSize(1024, 576);
this.render_camera.aspect = 1024 / 576;
this.record_stream = this.rawRenderer.domElement.captureStream(60); // Capture at 60 FPS
this.recorder = new MediaRecorder(this.record_stream, { mimeType: 'video/webm' });
this.recorder = new MediaRecorder(this.record_stream, {
mimeType: "video/webm",
});
this.recorder.ondataavailable = (event) => {
if (event.data.size > 0) {
this.frame_buffer.push(event.data);
@@ -729,7 +733,7 @@ class Editor {
};
this.recorder.onstop = () => {
this.stopPlayback();
}
};
this.recorder.start();
}
@@ -797,7 +801,7 @@ class Editor {
if (changeView) {
this.switchCameraView();
}
} else if (this.last_scrub == this.timeline.scrubber_frame_position) {
} else if (this.last_scrub !== this.timeline.scrubber_frame_position) {
this.updateSelectedUI();
}
@@ -864,10 +868,10 @@ class Editor {
audioSegment,
"-filter_complex",
"[1:a]adelay=" +
startTime * 1000 +
"|" +
startTime * 1000 +
"[a1];[0:a][a1]amix=inputs=2[a]",
startTime * 1000 +
"|" +
startTime * 1000 +
"[a1];[0:a][a1]amix=inputs=2[a]",
"-map",
"[a]",
`${itteration}final_tmp.wav`,
@@ -895,9 +899,9 @@ class Editor {
async _debugDownloadVideo(videoURL: string) {
// DEBUG ONLY to download the video
let a = document.createElement('a');
const a = document.createElement("a");
a.href = videoURL;
a.download = 'video.mp4'; // Name of the downloaded file
a.download = "video.mp4"; // Name of the downloaded file
document.body.appendChild(a);
a.click(); // Trigger the download
}
@@ -906,18 +910,17 @@ class Editor {
//let video_fps = Math.floor(this.frames * (this.cap_fps / this.timeline.timeline_limit));
//console.log("Video FPS:", video_fps)
this.rendering = false;
const videoBlob = new Blob(this.frame_buffer, { type: 'video/webm' });
const videoBlob = new Blob(this.frame_buffer, { type: "video/webm" });
const videoURL = URL.createObjectURL(videoBlob);
this.generating_preview = true;
const ffmpeg = createFFmpeg({ log: true });
await ffmpeg.load();
this.updateLoad(50, "Processing ...")
this.updateLoad(50, "Processing ...");
// Write the Uint8Array to the FFmpeg file system
ffmpeg.FS('writeFile', 'input.webm', await fetchFile(videoURL));
ffmpeg.FS("writeFile", "input.webm", await fetchFile(videoURL));
await ffmpeg.run(
"-i",
@@ -1106,13 +1109,12 @@ class Editor {
// This initializes the generation of a video render scene is where the core work happens
generateVideo() {
console.log("Generating video...", this.frame_buffer);
if (this.rendering) {
return;
}
this.showLoading()
this.showLoading();
this.rendering = true; // has to go first to debounce
this.startPlayback();
@@ -1127,8 +1129,7 @@ class Editor {
}
startPlayback() {
this.updateLoad(25, "Starting Processing")
this.updateLoad(25, "Starting Processing");
this.timeline.is_playing = true;
this.timeline.scrubber_frame_position = 0;
@@ -1143,7 +1144,7 @@ class Editor {
}
updateSelectedUI() {
if (this.selected == undefined) {
if (this.selected === undefined || this.timeline.is_playing) {
return;
}
const pos = this.selected.position;
+6
View File
@@ -41,6 +41,12 @@ export interface Keyframe {
export interface CharacterGroup {
id: string;
characters: CharacterTrack[];
}
export interface CharacterTrack {
object_uuid: string;
media_id: string;
name: string;
muted: boolean;
animationClips: Clip[];
@@ -1,8 +1,8 @@
import { CharacterGroup, MediaItem } from "~/pages/PageEnigma/models";
import { CharacterTrack, MediaItem } from "~/pages/PageEnigma/models";
import Queue from "~/pages/PageEnigma/Queue/Queue";
import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
import { characterGroups } from "~/pages/PageEnigma/store";
import { characterGroup } from "~/pages/PageEnigma/store";
export function addCharacter(character: MediaItem) {
Queue.publish({
@@ -13,16 +13,25 @@ export function addCharacter(character: MediaItem) {
}
export function addNewCharacter(data: MediaItem) {
const newCharacterGroups = {
...characterGroup.value,
characters: [...characterGroup.value.characters],
};
const newCharacter = {
id: data.object_uuid,
object_uuid: data.object_uuid,
name: data.name,
media_id: data.media_id,
muted: false,
animationClips: [],
positionKeyframes: [],
lipSyncClips: [],
} as CharacterGroup;
} as CharacterTrack;
characterGroups.value = [...characterGroups.value, newCharacter].sort(
(charA, charB) => (charA.id < charB.id ? -1 : 1),
newCharacterGroups.characters.push(newCharacter);
newCharacterGroups.characters.sort((charA, charB) =>
charA.object_uuid < charB.object_uuid ? -1 : 1,
);
characterGroup.value = newCharacterGroups;
}
@@ -0,0 +1,276 @@
import {
CharacterGroup,
Clip,
ClipGroup,
ClipType,
Keyframe,
MediaItem,
QueueKeyframe,
} from "~/pages/PageEnigma/models";
import { signal } from "@preact/signals-core";
import Queue from "~/pages/PageEnigma/Queue/Queue";
import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
import * as uuid from "uuid";
export const characterGroup = signal<CharacterGroup>({
id: "CG1",
characters: [],
});
export function addCharacterAnimation({
dragItem,
characterId,
offset,
}: {
dragItem: MediaItem;
characterId: string;
offset: number;
}) {
const clip_uuid = uuid.v4();
const newClip = {
version: 1,
media_id: dragItem.media_id,
group: ClipGroup.CHARACTER,
type: ClipType.ANIMATION,
offset,
length: dragItem.length,
clip_uuid,
name: dragItem.name,
object_uuid: characterId,
} as Clip;
const oldCharacterGroup = characterGroup.value;
characterGroup.value = {
...oldCharacterGroup,
characters: oldCharacterGroup.characters.map((character) => {
if (character.object_uuid !== characterId) {
return { ...character };
}
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.ADD_CLIP,
data: newClip,
});
return {
...character,
animationClips: [...character.animationClips, newClip].sort(
(clipA, clipB) => clipA.offset - clipB.offset,
),
};
}),
};
}
export function addCharacterAudio({
dragItem,
characterId,
offset,
}: {
dragItem: MediaItem;
characterId: string;
offset: number;
}) {
const clip_uuid = uuid.v4();
const newClip = {
version: 1,
media_id: dragItem.media_id,
group: ClipGroup.CHARACTER,
type: ClipType.AUDIO,
offset,
length: dragItem.length,
clip_uuid,
object_uuid: characterId,
name: dragItem.name,
} as Clip;
const oldCharacterGroup = characterGroup.value;
characterGroup.value = {
...oldCharacterGroup,
characters: oldCharacterGroup.characters.map((character) => {
if (character.object_uuid !== characterId) {
return { ...character };
}
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.ADD_CLIP,
data: newClip,
});
return {
...character,
lipSyncClips: [...character.lipSyncClips, newClip].sort(
(clipA, clipB) => clipA.offset - clipB.offset,
),
};
}),
};
}
export function addCharacterKeyframe(
keyframe: QueueKeyframe,
offset: number,
addToast: (type: "error" | "warning" | "success", message: string) => void,
) {
const oldCharacterGroup = characterGroup.value;
if (
oldCharacterGroup.characters.some((characterTrack) => {
if (characterTrack.object_uuid !== keyframe.object_uuid) {
return;
}
return characterTrack.positionKeyframes.some(
(row) => row.offset === offset,
);
})
) {
addToast("warning", "There can only be one keyframe at this offset.");
return;
}
const newKeyframe = {
version: keyframe.version,
keyframe_uuid: uuid.v4(),
group: keyframe.group,
object_uuid: keyframe.object_uuid,
offset,
position: keyframe.position,
rotation: keyframe.rotation,
scale: keyframe.scale,
selected: false,
} as Keyframe;
characterGroup.value = {
...oldCharacterGroup,
characters: oldCharacterGroup.characters.map((character) => {
if (character.object_uuid !== keyframe.object_uuid) {
return { ...character };
}
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.ADD_CLIP,
data: newKeyframe,
});
return {
...character,
positionKeyframes: [...character.positionKeyframes, newKeyframe].sort(
(clipA, clipB) => clipA.offset - clipB.offset,
),
};
}),
};
}
export function toggleLipSyncMute(characterId: string) {
const oldCharacterGroup = characterGroup.value;
characterGroup.value = {
...oldCharacterGroup,
characters: oldCharacterGroup.characters.map((character) => {
if (character.object_uuid === characterId) {
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: character?.muted
? toEngineActions.UNMUTE
: toEngineActions.MUTE,
data: {
version: 1,
type: ClipType.AUDIO,
group: ClipGroup.CHARACTER,
object_uuid: characterId,
},
});
}
return {
...character,
muted:
character.object_uuid === characterId
? !character.muted
: character.muted,
};
}),
};
}
export function selectCharacterClip(clipId: string) {
const oldCharacterGroup = characterGroup.value;
characterGroup.value = {
...oldCharacterGroup,
characters: oldCharacterGroup.characters.map((character) => ({
...character,
animationClips: character.animationClips.map((clip) => ({
...clip,
selected: clip.clip_uuid === clipId ? !clip.selected : clip.selected,
})),
positionClips: character.positionKeyframes.map((keyframe) => ({
...keyframe,
selected:
keyframe.keyframe_uuid === clipId
? !keyframe.selected
: keyframe.selected,
})),
lipSyncClips: character.lipSyncClips.map((clip) => ({
...clip,
selected: clip.clip_uuid === clipId ? !clip.selected : clip.selected,
})),
})),
};
}
export function deleteCharacterClip(clip: Clip) {
const oldCharacterGroup = characterGroup.value;
characterGroup.value = {
...oldCharacterGroup,
characters: oldCharacterGroup.characters.map((character) => ({
...character,
animationClips: character.animationClips.filter((row) => {
if (row.clip_uuid === clip.clip_uuid) {
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.DELETE_CLIP,
data: row,
});
return false;
}
return true;
}),
lipSyncClips: character.lipSyncClips.filter((row) => {
if (row.clip_uuid === clip.clip_uuid) {
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.DELETE_CLIP,
data: row,
});
return false;
}
return true;
}),
})),
};
}
export function deleteCharacterKeyframe(keyframe: Keyframe) {
const oldCharacterGroup = characterGroup.value;
characterGroup.value = {
...oldCharacterGroup,
characters: oldCharacterGroup.characters.map((character) => ({
...character,
positionKeyframes: character.positionKeyframes.filter((row) => {
if (row.keyframe_uuid === keyframe.keyframe_uuid) {
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.DELETE_KEYFRAME,
data: row,
});
return false;
}
return true;
}),
})),
};
}
@@ -1,245 +0,0 @@
import {
CharacterGroup,
Clip,
ClipGroup,
ClipType,
Keyframe,
MediaItem,
QueueKeyframe,
} from "~/pages/PageEnigma/models";
import { signal } from "@preact/signals-core";
import Queue from "~/pages/PageEnigma/Queue/Queue";
import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
import * as uuid from "uuid";
export const characterGroups = signal<CharacterGroup[]>([]);
export function addCharacterAnimation({
dragItem,
characterId,
offset,
}: {
dragItem: MediaItem;
characterId: string;
offset: number;
}) {
const clip_uuid = uuid.v4();
const newClip = {
version: 1,
media_id: dragItem.media_id,
group: ClipGroup.CHARACTER,
type: ClipType.ANIMATION,
offset,
length: dragItem.length,
clip_uuid,
name: dragItem.name,
object_uuid: characterId,
} as Clip;
const oldCharacterGroups = characterGroups.value;
characterGroups.value = oldCharacterGroups.map((character) => {
if (character.id !== characterId) {
return { ...character };
}
return {
...character,
animationClips: [...character.animationClips, newClip].sort(
(clipA, clipB) => clipA.offset - clipB.offset,
),
};
});
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.ADD_CLIP,
data: newClip,
});
}
export function addCharacterAudio({
dragItem,
characterId,
offset,
}: {
dragItem: MediaItem;
characterId: string;
offset: number;
}) {
const clip_uuid = uuid.v4();
const newClip = {
version: 1,
media_id: dragItem.media_id,
group: ClipGroup.CHARACTER,
type: ClipType.AUDIO,
offset,
length: dragItem.length,
clip_uuid,
object_uuid: characterId,
name: dragItem.name,
} as Clip;
const oldCharacterGroups = characterGroups.value;
characterGroups.value = oldCharacterGroups.map((character) => {
if (character.id !== characterId) {
return { ...character };
}
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.ADD_CLIP,
data: newClip,
});
return {
...character,
lipSyncClips: [...character.lipSyncClips, newClip].sort(
(clipA, clipB) => clipA.offset - clipB.offset,
),
};
});
}
export function addCharacterKeyframe(
keyframe: QueueKeyframe,
offset: number,
addToast: (type: "error" | "warning" | "success", message: string) => void,
) {
const oldCharacterGroups = characterGroups.value;
if (
oldCharacterGroups.some((characterGroup) => {
return characterGroup.positionKeyframes.some(
(row) => row.offset === offset,
);
})
) {
addToast("warning", "There can only be one keyframe at this offset.");
return;
}
const newKeyframe = {
version: keyframe.version,
keyframe_uuid: uuid.v4(),
group: keyframe.group,
object_uuid: keyframe.object_uuid,
offset,
position: keyframe.position,
rotation: keyframe.rotation,
scale: keyframe.scale,
selected: false,
} as Keyframe;
characterGroups.value = oldCharacterGroups.map((character) => {
if (character.id !== keyframe.object_uuid) {
return { ...character };
}
return {
...character,
positionKeyframes: [...character.positionKeyframes, newKeyframe].sort(
(keyFrameA, keyframeB) => keyFrameA.offset - keyframeB.offset,
),
};
});
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.ADD_KEYFRAME,
data: newKeyframe,
});
}
export function toggleLipSyncMute(characterId: string) {
const oldCharacterGroups = characterGroups.value;
characterGroups.value = oldCharacterGroups.map((character) => {
if (character.id === characterId) {
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: character?.muted
? toEngineActions.UNMUTE
: toEngineActions.MUTE,
data: {
version: 1,
type: ClipType.AUDIO,
group: ClipGroup.CHARACTER,
object_uuid: characterId,
},
});
}
return {
...character,
muted: character.id === characterId ? !character.muted : character.muted,
};
});
}
export function selectCharacterClip(clipId: string) {
const oldCharacterGroups = characterGroups.value;
characterGroups.value = oldCharacterGroups.map((character) => ({
...character,
animationClips: character.animationClips.map((clip) => ({
...clip,
selected: clip.clip_uuid === clipId ? !clip.selected : clip.selected,
})),
positionClips: character.positionKeyframes.map((keyframe) => ({
...keyframe,
selected:
keyframe.keyframe_uuid === clipId
? !keyframe.selected
: keyframe.selected,
})),
lipSyncClips: character.lipSyncClips.map((clip) => ({
...clip,
selected: clip.clip_uuid === clipId ? !clip.selected : clip.selected,
})),
}));
}
export function deleteCharacterClip(clip: Clip) {
const oldCharacterGroups = characterGroups.value;
characterGroups.value = oldCharacterGroups.map((character) => ({
...character,
animationClips: character.animationClips.filter((row) => {
if (row.clip_uuid === clip.clip_uuid) {
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.DELETE_CLIP,
data: row,
});
return false;
}
return true;
}),
lipSyncClips: character.lipSyncClips.filter((row) => {
if (row.clip_uuid === clip.clip_uuid) {
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.DELETE_CLIP,
data: row,
});
return false;
}
return true;
}),
}));
}
export function deleteCharacterKeyframe(keyframe: Keyframe) {
const oldCharacterGroups = characterGroups.value;
characterGroups.value = oldCharacterGroups.map((character) => ({
...character,
positionKeyframes: character.positionKeyframes.filter((row) => {
if (row.keyframe_uuid === keyframe.keyframe_uuid) {
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.DELETE_KEYFRAME,
data: row,
});
return false;
}
return true;
}),
}));
}
@@ -1,8 +1,11 @@
import { MediaItem } from "~/pages/PageEnigma/models";
import { characterGroups } from "~/pages/PageEnigma/store";
import { characterGroup } from "~/pages/PageEnigma/store";
export function deleteCharacter(item: MediaItem) {
characterGroups.value = characterGroups.value.filter(
(character) => character.id !== item.object_uuid,
);
characterGroup.value = {
...characterGroup.value,
characters: characterGroup.value.characters.filter(
(character) => character.object_uuid !== item.object_uuid,
),
};
}
@@ -1,5 +1,5 @@
export * from "./addCharacter";
export * from "./characterGroups";
export * from "./characterGroup";
export * from "./deleteCharacter";
export * from "./loadCharacterData";
export * from "./updateCharacters";
@@ -1,15 +1,15 @@
import { ClipUI } from "~/pages/PageEnigma/datastructures/clips/clip_ui";
import {
CharacterGroup,
CharacterTrack,
Clip,
ClipType,
Keyframe,
} from "~/pages/PageEnigma/models";
import { characterGroups } from "~/pages/PageEnigma/store";
import { characterGroup } from "~/pages/PageEnigma/store";
function getAddCharacter(item: ClipUI) {
const existingCharacter = characterGroups.value.find(
(character) => character.id === item.object_uuid,
const existingCharacter = characterGroup.value.characters.find(
(character) => character.object_uuid === item.object_uuid,
);
if (existingCharacter) {
@@ -17,24 +17,28 @@ function getAddCharacter(item: ClipUI) {
}
const newCharacter = {
id: item.object_uuid,
object_uuid: item.object_uuid,
name: item.object_name,
media_id: item.media_id,
muted: false,
animationClips: [],
positionKeyframes: [],
lipSyncClips: [],
} as CharacterGroup;
} as CharacterTrack;
characterGroups.value = [
...characterGroups.value.filter(
(character) => character.id !== item.object_uuid,
),
newCharacter,
].sort((charA, charB) => (charA.id < charB.id ? -1 : 1));
characterGroup.value = {
...characterGroup.value,
characters: [
...characterGroup.value.characters.filter(
(character) => character.object_uuid !== item.object_uuid,
),
newCharacter,
].sort((charA, charB) => (charA.object_uuid < charB.object_uuid ? -1 : 1)),
};
return characterGroups.value.find(
(character) => character.id === item.object_uuid,
) as CharacterGroup;
return characterGroup.value.characters.find(
(character) => character.object_uuid === item.object_uuid,
) as CharacterTrack;
}
export function loadCharacterData(item: ClipUI) {
@@ -2,7 +2,7 @@ import { ClipType } from "~/pages/PageEnigma/models";
import Queue from "~/pages/PageEnigma/Queue/Queue";
import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
import { characterGroups } from "~/pages/PageEnigma/store";
import { characterGroup } from "~/pages/PageEnigma/store";
export function updateCharacters({
type,
@@ -15,80 +15,89 @@ export function updateCharacters({
length?: number;
offset: number;
}) {
const oldCharacterGroups = characterGroups.value;
const oldCharacterGroup = characterGroup.value;
if (type === ClipType.ANIMATION) {
characterGroups.value = oldCharacterGroups.map((character) => {
const newAnimationClips = [...character.animationClips];
const clipIndex = newAnimationClips.findIndex(
(row) => row.clip_uuid === id,
);
if (clipIndex === -1) {
return { ...character };
}
const clip = newAnimationClips[clipIndex];
clip.offset = offset;
clip.length = length!;
characterGroup.value = {
...oldCharacterGroup,
characters: oldCharacterGroup.characters.map((character) => {
const newAnimationClips = [...character.animationClips];
const clipIndex = newAnimationClips.findIndex(
(row) => row.clip_uuid === id,
);
if (clipIndex === -1) {
return { ...character };
}
const clip = newAnimationClips[clipIndex];
clip.offset = offset;
clip.length = length!;
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.UPDATE_CLIP,
data: clip,
});
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.UPDATE_CLIP,
data: clip,
});
return {
...character,
animationClips: newAnimationClips,
};
});
return {
...character,
animationClips: newAnimationClips,
};
}),
};
}
if (type === ClipType.TRANSFORM) {
characterGroups.value = oldCharacterGroups.map((character) => {
const newPositionKeyframes = [...character.positionKeyframes];
const keyframeIndex = newPositionKeyframes.findIndex(
(row) => row.keyframe_uuid === id,
);
if (keyframeIndex === -1) {
return { ...character };
}
const keyframe = newPositionKeyframes[keyframeIndex];
keyframe.offset = offset;
characterGroup.value = {
...oldCharacterGroup,
characters: oldCharacterGroup.characters.map((character) => {
const newPositionKeyframes = [...character.positionKeyframes];
const keyframeIndex = newPositionKeyframes.findIndex(
(row) => row.keyframe_uuid === id,
);
if (keyframeIndex === -1) {
return { ...character };
}
const keyframe = newPositionKeyframes[keyframeIndex];
keyframe.offset = offset;
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.UPDATE_KEYFRAME,
data: keyframe,
});
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.UPDATE_KEYFRAME,
data: keyframe,
});
return {
...character,
positionKeyframes: newPositionKeyframes,
};
});
return {
...character,
positionKeyframes: newPositionKeyframes,
};
}),
};
}
if (type === ClipType.AUDIO) {
characterGroups.value = oldCharacterGroups.map((character) => {
const newLipSyncClips = [...character.lipSyncClips];
const clipIndex = newLipSyncClips.findIndex(
(row) => row.clip_uuid === id,
);
if (clipIndex === -1) {
return { ...character };
}
const clip = newLipSyncClips[clipIndex];
clip.offset = offset;
clip.length = length!;
characterGroup.value = {
...oldCharacterGroup,
characters: oldCharacterGroup.characters.map((character) => {
const newLipSyncClips = [...character.lipSyncClips];
const clipIndex = newLipSyncClips.findIndex(
(row) => row.clip_uuid === id,
);
if (clipIndex === -1) {
return { ...character };
}
const clip = newLipSyncClips[clipIndex];
clip.offset = offset;
clip.length = length!;
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.UPDATE_CLIP,
data: clip,
});
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.UPDATE_CLIP,
data: clip,
});
return {
...character,
lipSyncClips: newLipSyncClips,
};
});
return {
...character,
lipSyncClips: newLipSyncClips,
};
}),
};
}
}
@@ -73,7 +73,7 @@ export function deleteObjectKeyframe(keyframe: Keyframe) {
const obj = oldObjectGroup.objects.find(
(row) => row.object_uuid === keyframe.object_uuid,
);
console.log("delete", obj, keyframe.keyframe_uuid);
if (!obj) {
return oldObjectGroup;
}
+2 -2
View File
@@ -1,5 +1,5 @@
import { signal, computed } from "@preact/signals-core";
import { characterGroups } from "~/pages/PageEnigma/store/characterGroups";
import { characterGroup } from "~/pages/PageEnigma/store/characterGroups";
import { objectGroup } from "~/pages/PageEnigma/store/objectGroup";
// timeline
@@ -9,7 +9,7 @@ export const timelineHeight = signal(0);
export const fullHeight = computed(() => {
return (
characterGroups.value.length * 268 +
characterGroup.value.characters.length * 268 +
objectGroup.value.objects.length * 60 +
300 +
96
+2 -1
View File
@@ -2,4 +2,5 @@ import { signal } from "@preact/signals-core";
export const currentTime = signal(0);
export const currentScroll = signal(0);
export const timelineScrollX = signal(0);
export const timelineScrollY = signal(0);
+10 -10
View File
@@ -2,21 +2,21 @@ import { State, Action, ACTION_TYPES } from "./types";
export function reducer(state: State, action: Action): State {
switch (action.type) {
case ACTION_TYPES.OPEN_DIALOGUE_TTS:{
return{
case ACTION_TYPES.OPEN_DIALOGUE_TTS: {
return {
...state,
diagloueTts:{
diagloueTts: {
isOpen: true,
}
}
},
};
}
case ACTION_TYPES.CLOSE_DIALOGUE_TTS:{
return{
case ACTION_TYPES.CLOSE_DIALOGUE_TTS: {
return {
...state,
diagloueTts:{
diagloueTts: {
isOpen: false,
}
}
},
};
}
case ACTION_TYPES.SHOW_CONTROLPANELS_SCENEOBJECT: {
// console.log("pay", action.payload);