diff --git a/frontend/src/features/authoring/AuthoringToolPage.jsx b/frontend/src/features/authoring/AuthoringToolPage.jsx index 025afca6..d0a57df1 100644 --- a/frontend/src/features/authoring/AuthoringToolPage.jsx +++ b/frontend/src/features/authoring/AuthoringToolPage.jsx @@ -47,20 +47,9 @@ export default function AuthoringToolPage() { return () => clearInterval(autosave); }, [sceneId]); - // if the active scene was deleted, switch to the first available scene - useEffect(() => { - if (!sceneId || !scenes) return; - if (!scenes.find((s) => s._id === sceneId)) { - const next = scenes[0]; - if (next) replace(next); - } - }, [scenes]); - useEffect(() => { const activeScene = localStorage.getItem(`${scenarioId}:activeScene`); - const found = activeScene - ? scenes.find((s) => s._id === activeScene) - : null; + const found = scenes.find((s) => s._id === activeScene); const target = found ?? scenes[0]; if (target) replace(target); diff --git a/frontend/src/features/authoring/canvas/Overlay.tsx b/frontend/src/features/authoring/canvas/Overlay.tsx index 6d8cfcdb..f553ad57 100644 --- a/frontend/src/features/authoring/canvas/Overlay.tsx +++ b/frontend/src/features/authoring/canvas/Overlay.tsx @@ -10,6 +10,7 @@ import SpeechHandles from "./handles/SpeechHandles"; import Rectangle from "./Rectangle"; import useEditorStore from "../stores/editor"; import useVisualScene from "../stores/visual"; +import { getSelectedComponentBounds } from "../handlers/pointer/pointer"; const componentMap: Record> = { speech: Speech, @@ -23,25 +24,36 @@ function resolve(type: Component["type"], bounds: Bounds) { return ; } +function ResolveHandles({ + type, + isMultiSelect, +}: { + type: string; + isMultiSelect: boolean; +}) { + if (isMultiSelect) return ; + switch (type) { + case "speech": + return ; + case "line": + return ; + default: + return ; + } +} + function Overlay() { - const selected = useEditorStore((state) => state.selected)!; - const bounds = useEditorStore((state) => state.mutationBounds); - const scene = useVisualScene((scene) => scene.components); - const mode = useEditorStore((scene) => scene.mode); - const createType = useEditorStore((scene) => scene.createType); + const selected = useEditorStore((s) => s.selected); + const mode = useEditorStore((s) => s.mode); + const createType = useEditorStore((s) => s.createType); + const mutationBounds = useEditorStore((s) => s.mutationBounds); + const components = useVisualScene((s) => s.components); - const component = scene[selected]; + const primaryComponent = + selected.length === 0 ? null : components[selected[0]]; - function ResolveHandles() { - switch (component.type) { - case "speech": - return ; - case "line": - return ; - default: - return ; - } - } + const bounds = getSelectedComponentBounds(); + const verts = bounds.verts; return ( - {component && ( + {primaryComponent && ( <> - + 1} + /> )} {mode.includes("mutation") && - resolve(component?.type ?? createType, bounds)} + resolve(primaryComponent?.type ?? createType, mutationBounds as Bounds)} ); } diff --git a/frontend/src/features/authoring/canvas/handles/ConstrainedHandle.tsx b/frontend/src/features/authoring/canvas/handles/ConstrainedHandle.tsx index 91c74877..56849610 100644 --- a/frontend/src/features/authoring/canvas/handles/ConstrainedHandle.tsx +++ b/frontend/src/features/authoring/canvas/handles/ConstrainedHandle.tsx @@ -1,6 +1,6 @@ import { getBoxCenter, rotate } from "../../util"; import useEditorStore from "../../stores/editor"; -import useVisualScene from "../../stores/visual"; +import { getSelectedComponentBounds } from "../../handlers/pointer/pointer"; interface Props { x: number; @@ -8,19 +8,24 @@ interface Props { } const ResizeHandle = ({ x, y }: Props) => { - const selected = useEditorStore((state) => state.selected)!; - const mode = useEditorStore((state) => state.mode); - const scene = useVisualScene((scene) => scene.components); + const mode = useEditorStore((s) => s.mode); - const bounds = scene[selected].bounds; - const verts = bounds.verts; + const componentBounds = getSelectedComponentBounds(); + const componentVerts = componentBounds.verts; + const componentRotation = componentBounds.rotation; let point = { - x: x === 0.5 ? (verts[0].x + verts[1].x) / 2 : verts[x].x, - y: y === 0.5 ? (verts[0].y + verts[1].y) / 2 : verts[y].y, + x: + x === 0.5 + ? (componentVerts[0].x + componentVerts[1].x) / 2 + : componentVerts[x].x, + y: + y === 0.5 + ? (componentVerts[0].y + componentVerts[1].y) / 2 + : componentVerts[y].y, }; - point = rotate(point, getBoxCenter(verts), bounds.rotation); + point = rotate(point, getBoxCenter(componentVerts), componentRotation); return ( { - const selected = useEditorStore((state) => state.selected)!; - const scene = useVisualScene((scene) => scene.components); - const mode = useEditorStore((state) => state.mode); + const mode = useEditorStore((s) => s.mode); - const bounds = scene[selected].bounds; - const center = getBoxCenter(bounds.verts); + const componentBounds = getSelectedComponentBounds(); + const componentVerts = componentBounds.verts; + const componentRotation = componentBounds.rotation; - const y = Math.min(bounds.verts[0].y, bounds.verts[1].y); + const componentCenter = getBoxCenter(componentBounds.verts); - const initial = rotate({ x: center.x, y }, center, bounds.rotation); - const point = rotate({ x: center.x, y: y - 40 }, center, bounds.rotation); + const y = Math.min(componentVerts[0].y, componentVerts[1].y); + + const initial = rotate( + { x: componentCenter.x, y }, + componentCenter, + componentRotation + ); + const point = rotate( + { x: componentCenter.x, y: y - 40 }, + componentCenter, + componentRotation + ); return ( { if (obj.type) { - setSelected(parseComponent(obj)); + newSelection.push(parseComponent(obj)); } else { + //! IMPORTANT I dont think this will ever run because ever copied component has a type const component = structuredClone(defaults["textbox"]); component.document = structuredClone(obj); - setSelected(add(component)); + newSelection.push(add(component)); } - } else if (text) { - const doc = plainToDoc(text); - const component = structuredClone(defaults["textbox"]); - component.document = structuredClone(doc); - setSelected(add(component)); - } + }); + setSelected(newSelection); } } -function addToClipboard(e: ClipboardEvent, selected: string) { +function addToClipboard(e: ClipboardEvent, selected: string[]) { const { mode, selection } = useEditorStore.getState(); - if (mode.includes("text")) { - if (!selection.end) return; - const { text, doc } = getSelectionContent(selected, selection); - e.clipboardData?.setData("text/plain", text); - e.clipboardData?.setData("application/component", JSON.stringify(doc)); - } else { + const plainTextChunks: string[] = []; + const components: Component[] = []; + selected.forEach((id: string) => { + if (mode.includes("text")) { + if (!selection.end) return; + + const { text, doc } = getSelectionContent(id, selection); + if (text) plainTextChunks.push(text); + if (doc) components.push(doc); + } else { + components.push(getComponent(id)); + if (getComponent(id).type === "textbox") { + const text = getDocumentText(id); + if (text) plainTextChunks.push(text); + } + } + }); + + if (plainTextChunks.length > 0) { + e.clipboardData?.setData("text/plain", plainTextChunks.join("\n")); + } + if (components.length > 0) { e.clipboardData?.setData( "application/component", - stringifyComponent(selected) || "" + JSON.stringify(components) ); - if (getComponent(selected).type === "textbox") { - const text = getDocumentText(selected); - e.clipboardData?.setData("text/plain", text); - } } } diff --git a/frontend/src/features/authoring/handlers/keyboard/keyboard.ts b/frontend/src/features/authoring/handlers/keyboard/keyboard.ts index f623c0c8..5cd01592 100644 --- a/frontend/src/features/authoring/handlers/keyboard/keyboard.ts +++ b/frontend/src/features/authoring/handlers/keyboard/keyboard.ts @@ -1,4 +1,4 @@ -import { redo, undo } from "../../scene/history"; +import { handleUndoRedo } from "../../scene/history"; import { bringForward, bringToFront, @@ -18,6 +18,7 @@ export function handleGlobal(e: KeyboardEvent) { const { selected } = useEditorStore.getState(); // don't want to interfere with input elements + const target = e.target as HTMLElement; if (target.tagName === "INPUT" || target.tagName === "TEXTAREA") return; @@ -30,12 +31,12 @@ function handleCtrlOperations(e: KeyboardEvent) { const { selected, setSelected } = useEditorStore.getState(); // TODO ADD e.key === "a" for all components or create new function for cmd - if (e.key === "z") undo(); - else if (e.key === "y") redo(); + if (e.key === "z") handleUndoRedo(true); + else if (e.key === "y") handleUndoRedo(false); else if (e.key === "d" && selected) { e.preventDefault(); - const id = duplicateComponent(selected); - setSelected(id); + const ids = duplicateComponent(selected); + setSelected(ids); } else if (e.key === "ArrowUp" && selected) { if (e.shiftKey) bringToFront(selected); else bringForward(selected); @@ -45,12 +46,12 @@ function handleCtrlOperations(e: KeyboardEvent) { } } -function handleComponentOperations(e: KeyboardEvent, selected: string) { +function handleComponentOperations(e: KeyboardEvent, selected: string[]) { const { setSelected } = useEditorStore.getState(); if (e.key === "Backspace") { + setSelected([]); remove(selected); - setSelected(null); } else if (e.key === "ArrowUp") { modifyComponentProp(selected, "bounds.verts", (prev: Vec2[]) => translate(prev, { x: 0, y: -5 }) diff --git a/frontend/src/features/authoring/handlers/keyboard/text.ts b/frontend/src/features/authoring/handlers/keyboard/text.ts index 3af194d1..d5ad1695 100644 --- a/frontend/src/features/authoring/handlers/keyboard/text.ts +++ b/frontend/src/features/authoring/handlers/keyboard/text.ts @@ -21,15 +21,15 @@ import type { VisualBlock, VisualSelection } from "../../text/types"; export function handleTextMode(e: KeyboardEvent) { const { selected } = useEditorStore.getState(); - if (!selected) return; + if (!selected || selected.length != 1) return; if ((e.metaKey || e.ctrlKey) && e.key == "a") { e.preventDefault(); - handleSelectAll(selected); + handleSelectAll(selected[0]); } else if (e.key.startsWith("Arrow") || ["Home", "End"].includes(e.key)) { - handleNavigation(e, selected); + handleNavigation(e, selected[0]); } else { - handleEditing(e, selected); + handleEditing(e, selected[0]); } } @@ -75,17 +75,17 @@ function handleEditing(e: KeyboardEvent, selected: string) { // insert character at cursor const newCursor = end ? insertSelection(selected, selection, e.key) - : insertChar(selected, start, e.key); + : insertChar([selected], start, e.key); setSelection({ start: newCursor, end: null }); } else if (e.key === "Backspace") { // delete character before cursor const newCursor = !end - ? deleteChar(selected, start) - : deleteSelection(selected, selection); + ? deleteChar([selected], start) + : deleteSelection([selected], selection); setSelection({ start: newCursor, end: null }); } else if (e.key === "Enter") { // create a new block at cursor - const newCursor = createBlock(selected, start); + const newCursor = createBlock([selected], start); setSelection({ start: newCursor, end }); } else if (e.key === "Escape") { // clear current selection diff --git a/frontend/src/features/authoring/handlers/pointer/ComponentContext.tsx b/frontend/src/features/authoring/handlers/pointer/ComponentContext.tsx index 384dba30..be1eadff 100644 --- a/frontend/src/features/authoring/handlers/pointer/ComponentContext.tsx +++ b/frontend/src/features/authoring/handlers/pointer/ComponentContext.tsx @@ -15,47 +15,47 @@ import { } from "../../scene/operations/component"; import useEditorStore from "../../stores/editor"; -const ComponentMenu = ({ id }: { id: string }) => { - function removeAndDeselect(id: string) { - remove(id); - useEditorStore.getState().setSelected(null); +const ComponentMenu = (ids: string[]) => { + function removeAndDeselect(selectedIds: string[]) { + remove(selectedIds); + useEditorStore.getState().setSelected([]); } return (