mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-04 17:38:10 +00:00
supress error for process env, added vscode setting to gitignore segrgate enums (#369) Co-authored-by: Christopher Oliphant <RALifeCoach@users.noreply.github.com> Refactor storybook (#370) Co-authored-by: Christopher Oliphant <RALifeCoach@users.noreply.github.com> button dropdown docs (#372) storybook doc for button dropdown component refactor side panel tabs (#371) Co-authored-by: Christopher Oliphant <RALifeCoach@users.noreply.github.com> fix enums (#373) Co-authored-by: Christopher Oliphant <RALifeCoach@users.noreply.github.com> Refactoring Auth Context to Signals (#374) - file restructure, store -> signals - rewrite AuthContext to signals, and get rid of AuthContext - modify all files that need to use AuthContext to use the authentication Signal - the complex login persistency logic is now just a persistLogin function ran at root! - fetchers uses the auth signal directly to get the sessionToken - components no longer useSignals for these fetchers, hence decoupled from the auth signals !! components and fetchers that are definitely subjected to refactor does not receive the useSignal decoupling treatment Co-authored-by: wil <wil@storyteller.ai> Refactoring Toaster Context to a Toaster Component + Signal (#378) - Created the Toaster Component - Created the Toasts Signal - Now no useContext is needed for Toast Poping, nor passing AddToast around - Simply import addToast from the Signals and you can pop a toast from anywhere in the codebase Co-authored-by: wil <wil@storyteller.ai> Refactoring AppUi Context (partial: delete unused, editorLoader becomes signal) (#379) - dialogueTTS is unused, hence deleted - view side-by-side mode is unused, hence deleted - timeline height in this context was already made into signal, hence deleted - editorLoader in this context is now made into a page level signal, hence deleted - components and dispatcher of the editorLoader are modified to use signal instead - app level reducer is no longer used, tugged into context folder to avoid confusion, will be deleted when all attributes in AppUi context is refactored into signals Co-authored-by: wil <wil@storyteller.ai> Refactor control panel (#391) - control panel - simplified and improved - Please also test it. It operates slightly differently. For example, if the user types an invalid character, the character is ignored. --------- Co-authored-by: Christopher Oliphant <RALifeCoach@users.noreply.github.com> Refactoring App Level Components - Components that are in the /app/component folder are split into reusable and signaled - reusable are in theory signal independent - signaled are dependent on signals (and maybe a use-once component) - some app level components has page level enums or signals, those are bumped to app level now - all components in the module directory are moved into the component directories depending on whether it's reusable or signaled - code changes in are mostly just import changes to get re-target to get the correct files - discovered a small bug in FilterButtons and is fixed - whether a component need more work, it is commented on the index file that import them into the grand collection of components Refactor - Signals Part 2 - store is renamed to signals in PageEditor for clarity - ObjectControlPanel now uses a Signal instead of the AppUiContext - Moved EditorContext to Page Level, all context are page level only now - editor is modified to read and use signals directly (for now), this includes: -- the ObjectPanel signal -- the Scene signal -- the Authentication signal - and hence, editor now has a constructor doesn't require any arguments - editor also has a simple initialization - AppUiContext is retired completely and deleted --------- Co-authored-by: wil <wil@storyteller.ai> Co-authored-by: Christopher Oliphant <RALifeCoach@users.noreply.github.com>
341 lines
10 KiB
TypeScript
341 lines
10 KiB
TypeScript
import { ChangeEvent, useContext, useEffect, useId, useState } from "react";
|
|
import { useSignals } from "@preact/signals-react/runtime";
|
|
import { Transition } from "@headlessui/react";
|
|
import {
|
|
faChevronDown,
|
|
faChevronUp,
|
|
faCube,
|
|
faLock,
|
|
faLockOpen,
|
|
faTrash,
|
|
} from "@fortawesome/pro-solid-svg-icons";
|
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
|
|
import {
|
|
objectPanel as objectPanelSignals,
|
|
sidePanelHeight,
|
|
} from "../../signals";
|
|
import { EngineContext } from "~/pages/PageEnigma/contexts/EngineContext";
|
|
import { Button, H5, InputVector } from "~/components";
|
|
|
|
import { QueueNames } from "../../Queue/QueueNames";
|
|
import Queue from "~/pages/PageEnigma/Queue/Queue";
|
|
import { toTimelineActions } from "../../Queue/toTimelineActions";
|
|
import { QueueKeyframe } from "~/pages/PageEnigma/models";
|
|
import { editorState } from "~/pages/PageEnigma/signals/engine";
|
|
import { twMerge } from "tailwind-merge";
|
|
import { EditorStates } from "~/pages/PageEnigma/enums";
|
|
import { sanitize } from "./utils/sanitize";
|
|
import { objectMismatch } from "~/pages/PageEnigma/comps/ControlPanelSceneObject/utils/objectMismatch";
|
|
import { XYZ } from "~/pages/PageEnigma/datastructures/common";
|
|
|
|
// TODO this will be useful later to fix the bug on leading zeros
|
|
// const formatNumber = (input: string): number => {
|
|
// // Convert the input string to a number to remove leading zeros
|
|
// const num = parseFloat(input);
|
|
// // Convert the number back to a string with at least two decimal places
|
|
// const str = num.toFixed(2);
|
|
// return parseFloat(str);
|
|
// };
|
|
|
|
const defaultAxises: Record<string, string> = {
|
|
x: "0",
|
|
y: "0",
|
|
z: "0",
|
|
};
|
|
|
|
export const ControlPanelSceneObject = () => {
|
|
useSignals();
|
|
const { isShowing, currentObject } = objectPanelSignals;
|
|
|
|
const editorEngine = useContext(EngineContext);
|
|
|
|
// const [appUiState] = useContext(AppUiContext);
|
|
const [isCollapsed, setIsCollapsed] = useState(false);
|
|
|
|
// local translation axises to allow for validation before handing them to the engine
|
|
const [localPosition, setLocalPosition] = useState(defaultAxises);
|
|
const [localRotation, setLocalRotation] = useState(defaultAxises);
|
|
const [localScale, setLocalScale] = useState(defaultAxises);
|
|
|
|
// used to update engine object
|
|
const [inputsUpdated, setInputsUpdated] = useState(false);
|
|
|
|
const [locked, setLocked] = useState(false);
|
|
|
|
const [color, setColor] = useState("#ffffff");
|
|
|
|
console.log("color", color);
|
|
|
|
const colorInputId = useId();
|
|
|
|
const toggleCollapse = () => {
|
|
setIsCollapsed(!isCollapsed);
|
|
};
|
|
|
|
const currentSceneObject = currentObject.value;
|
|
const position = currentSceneObject?.objectVectors?.position;
|
|
const rotation = currentSceneObject?.objectVectors?.rotation;
|
|
const scale = currentSceneObject?.objectVectors?.scale;
|
|
|
|
function localToEngine(xyz: Record<string, string>) {
|
|
return {
|
|
x: parseFloat(xyz.x),
|
|
y: parseFloat(xyz.y),
|
|
z: parseFloat(xyz.z),
|
|
};
|
|
}
|
|
function engineToLocal(xyz: XYZ) {
|
|
return {
|
|
x: xyz.x.toString(),
|
|
y: xyz.y.toString(),
|
|
z: xyz.z.toString(),
|
|
};
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (!inputsUpdated || !editorEngine) {
|
|
return;
|
|
}
|
|
|
|
setInputsUpdated(false);
|
|
editorEngine.setSelectedObject(
|
|
localToEngine(localPosition),
|
|
localToEngine(localRotation),
|
|
localToEngine(localScale),
|
|
);
|
|
}, [inputsUpdated, localPosition, localRotation, localScale, editorEngine]);
|
|
|
|
useEffect(() => {
|
|
if (!editorEngine || !currentSceneObject) {
|
|
return;
|
|
}
|
|
|
|
console.log("44");
|
|
|
|
const vectors = currentSceneObject.objectVectors;
|
|
|
|
// local state relies on strings
|
|
setLocalPosition(engineToLocal(vectors.position));
|
|
setLocalRotation(engineToLocal(vectors.rotation));
|
|
setLocalScale(engineToLocal(vectors.scale));
|
|
|
|
setLocked(editorEngine.isObjectLocked(editorEngine?.selected?.uuid || ""));
|
|
setColor(editorEngine?.selected?.userData.color);
|
|
}, [currentSceneObject, editorEngine]);
|
|
|
|
if (!currentSceneObject || editorState.value === EditorStates.PREVIEW) {
|
|
return null;
|
|
}
|
|
|
|
const isInvalid = (xyz: Record<string, string>) =>
|
|
Object.values(xyz).some((value) => {
|
|
if (value === "" || value === "-" || value === ".") {
|
|
return true;
|
|
}
|
|
return !/^-?[0-9]*(.[0-9]*)?$/.test(value);
|
|
});
|
|
|
|
const toggleLock = () => {
|
|
setLocked((lockState: boolean) => !lockState);
|
|
editorEngine?.lockUnlockObject(editorEngine?.selected?.uuid || "");
|
|
};
|
|
|
|
const handlePositionChange = (xyz: Record<string, string>) => {
|
|
if (isInvalid(xyz)) {
|
|
setLocalPosition(xyz);
|
|
return;
|
|
}
|
|
const cleanXyz = sanitize(xyz);
|
|
if (objectMismatch(localPosition, cleanXyz)) {
|
|
setInputsUpdated(true);
|
|
}
|
|
setLocalPosition(xyz);
|
|
};
|
|
|
|
const handleRotationChange = (xyz: Record<string, string>) => {
|
|
if (isInvalid(xyz)) {
|
|
setLocalRotation(xyz);
|
|
return;
|
|
}
|
|
const cleanXyz = sanitize(xyz);
|
|
if (objectMismatch(localRotation, cleanXyz)) {
|
|
setInputsUpdated(true);
|
|
}
|
|
setLocalRotation(xyz);
|
|
};
|
|
|
|
const handleScaleChange = (xyz: Record<string, string>) => {
|
|
if (isInvalid(xyz)) {
|
|
setLocalScale(xyz);
|
|
return;
|
|
}
|
|
const cleanXyz = sanitize(xyz);
|
|
if (objectMismatch(localScale, cleanXyz)) {
|
|
setInputsUpdated(true);
|
|
}
|
|
setLocalScale(xyz);
|
|
};
|
|
|
|
const handleOnAddKeyFrame = () => {
|
|
if (currentSceneObject) {
|
|
if (position == null || rotation == null || scale == null) {
|
|
return;
|
|
}
|
|
|
|
for (const key in editorEngine?.timeline.characters) {
|
|
const element = editorEngine?.timeline.characters[key];
|
|
if (key == currentSceneObject.object_uuid) {
|
|
currentSceneObject.group = element;
|
|
break;
|
|
}
|
|
}
|
|
|
|
Queue.publish({
|
|
queueName: QueueNames.TO_TIMELINE,
|
|
action: toTimelineActions.ADD_KEYFRAME,
|
|
data: {
|
|
group: currentSceneObject.group,
|
|
object_uuid: currentSceneObject.object_uuid,
|
|
object_name: currentSceneObject.object_name,
|
|
version: 1,
|
|
|
|
position: currentSceneObject.objectVectors.position,
|
|
rotation: currentSceneObject.objectVectors.rotation,
|
|
scale: currentSceneObject.objectVectors.scale,
|
|
} as QueueKeyframe,
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleDeleteObject = () => {
|
|
editorEngine?.deleteObject(currentSceneObject.object_uuid);
|
|
};
|
|
|
|
const getScale = () => {
|
|
const height = sidePanelHeight.value; // Ensure this is the correct way to get the height
|
|
return height < 603 ? height / 603 : 1;
|
|
};
|
|
|
|
return (
|
|
<Transition
|
|
show={isShowing.value}
|
|
className={twMerge(
|
|
"absolute bottom-0 right-0 m-3 flex h-fit w-56 origin-bottom-right flex-col gap-2 rounded-lg border border-ui-panel-border bg-ui-panel p-3.5 text-white shadow-lg",
|
|
)}
|
|
enter="transition-opacity duration-150"
|
|
enterFrom="opacity-0"
|
|
enterTo="opacity-100"
|
|
leave="transition-opacity duration-150"
|
|
leaveFrom="opacity-100"
|
|
leaveTo="opacity-0"
|
|
style={{ transform: `scale(${getScale()})` }}
|
|
>
|
|
<div className="mb-1 flex justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<FontAwesomeIcon icon={faCube} />
|
|
<p className="max-w-36 truncate font-semibold">
|
|
{currentSceneObject.object_name}
|
|
</p>
|
|
</div>
|
|
<FontAwesomeIcon
|
|
icon={isCollapsed ? faChevronUp : faChevronDown}
|
|
onClick={toggleCollapse}
|
|
className="cursor-pointer opacity-75 transition-opacity duration-100 ease-in-out hover:opacity-50"
|
|
/>
|
|
</div>
|
|
|
|
<Transition
|
|
show={!isCollapsed}
|
|
enter="transition-all duration-200 ease-in-out"
|
|
enterFrom="opacity-0 max-h-0"
|
|
enterTo="opacity-100 max-h-96"
|
|
leave="transition-all duration-200 ease-in-out"
|
|
leaveFrom="opacity-100 max-h-96"
|
|
leaveTo="opacity-0 max-h-0"
|
|
className={"flex flex-col gap-2 overflow-y-auto"}
|
|
>
|
|
<Button
|
|
variant="secondary"
|
|
icon={locked ? faLock : faLockOpen}
|
|
onClick={toggleLock}
|
|
className={
|
|
locked ? "bg-brand-primary/20 hover:bg-brand-primary/40" : ""
|
|
}
|
|
>
|
|
{locked ? "Unlock" : "Lock"} object
|
|
</Button>
|
|
|
|
<div className="flex flex-col gap-1">
|
|
<H5>Color</H5>
|
|
<input
|
|
className="h-0 w-0 cursor-pointer opacity-0"
|
|
id={colorInputId}
|
|
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
|
editorEngine?.setColor(
|
|
editorEngine?.selected?.uuid || "",
|
|
e.target.value,
|
|
);
|
|
setColor(e.target.value);
|
|
}}
|
|
type="color"
|
|
value={color}
|
|
/>
|
|
<Button
|
|
className="cursor-pointer p-3.5"
|
|
htmlFor={colorInputId}
|
|
style={{
|
|
backgroundColor: color,
|
|
}}
|
|
></Button>
|
|
</div>
|
|
<div className="flex flex-col gap-1">
|
|
<H5>Location</H5>
|
|
<InputVector
|
|
x={localPosition.x.toString()}
|
|
y={localPosition.y.toString()}
|
|
z={localPosition.z.toString()}
|
|
onChange={handlePositionChange}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-1">
|
|
<H5>Rotation</H5>
|
|
<InputVector
|
|
x={localRotation.x.toString()}
|
|
y={localRotation.y.toString()}
|
|
z={localRotation.z.toString()}
|
|
onChange={handleRotationChange}
|
|
increment={1}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-1">
|
|
<H5>Scale</H5>
|
|
<InputVector
|
|
x={localScale.x.toString()}
|
|
y={localScale.y.toString()}
|
|
z={localScale.z.toString()}
|
|
onChange={handleScaleChange}
|
|
/>
|
|
</div>
|
|
</Transition>
|
|
|
|
<div className="mt-2 flex gap-1.5">
|
|
<Button
|
|
variant="secondary"
|
|
className="grow"
|
|
onClick={handleOnAddKeyFrame}
|
|
>
|
|
Add Keyframe (K)
|
|
</Button>
|
|
<Button
|
|
variant="secondary"
|
|
icon={faTrash}
|
|
onClick={handleDeleteObject}
|
|
/>
|
|
</div>
|
|
</Transition>
|
|
);
|
|
};
|