import { useContext, useEffect, useId, useState } from "react"; 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 { AppUiContext } from "~/contexts/AppUiContext"; import { EngineContext } from "~/contexts/EngineContext"; import { Button, H5, InputVector } from "~/components"; import { XYZ } from "../../datastructures/common"; 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, EditorStates } from "~/pages/PageEnigma/store/engine"; import { sidePanelHeight } from "../../store"; import { twMerge } from "tailwind-merge"; // import { current } from "tailwindcss/colors"; // 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 = { x: "0", y: "0", z: "0", }; // interface Axises { // x: number | string; // y: number | string; // z: number | string; // } // console.log(formatNumber("000123.4567")); // Outputs: "123.46" // console.log(formatNumber("000123.4")); // Outputs: "123.40" // console.log(formatNumber("000123")); // Outputs: "123.00" // console.log(formatNumber("0000.00")); // Outputs: "0.00" // console.log(formatNumber("0000.000001")); // Outputs: "0.00" export const ControlPanelSceneObject = () => { const editorEngine = useContext(EngineContext); const [appUiState, dispatchAppUiState] = useContext(AppUiContext); const [isCollapsed, setIsCollapsed] = useState(false); // obj token to track what is selected const [initializedObj, initializedObjSet] = useState(""); // local translation axises to allow for validation before handing them to the engine const [localPosition, localPositionSet] = useState(defaultAxises); const [localRotation, localRotationSet] = useState(defaultAxises); const [localScale, localScaleSet] = useState(defaultAxises); // used to update engine object const [inputsUpdated, inputsUpdatedSet] = useState(false); const [inputsFocused, inputsFocusedSet] = useState(false); const [locked, lockedSet] = useState(false); const [color, colorSet] = useState("#ffffff"); const colorInputId = useId(); // clears leading and trailing zeros const sanitizeNumericInput = (input: string): number => { const regex = /(\-?\d+)(\.\d+)?/; const matches = input.match(regex) || []; const integerPart = matches[1]; const decimalPart = matches[2]; const decimal = decimalPart !== undefined ? parseFloat(decimalPart) : 0.0; if (integerPart === undefined) { return decimal; } const integer = parseInt(integerPart); if (decimalPart === undefined) { return integer; } if (Number(input) > 0) { return integer + decimal; } else { return integer - decimal; } }; // runs sanitizeNumericInput on each axis, creating a new object of sanitized values const sanitize = (xyz: { [i: string]: string }) => { return Object.keys(xyz).reduce((obj, currentKey) => { return { ...obj, [currentKey]: sanitizeNumericInput(xyz[currentKey].toString()), }; }, {}); }; // checks if *ANY* number within a translation object is NaN and therefore will not be sent to the engine const isValid = (xyz: { [i: string]: string }) => !Object.values(xyz).some((n) => { return isNaN(n); }); const positionSanitized = sanitize(localPosition); const rotationSanitized = sanitize(localRotation); const scaleSanitized = sanitize(localScale); const positionValid = isValid(positionSanitized); const rotationValid = isValid(rotationSanitized); const scaleValid = isValid(scaleSanitized); const toggleCollapse = () => { setIsCollapsed(!isCollapsed); }; const position = appUiState.controlPanel.currentSceneObject?.objectVectors?.position; const rotation = appUiState.controlPanel.currentSceneObject?.objectVectors?.rotation; const scale = appUiState.controlPanel.currentSceneObject?.objectVectors?.scale; const currentSceneObject = appUiState.controlPanel.currentSceneObject; useEffect(() => { const isCurrentObj = (editorEngine?.selected?.uuid || "") === initializedObj || appUiState.controlPanel.currentSceneObject.object_uuid === initializedObj; // TODO this causes a subtle bug because it renders way too many times. if (!appUiState.controlPanel.currentSceneObject) { return; } const vectors = appUiState.controlPanel.currentSceneObject.objectVectors; const numsToStrings = (inputObj: { [i: string]: number }) => Object.keys(inputObj).reduce( (obj, currentKey) => ({ ...obj, [currentKey]: Number(inputObj[currentKey]).toString(), }), defaultAxises, ); const objMatch = ( inputObj: { [i: string]: number }, refObj: { [i: string]: number }, ) => Object.keys(inputObj).some( (currentKey: string) => inputObj[currentKey] !== refObj[currentKey], ); const positionMismatch = objMatch(vectors.position, positionSanitized); const rotationMismatch = objMatch(vectors.rotation, rotationSanitized); const scaleMismatch = objMatch(vectors.scale, scaleSanitized); if ( (editorEngine?.switchPreviewToggle === false && appUiState.controlPanel.isShowing && // if the current object isn't the cached object: update local !isCurrentObj) || // if the inputs are not in focus, and local state does not match the selected engine object values: update local (!inputsFocused && (positionMismatch || rotationMismatch || scaleMismatch)) ) { // update cached object initializedObjSet(appUiState.controlPanel.currentSceneObject.object_uuid); // local state relies on strings localPositionSet(numsToStrings(vectors.position)); localRotationSet(numsToStrings(vectors.rotation)); localScaleSet(numsToStrings(vectors.scale)); lockedSet( editorEngine.isObjectLocked(editorEngine?.selected?.uuid || ""), ); colorSet(editorEngine?.selected?.userData.color); } else if (!appUiState.controlPanel.isShowing && isCurrentObj) { initializedObjSet(""); localPositionSet(defaultAxises); localRotationSet(defaultAxises); localScaleSet(defaultAxises); lockedSet(false); colorSet("#ffffff"); } // updating engine if values originate from the inputs and are valid if (inputsUpdated && positionValid && rotationValid && scaleValid) { inputsUpdatedSet(false); editorEngine?.setSelectedObject( positionSanitized, rotationSanitized, scaleSanitized, ); } }, [ appUiState.controlPanel, editorEngine, initializedObj, inputsFocused, inputsUpdated, positionSanitized, positionValid, rotationSanitized, rotationValid, scaleSanitized, scaleValid, ]); if ( !appUiState.controlPanel.currentSceneObject || editorState.value === EditorStates.PREVIEW ) { return null; } const toggleLock = () => { lockedSet((lockState: boolean) => !lockState); editorEngine.lockUnlockObject(editorEngine?.selected?.uuid || ""); }; const handlePositionChange = (xyz: XYZ) => { localPositionSet(xyz); inputsUpdatedSet(true); // onChange functions no longer update the engine directly. Commented out for future reference. // if (!currentSceneObject) { // console.log("Missing Scene Object Position"); // return; // } // dispatchAppUiState({ // type: ACTION_TYPES.UPDATE_CONTROLPANELS_SCENEOBJECT, // payload: { // group: currentSceneObject.group, // object_uuid: currentSceneObject.object_uuid, // object_name: currentSceneObject.object_name, // version: currentSceneObject.version, // objectVectors: { // ...appUiState.controlPanel.currentSceneObject.objectVectors, // position: { // ...stringsToNums(xyz), // }, // }, // }, // }); }; const handleRotationChange = (xyz: XYZ) => { localRotationSet(xyz); inputsUpdatedSet(true); // if (appUiState) { // if (currentSceneObject == null) { // console.log("Missing Scene Object Rotation"); // return; // } // dispatchAppUiState({ // type: ACTION_TYPES.UPDATE_CONTROLPANELS_SCENEOBJECT, // payload: { // group: currentSceneObject.group, // object_uuid: currentSceneObject.object_uuid, // object_name: currentSceneObject.object_name, // version: currentSceneObject.version, // objectVectors: { // ...appUiState.controlPanel.currentSceneObject.objectVectors, // rotation: { // ...stringsToNums(xyz), // }, // }, // }, // }); // } }; const handleScaleChange = (xyz: XYZ) => { localScaleSet(xyz); inputsUpdatedSet(true); // if (appUiState) { // if (currentSceneObject == null) { // console.log("Missing Scene Object Scale"); // return; // } // dispatchAppUiState({ // type: ACTION_TYPES.UPDATE_CONTROLPANELS_SCENEOBJECT, // payload: { // group: currentSceneObject.group, // object_uuid: currentSceneObject.object_uuid, // object_name: currentSceneObject.object_name, // version: currentSceneObject.version, // objectVectors: { // ...appUiState.controlPanel.currentSceneObject.objectVectors, // scale: { // ...stringsToNums(xyz), // }, // }, // }, // }); // } }; const onFocus = () => { inputsFocusedSet(true); }; const onPositionBlur = () => { inputsFocusedSet(false); if (positionValid) { localPositionSet(positionSanitized); } }; const onRotationBlur = () => { inputsFocusedSet(false); if (rotationValid) { localRotationSet(rotationSanitized); } }; const onScaleBlur = () => { inputsFocusedSet(false); if (scaleValid) { localScaleSet(scaleSanitized); } }; const handleOnAddKeyFrame = () => { if (appUiState) { if ( position == null || rotation == null || scale == null || currentSceneObject == 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 (

{appUiState.controlPanel.currentSceneObject.object_name}

Color
{ editorEngine.setColor( editorEngine?.selected?.uuid || "", e.target.value, ); colorSet(e.target.value); }} type="color" value={color} />
Location
Rotation
Scale
); };