This commit is contained in:
Robin Appelman 2023-11-24 22:42:25 +01:00
commit 189788a1b6
14 changed files with 599 additions and 6 deletions

106
script/edit/EditPage.tsx Normal file
View file

@ -0,0 +1,106 @@
import {Duration, formatDuration} from '../components/Duration';
import Element = JSX.Element;
import {Section} from "../components/Section";
import MultiRangeSlider from "../components/MultiSlider";
import {DemoHead} from "../header";
import {createSignal} from "solid-js";
import {downloadBuffer, edit} from "./tools";
export interface EditPageProps {
header: DemoHead;
demoData: ArrayBuffer;
setDropText: (string) => void,
name: string,
}
export const Editor = (props: EditPageProps) => {
let demoInfo: any[] | Element = [];
demoInfo = (
<div className="demo-info">
{props.header.map}
<Duration className="time"
duration={Math.floor(props.header.duration)}/>
</div>
);
const [loading, setLoading] = createSignal(false);
const [cutFrom, setCutFrom] = createSignal(0);
const [cutTo, setCutTo] = createSignal(props.header.ticks);
const intervalPerTick = props.header.duration / props.header.ticks;
const [unlockPov, setUnlockPov] = createSignal(false);
const editCb = async () => {
try {
setLoading(true);
(props.setDropText)("Editing...");
await doEdit(props.name, props.demoData, props.header, unlockPov(), cutFrom(), cutTo());
(props.setDropText)(props.name);
setLoading(false);
} catch (e) {
(props.setDropText)("Error: " + e.toString());
setLoading(false);
}
}
return (
<div>
{demoInfo}
<Section title="Unlock camera">
<ul key={1}>
<li key={1}>Unlocks the camera in pov demos, allowing free movement as if it were an stv demo.</li>
<li key={2}>When the player respawns the camera will be moved.</li>
<li key={3}>As pov demos only contain data near the player, far away players might freeze, teleport or otherwise behave weirdly.</li>
</ul>
<p key={2}>
<input type="checkbox" id="pov-unlock"
checked={unlockPov() ? "checked" : null}
onChange={() => {
setUnlockPov(!unlockPov())
}}/>
<label htmlFor="pov-unlock">Unlock camara for pov demo</label>
</p>
</Section>
<Section title="Cut demo">
<ul key={1}>
<li key={1}>Cuts the demo file to the selected tick range.</li>
<li key={2}>Cutting demos is experimental, resulting demo files might crash, have broken animations or have other issues.</li>
<li key={3}>Changing the specific cut range can sometimes work around issues with broken demos.</li>
</ul>
<p key={2}>
<MultiRangeSlider
min={0}
max={props.header.ticks}
onChange={(min, max) => {
if (min !== cutFrom() || max !== cutTo()) {
setCutFrom(min);
setCutTo(max);
}
}}
labelFn={(ticks) => formatDuration(ticks * intervalPerTick)}
/>
</p>
</Section>
<Section title="Process">
<p key={1}>
<button onClick={editCb}
className="pure-button pure-button-primary"
disabled={loading() ? "disabled" : null}>
{() => loading() ? 'Processing...' : 'Edit'}
</button>
</p>
</Section>
</div>
);
}
async function doEdit(name: string, data: ArrayBuffer, header: DemoHead, unlockPov: boolean, from: number, to: number) {
let options = {
unlock_pov: unlockPov,
cut: (from > 0 || to < header.ticks) ? {from, to} : undefined,
}
console.log(options);
const edited = await edit(data, options);
downloadBuffer(edited, name.replace('.dem', '_edited.dem'));
}

45
script/edit/EditWorker.ts Normal file
View file

@ -0,0 +1,45 @@
import init from "@demostf/edit";
import {count_ticks, edit_js} from "@demostf/edit";
declare function postMessage(message: any, transfer?: any[]): void;
function getCacheBuster(): string {
const url = self.location.href;
return url.substring(url.indexOf('?'));
}
/**
* @global postMessage
* @param event
*/
onmessage = async (event: MessageEvent) => {
await init(`/tf-demo-editor.wasm${getCacheBuster()}`);
if (event.data.count) {
const buffer: ArrayBuffer = event.data.buffer;
const bytes = new Uint8Array(buffer);
try {
const ticks = count_ticks(bytes);
postMessage({
ticks: ticks
});
} catch(e) {
postMessage({
error: e
});
}
} else {
const buffer: ArrayBuffer = event.data.buffer;
const options = event.data.options;
const bytes = new Uint8Array(buffer);
try {
const edited = edit_js(bytes, options);
postMessage({
buffer: edited.buffer
}, [edited.buffer]);
} catch (e) {
postMessage({
error: e
});
}
}
};

59
script/edit/tools.ts Normal file
View file

@ -0,0 +1,59 @@
export interface EditOptions {
unlock_pov: boolean,
cut?: TickRange,
}
export interface TickRange {
from: number,
to: number,
}
function getCacheBuster(): string {
const url = document.querySelector('script[src*="editor"]').attributes.src.value;
return url.substring("/editor.js".length);
}
export function edit(data: ArrayBuffer, options: EditOptions): Promise<ArrayBuffer> {
return new Promise((resolve, reject) => {
const worker = new Worker(`/edit-worker.js${getCacheBuster()}`);
worker.postMessage({
buffer: data,
options
});
worker.onmessage = (event) => {
if (event.data.error) {
reject(event.data.error);
return;
} else if (event.data.buffer) {
resolve(event.data.buffer);
}
}
});
}
export function count_ticks(data: ArrayBuffer): Promise<number> {
return new Promise((resolve, reject) => {
const worker = new Worker(`/edit-worker.js${getCacheBuster()}`);
worker.postMessage({
buffer: data,
count: true
});
worker.onmessage = (event) => {
if (event.data.error) {
reject(event.data.error);
return;
} else if (event.data.ticks) {
resolve(event.data.ticks);
}
}
});
}
export function downloadBuffer(arrayBuffer: ArrayBuffer, fileName: string) {
const a = document.createElement('a')
a.href = URL.createObjectURL(new Blob(
[arrayBuffer],
))
a.download = fileName
a.click()
}