Skip to content
16 changes: 2 additions & 14 deletions frontend/src/features/authoring/AuthoringToolPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -76,8 +65,7 @@ export default function AuthoringToolPage() {
}, []);

function playScenario() {
const startScene = sceneId ? `?startScene=${sceneId}` : "";
window.open(`/play/${scenarioId}${startScene}`, "_blank");
window.open(`/play/${scenarioId}`, "_blank");
}

function goToGroups() {
Expand Down
57 changes: 36 additions & 21 deletions frontend/src/features/authoring/canvas/Overlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, React.FC<any>> = {
speech: Speech,
Expand All @@ -23,46 +24,60 @@ function resolve(type: Component["type"], bounds: Bounds) {
return <Fc bounds={bounds} fill="none" stroke="green" strokeWidth={3} />;
}

function ResolveHandles({
type,
isMultiSelect,
}: {
type: string;
isMultiSelect: boolean;
}) {
if (isMultiSelect) return <DragHandles />;
switch (type) {
case "speech":
return <SpeechHandles />;
case "line":
return <LineHandles />;
default:
return <DragHandles />;
}
}

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 <SpeechHandles />;
case "line":
return <LineHandles />;
default:
return <DragHandles />;
}
}
const bounds = getSelectedComponentBounds();
const verts = bounds.verts;
Comment thread
K1mmyn marked this conversation as resolved.

return (
<svg
id="overlay"
className="w-full h-full absolute pointer-events-none"
viewBox={`-50 -50 ${1920 + 50 * 2} ${1080 + 50 * 2}`}
>
{component && (
{primaryComponent && (
<>
<Rectangle
bounds={component.bounds}
rotationOrigin={getBoxCenter(component.bounds.verts)}
bounds={bounds}
rotationOrigin={getBoxCenter(verts)}
fill="none"
stroke="blue"
strokeWidth={3}
/>
<ResolveHandles />
<ResolveHandles
type={primaryComponent?.type}
isMultiSelect={selected.length > 1}
/>
</>
)}
{mode.includes("mutation") &&
resolve(component?.type ?? createType, bounds)}
resolve(primaryComponent?.type ?? createType, mutationBounds as Bounds)}
</svg>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,31 @@
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;
y: number;
}

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 (
<g
Expand Down
27 changes: 18 additions & 9 deletions frontend/src/features/authoring/canvas/handles/RotationHandle.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
import { getBoxCenter, rotate } from "../../util";
import useEditorStore from "../../stores/editor";
import useVisualScene from "../../stores/visual";
import { getSelectedComponentBounds } from "../../handlers/pointer/pointer";

const RotationHandle = () => {
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 (
<g
Expand Down
106 changes: 68 additions & 38 deletions frontend/src/features/authoring/handlers/keyboard/clipboard.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import {
defaults,
parseComponent,
stringifyComponent,
} from "../../scene/operations/component";
import { defaults, parseComponent } from "../../scene/operations/component";
import { add, remove } from "../../scene/operations/modifiers";
import {
getDocumentText,
Expand All @@ -12,7 +8,7 @@ import {
import { getComponent } from "../../scene/scene";
import useEditorStore from "../../stores/editor";
import { syncVisualCursor } from "../../text/cursor";
import type { ModelDocument } from "../../types";
import type { Component, ModelDocument } from "../../types";

function plainToDoc(text: string) {
const plainBlocks = text.split("\n");
Expand Down Expand Up @@ -40,64 +36,98 @@ export function cut(e: ClipboardEvent) {

addToClipboard(e, selected);
remove(selected);
setSelected(null);
setSelected([]);
}

export function paste(e: ClipboardEvent) {
e.preventDefault();
const { mode, selected, selection, setSelected, setSelection } =
useEditorStore.getState();

const app = e.clipboardData?.getData("application/component");
const text = e.clipboardData?.getData("text/plain");
const components = e.clipboardData?.getData("application/component");
const plainTexts = e.clipboardData?.getData("text/plain");

if (selected && mode.includes("text")) {
if (app) {
const obj = JSON.parse(app);
const doc = obj.type === "textbox" ? obj.document : obj;
const cursor = mergeDocs(selected, selection.start!, doc);
//! IMPORTANT This one I got gpt to write lowkey not to sure if its good
if (!selection.start) return;
let cursor = selection.start;

if (components) {
let parsed: unknown;
try {
parsed = JSON.parse(components);
} catch {
return;
}
const items = Array.isArray(parsed) ? parsed : [parsed];

for (const item of items) {
const doc = item.type === "textbox" ? item.document : item;
cursor = mergeDocs(selected, cursor, doc);
}
setSelection({ start: cursor, end: null });
} else if (text) {
const doc = plainToDoc(text) as ModelDocument;
const cursor = mergeDocs(selected, selection.start!, doc);
} else if (plainTexts) {
const doc = plainToDoc(plainTexts) as ModelDocument;
cursor = mergeDocs(selected, cursor, doc);
setSelection({ start: cursor, end: null });
}
syncVisualCursor();
} else {
if (app) {
const obj = JSON.parse(app);
if (!components) return;
setSelected([]);

let newSelection: string[] = [];

let parsed: unknown;
try {
parsed = JSON.parse(components);
} catch {
return;
}
const items = Array.isArray(parsed) ? parsed : [parsed];

items.forEach((obj) => {
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);
}
}
}
15 changes: 8 additions & 7 deletions frontend/src/features/authoring/handlers/keyboard/keyboard.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { redo, undo } from "../../scene/history";
import { handleUndoRedo } from "../../scene/history";
import {
bringForward,
bringToFront,
Expand All @@ -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;

Expand All @@ -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);
Expand All @@ -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 })
Expand Down
Loading