Something went wrong. Try again.
Starship Simulator Controls thoriumsim.com
Something went wrong. Try again.
6.6 kB · 213 lines
TSX
at develop
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214import path from "node:path";import { Writable } from "node:stream";
import { pubsub } from "@thorium/.server/init/pubsub";import { t } from "@thorium/.server/init/t";import { thoriumContext } from "@thorium/utils/.server/context";import { getShipSystem } from "@thorium/utils/.server/ship/getShipSystem";import { shipPubsubFilter } from "@thorium/utils/.server/shipPubsubFilter";import { Entity } from "@thorium/utils/ecs";import { produce } from "immer";import PDFDocument from "pdfkit";import z from "zod";
class BlobStream extends Writable { #chunks: Uint8Array<ArrayBuffer>[] = []; #blob: Blob | null = null; length = 0;
_write(chunk: any, encoding: BufferEncoding, callback: (error?: Error | null) => void): void { if (!(chunk instanceof Uint8Array)) { chunk = new Uint8Array(chunk); }
this.length += chunk.length; this.#chunks.push(chunk); callback(); } toBlob(type?: string) { type = type || "application/octet-stream"; if (!this.#blob) { this.#blob = new Blob(this.#chunks, { type: type, });
this.#chunks = []; // free memory }
if (this.#blob.type !== type) this.#blob = new Blob([this.#blob], { type: type });
return this.#blob; }}
export const documents = t.router({ get: t.procedure .input(z.object({ shipId: z.number() })) .filter(shipPubsubFilter) .autoPublish(["isDocument"], (entity) => entity.components.isDocument ? { shipId: entity.components.isDocument?.shipId } : null, ) .request(({ input, ctx }) => { const documents: { id: number; name: string; filePath: string; annotations: [number, number, number][][][]; }[] = []; for (const document of ctx.ecs.componentCache.get("isDocument") || []) { if (document.components.isDocument?.shipId === input.shipId) { documents.push({ id: document.id, name: document.components.identity?.name || "Document", filePath: document.components.isDocument.filePath, annotations: document.components.isDocument.annotations, }); } } return documents; }), addDocument: t.procedure .input( z.object({ shipId: z.number(), name: z.string(), file: z.union([z.instanceof(File), z.string()]), }), ) .send(async ({ ctx, input }) => { if (!ctx.flight) throw new Error("Flight has not started."); const document = new Entity(); document.addComponent("isDocument", { shipId: input.shipId, filePath: typeof input.file === "string" ? input.file : await ctx.uploadFile.call(ctx.flight, input.file), }); document.addComponent("identity", { name: input.name, }); ctx.ecs.addEntity(document);
pubsub.publish.documents.get({ shipId: input.shipId });
return { id: document.id }; }), removeDocument: t.procedure.input(z.object({ documentId: z.number() })).send(({ input, ctx }) => { const document = ctx.ecs.getEntityById(input.documentId); if (!document) return; const shipId = document.components.isDocument?.shipId || -1; ctx.ecs.removeEntity(document); pubsub.publish.documents.get({ shipId }); }), addAnnotation: t.procedure .input( z.object({ documentId: z.number(), annotation: z.array(z.tuple([z.number(), z.number(), z.number()])), page: z.number(), }), ) .send(({ input, ctx }) => { const document = ctx.ecs.getEntityById(input.documentId); if (!document) return; const shipId = document.components.isDocument?.shipId || -1;
const annotations = produce(document.components.isDocument?.annotations || [], (draft) => { if (!draft[input.page]) draft[input.page] = []; const page = draft[input.page]; page.push(input.annotation); }); document.updateComponent("isDocument", { annotations });
pubsub.publish.documents.get({ shipId }); }), clearAnnotations: t.procedure .input(z.object({ documentId: z.number(), page: z.number() })) .send(({ input, ctx }) => { const document = ctx.ecs.getEntityById(input.documentId); if (!document) return; const shipId = document.components.isDocument?.shipId || -1;
const annotations = produce(document.components.isDocument?.annotations || [], (draft) => { draft[input.page] = []; }); document.updateComponent("isDocument", { annotations });
pubsub.publish.documents.get({ shipId }); }), undoAnnotations: t.procedure .input(z.object({ documentId: z.number(), page: z.number() })) .send(({ input, ctx }) => { const document = ctx.ecs.getEntityById(input.documentId); if (!document) return; const shipId = document.components.isDocument?.shipId || -1;
const annotations = produce(document.components.isDocument?.annotations || [], (draft) => { draft[input.page].pop(); }); document.updateComponent("isDocument", { annotations });
pubsub.publish.documents.get({ shipId }); }), renderPdf: t.procedure .input( z.object({ shipId: z.number(), fontName: z.string().nullable(), name: z.string(), heading: z.string(), message: z.string(), }), ) .meta({ action: () => ({ message: { type: "textarea" } }) }) .output(z.object({ documentId: z.number() })) .send(async ({ ctx, input }) => { if (!ctx.flight) throw new Error("Flight has not started.");
const longRangeComm = getShipSystem(ctx.ecs, { shipId: input.shipId, systemType: "longRangeComm", }); const cypher = longRangeComm.components.isLongRangeComm?.cyphers.find( (c) => c.name === input.fontName, ); if (!cypher) throw new Error("Cypher font not found.");
const file = await new Promise<Blob>((resolve) => { const doc = new PDFDocument({ size: "LETTER" }); const thoriumPath = thoriumContext.getStore()!.thoriumPath; doc.registerFont(cypher.name, path.join(thoriumPath, cypher.font)); const stream = doc.pipe(new BlobStream()); stream.on("finish", () => { resolve(stream.toBlob("application/pdf")); }); // and some justified text wrapped into columns doc .font("Courier", 32) .text(input.heading, { align: "center" }) .moveDown() .font(cypher.name, 24) .text(input.message.toLowerCase(), { lineGap: 32, ellipsis: true, });
doc.end(); });
const document = new Entity(); document.addComponent("isDocument", { shipId: input.shipId, filePath: await ctx.uploadFile.call(ctx.flight, file, `${input.name}.pdf`), }); document.addComponent("identity", { name: input.name, }); ctx.ecs.addEntity(document);
pubsub.publish.documents.get({ shipId: input.shipId }); return { documentId: document.id }; }),});