diff --git a/src/restapi.ts b/src/restapi.ts index de40742..27ddb13 100644 --- a/src/restapi.ts +++ b/src/restapi.ts @@ -1,9 +1,10 @@ -import { Context, Router } from './deps.ts'; +import { Context, Router, TextLineStream, readableStreamFromReader } from './deps.ts'; import { FileMetaData, FileService } from './services/mod.ts'; import { readerFromStreamReader } from './deps.ts'; import { asyncIterableToArray } from './utils.ts'; -import { createMockNext } from '../vendor/deno.land/x/oak@v12.1.0/testing.ts'; import { Status } from "../vendor/deno.land/std@0.178.0/http/http_status.ts"; +import { WaveFrontServiceImpl } from "./services/wavefrontService.ts"; +import { Vector3D, WaveFrontService } from './services/interfaces.ts'; type Handler = (ctx: Context) => void; @@ -11,6 +12,7 @@ class FileDTO { readonly path: string; readonly id: string; readonly contentSize: number; + readonly fileInfoURL: string; readonly contentURL: string; constructor(path: string, id: string, size: number){ @@ -18,6 +20,7 @@ class FileDTO { this.id = id; this.contentSize = size; this.contentURL = `/v1/content/${id}/raw`; + this.fileInfoURL = `/v1/files/${id}`; } static fromFileMetaData(fm: FileMetaData): FileDTO{ @@ -53,12 +56,9 @@ function renameFileHandlerFactory(fileService: FileService): Handler { const fileId: string = untypedCtx.params.fileId; const reqBody = await ctx.request.body().value; const newPath = reqBody.path; - console.log("> " + reqBody) if(!reqBody.path){ return; } - console.log(">>> " + fileId); - console.log(">>> " + newPath); const ret = await fileService.renameById(fileId, newPath); if(ret === null){ ctx.response.status = Status.NotFound; @@ -98,6 +98,43 @@ function getDownloadFileHandlerFactory(fileService: FileService): Handler { }; } +function getDownloadTransformedFileHandlerFactory(fileService: FileService, waveFrontService: WaveFrontService): Handler { + function getScaleVector(params: any): Vector3D | null { + const x = parseFloat(params.get('scaleX') || 1); + const y = parseFloat(params.get('scaleY') || 1); + const z = parseFloat(params.get('scaleZ') || 1); + const ret: Vector3D = [x, y, z]; + return ret.filter(isNaN).length === 0 ? ret : null + } + + function getTranslateVector(params: any): Vector3D | null{ + const x = parseFloat(params.get('translateX') || 0); + const y = parseFloat(params.get('translateY') || 0); + const z = parseFloat(params.get('translateZ') || 0); + const ret: Vector3D = [x, y, z]; + return ret.filter(isNaN).length === 0 ? ret : null + } + + return async (ctx: Context) => { + const untypedCtx: any = ctx; + const fileId = untypedCtx.params.fileId; + const reader = await fileService.getContentById(fileId); + if (reader == null) { + ctx.response.status = 404; + return; + } + + const scale = getScaleVector(untypedCtx.request.url.searchParams); + const translate = getTranslateVector(untypedCtx.request.url.searchParams); + if(scale === null || translate === null){ + ctx.response.status = Status.BadRequest; + return; + } + ctx.response.status = 200; + ctx.response.body = waveFrontService.scaleAndTranslate(reader, scale, translate); + }; +} + function deleteFileHandlerFactory(fileService: FileService): Handler { return async (ctx: Context) => { const untypedCtx: any = ctx; @@ -111,7 +148,7 @@ function deleteFileHandlerFactory(fileService: FileService): Handler { }; } -export function buildAPI(router: Router, fileService: FileService): Router { +export function buildAPI(router: Router, fileService: FileService, waveFrontService: WaveFrontService): Router { router.get('/files', listFileHandlerFactory(fileService)); router.get('/files/:fileId', getFileHandlerFactory(fileService)); router.delete('/files/:fileId', deleteFileHandlerFactory(fileService)); @@ -119,6 +156,8 @@ export function buildAPI(router: Router, fileService: FileService): Router { router.post('/files', createFileHandlerFactory(fileService)); router.get('/content/:fileId/raw', getDownloadFileHandlerFactory(fileService)); + router.get('/content/:fileId/transform', getDownloadTransformedFileHandlerFactory(fileService, waveFrontService)); + return router; } diff --git a/src/server.ts b/src/server.ts index e488224..0f36e2d 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,15 +1,17 @@ import { Application, Router } from './deps.ts'; import { buildAPI } from './restapi.ts'; import { DenoKVFileService } from './services/mod.ts'; +import { WaveFrontServiceImpl } from './services/wavefrontService.ts'; async function main() { const app = new Application(); const kv = await Deno.openKv(); const fileService = new DenoKVFileService(kv); + const waveFrontService = new WaveFrontServiceImpl(); const router = new Router({ prefix: '/v1' }); app.use( - buildAPI(router, fileService).routes(), + buildAPI(router, fileService, waveFrontService).routes(), ); await app.listen({ port: 9000 }); diff --git a/src/services/interfaces.ts b/src/services/interfaces.ts index fbd96a1..e7edbc5 100644 --- a/src/services/interfaces.ts +++ b/src/services/interfaces.ts @@ -39,3 +39,11 @@ export interface BlobService { getBlob(index: string): Deno.Reader; getNumberOfChunks(index: string): Promise; } + + +export type Vector3D = [x: number, y: number, z: number]; + + +export interface WaveFrontService { + scaleAndTranslate(objFile: Deno.Reader, scale: Vector3D, translate: Vector3D): Deno.Reader +} \ No newline at end of file diff --git a/src/services/wavefrontService.ts b/src/services/wavefrontService.ts new file mode 100644 index 0000000..5714ae3 --- /dev/null +++ b/src/services/wavefrontService.ts @@ -0,0 +1,44 @@ +import { TextLineStream, readableStreamFromReader, readerFromIterable } from "../deps.ts"; +import { WaveFrontService, Vector3D } from "./interfaces.ts"; + +export type LineTransformer = (line: string) => string + +const vectorLineRegexExp = /^v (?-?\d+(\.\d+)?) (?-?\d+(\.\d+)?) (?-?\d+(\.\d+)?)\s*(#.*)?$/; + +export class WaveFrontServiceImpl implements WaveFrontService { + private async *joinLineIterator(lines: AsyncIterable, fn: LineTransformer): AsyncIterable{ + const encoder = new TextEncoder(); + for await(const line of lines){ + yield encoder.encode(fn(line) + '\n') + } + } + + getLineTransformer(scale: Vector3D, translate: Vector3D): LineTransformer { + return (line: string) => { + const match = line.match(vectorLineRegexExp); + if(match === null){ + return line; + } + const x = parseFloat(match.groups!.x); + const y = parseFloat(match.groups!.y); + const z = parseFloat(match.groups!.z); + return `v ${x * scale[0] + translate[0]} ${y * scale[1] + translate[1]} ${z * scale[2] + translate[2]}` + } + } + + transformLine(objFile: Deno.Reader, transformer: LineTransformer): Deno.Reader{ + const lines = readableStreamFromReader(objFile) + .pipeThrough(new TextDecoderStream()) + .pipeThrough(new TextLineStream()); + return readerFromIterable(this.joinLineIterator(lines, transformer)); + } + + scaleAndTranslate(objFile: Deno.Reader, scale: Vector3D, translate: Vector3D): Deno.Reader { + const fn = this.getLineTransformer(scale, translate); + return this.transformLine(objFile, fn); + } + + + + +} \ No newline at end of file diff --git a/tests/waveFrontService_test.ts b/tests/waveFrontService_test.ts new file mode 100644 index 0000000..058afaa --- /dev/null +++ b/tests/waveFrontService_test.ts @@ -0,0 +1,59 @@ +import { StringReader, readAll } from "../src/deps.ts"; +import { Vector3D } from "../src/services/interfaces.ts"; +import { WaveFrontServiceImpl } from "../src/services/wavefrontService.ts"; +import { assertEquals } from 'https://deno.land/std/testing/asserts.ts'; + +Deno.test('WaveFrontServiceImpl happy path ', async (t) => { + const waveFrontService = new WaveFrontServiceImpl(); + + + await t.step("test trivial transform", async () => { + const content = "line 1\nline2\nline3\n" + const reader = new StringReader(content); + + const ret = waveFrontService.transformLine(reader, (s) => `hello ${s}`); + const expt = "hello line 1\nhello line2\nhello line3\n" + assertEquals(new TextDecoder().decode(await readAll(ret)), expt); + }); + + await t.step("test trivial transform (no final new line in original file) `", async () => { + const content = "line 1\nline2\nline3" + const reader = new StringReader(content); + + const ret = waveFrontService.transformLine(reader, (s) => `hello ${s}`); + const expt = "hello line 1\nhello line2\nhello line3\n" + assertEquals(new TextDecoder().decode(await readAll(ret)), expt); + }); +}); + +Deno.test('WaveFrontServiceImpl getLineTransformer', async (t) => { + const waveFrontService = new WaveFrontServiceImpl(); + + await t.step("trivial cases", () => { + const fn = waveFrontService.getLineTransformer([1, 2, 3], [10, 11, 12]) + + assertEquals(fn(''), ''); + assertEquals(fn('hello'), 'hello'); + assertEquals(fn('hello 1 2 3'), 'hello 1 2 3'); + }); + + await t.step("transform cases", () => { + const scale: Vector3D = [5, 6, 7]; + const translate: Vector3D = [10, 11, 12]; + const fn = waveFrontService.getLineTransformer(scale, translate); + const expt = (x: number, y: number, z: number) => `v ${x * scale[0] + translate[0]} ${y * scale[1] + translate[1]} ${z * scale[2] + translate[2]}`; + const values = [-1, 2, 3, 10, 5.23, 34.56, -12.75 ] + for(const x of values){ + for(const y of values){ + for(const z of values){ + assertEquals(fn(`v ${x} ${y} ${z}`), expt(x, y, z)); + assertEquals(fn(`v ${x} ${y} ${z} `), expt(x, y, z)); + assertEquals(fn(`v ${x} ${y} ${z} # foo bar`), expt(x, y, z)); + + } + } + } + }); + + +}) \ No newline at end of file