Three stores, three importers and three pickers that differ only in their type are one of each (#85)
Refactoring round one: HttpFontStore/HttpSoundStore/HttpTextureStore collapse into a generic HttpFileStore over a FileStore contract; fontImport/soundImport/textureImport share assetImport with an ImportKind per sort; ImagePicker/FontPicker/SoundPicker sit on FilePicker and describe only their row. The group/comment drag logic leaves GraphCanvas for annotationDrag.ts (with its own unit test), declaredRows.ts replaces the duplicated row build in particles.ts and shaders.ts, and dead code in font/sound/compile/emission/transform/actions is gone. README: asset list, check count 958, the two-job CI, the Scenes wiki page. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
15bdfdf07a
commit
cc6db5f350
42 files changed
+1375
-1650
No files matched your search
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* What the server answers for an upload: the content-addressed name it stored the bytes under
|
||||
* (`{sha256}.{ext}`, see `isTextureFile`/`isFontFile`/`isSoundFile`) and the caption kept beside it.
|
||||
*/
|
||||
export interface UploadedFile {
|
||||
file: string;
|
||||
/** The name the file was uploaded under, '' when the server knows none. Never a path. */
|
||||
name: string;
|
||||
bytes: number;
|
||||
}
|
||||
|
||||
/** A file the server holds, whether any project references it or not. */
|
||||
export interface StoredFile extends UploadedFile {
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Raw bytes rather than a multipart form: the server derives name and format from the file itself,
|
||||
* so there is nothing else to send. `type` only fills the Content-Type header – the server trusts
|
||||
* the bytes, not the label – and uploading the same bytes twice yields the same file, the hash of
|
||||
* its content. `name` is the caption kept beside it; uploading the same bytes under a new name
|
||||
* renames what the pickers show.
|
||||
*/
|
||||
export interface FileStore<U extends UploadedFile = UploadedFile, S extends StoredFile = StoredFile> {
|
||||
upload(bytes: Blob | ArrayBuffer, type: string, name?: string): Promise<U>;
|
||||
list(): Promise<S[]>;
|
||||
delete(file: string): Promise<void>;
|
||||
}
|
||||
@@ -1,26 +1,9 @@
|
||||
export interface UploadedFont {
|
||||
/** Content-addressed name in `data/fonts/`: `{sha256}.{ttf|otf|woff|woff2}` (see `isFontFile`). */
|
||||
file: string;
|
||||
/** The name the file was uploaded under, '' when the server knows none. Never a path. */
|
||||
name: string;
|
||||
bytes: number;
|
||||
}
|
||||
|
||||
/** A typeface the server holds, whether any project references it or not. */
|
||||
export interface StoredFont {
|
||||
file: string;
|
||||
name: string;
|
||||
bytes: number;
|
||||
updatedAt: string;
|
||||
}
|
||||
import type { FileStore, StoredFile, UploadedFile } from './FileStore';
|
||||
|
||||
/**
|
||||
* Raw bytes, like an image upload: the server derives name and format from the file itself. Unlike
|
||||
* an image a font is never re-encoded on the way, so what the server stores is what was picked.
|
||||
* Stored in `data/fonts/` as `{sha256}.{ttf|otf|woff|woff2}`. Unlike an image a font is never
|
||||
* re-encoded on the way, so what the server stores is what was picked.
|
||||
*/
|
||||
export interface FontStore {
|
||||
/** `name` is kept beside the file as its caption; the stored file is always the hash. */
|
||||
upload(bytes: Blob | ArrayBuffer, type: string, name?: string): Promise<UploadedFont>;
|
||||
list(): Promise<StoredFont[]>;
|
||||
delete(file: string): Promise<void>;
|
||||
}
|
||||
export type UploadedFont = UploadedFile;
|
||||
export type StoredFont = StoredFile;
|
||||
export type FontStore = FileStore<UploadedFont, StoredFont>;
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { FileStore, StoredFile, UploadedFile } from './FileStore';
|
||||
import { apiRequest } from './http';
|
||||
|
||||
/** The API's three file collections, one route each; textures, fonts and sounds differ in nothing else. */
|
||||
export type FileRoute = 'textures' | 'fonts' | 'sounds';
|
||||
|
||||
export class HttpFileStore<
|
||||
U extends UploadedFile = UploadedFile,
|
||||
S extends StoredFile = StoredFile,
|
||||
> implements FileStore<U, S> {
|
||||
constructor(
|
||||
private readonly endpoint: string,
|
||||
private readonly route: FileRoute,
|
||||
) {}
|
||||
|
||||
async upload(bytes: Blob | ArrayBuffer, type: string, name = ''): Promise<U> {
|
||||
return apiRequest<U>(this.endpoint, 'POST', this.route, {
|
||||
headers: { 'Content-Type': type },
|
||||
body: bytes,
|
||||
query: { name },
|
||||
});
|
||||
}
|
||||
|
||||
async list(): Promise<S[]> {
|
||||
return apiRequest<S[]>(this.endpoint, 'GET', this.route, {});
|
||||
}
|
||||
|
||||
async delete(file: string): Promise<void> {
|
||||
await apiRequest<void>(this.endpoint, 'DELETE', `${this.route}/${encodeURIComponent(file)}`, {});
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import type { FontStore, StoredFont, UploadedFont } from './FontStore';
|
||||
import { apiRequest } from './http';
|
||||
|
||||
export class HttpFontStore implements FontStore {
|
||||
constructor(private readonly endpoint: string) {}
|
||||
|
||||
async upload(bytes: Blob | ArrayBuffer, type: string, name = ''): Promise<UploadedFont> {
|
||||
return this.request<UploadedFont>('POST', 'fonts', { bytes, type, name });
|
||||
}
|
||||
|
||||
async list(): Promise<StoredFont[]> {
|
||||
return this.request<StoredFont[]>('GET', 'fonts');
|
||||
}
|
||||
|
||||
async delete(file: string): Promise<void> {
|
||||
await this.request<void>('DELETE', `fonts/${encodeURIComponent(file)}`);
|
||||
}
|
||||
|
||||
private async request<T>(
|
||||
method: string,
|
||||
route: string,
|
||||
payload?: { bytes: Blob | ArrayBuffer; type: string; name: string },
|
||||
): Promise<T> {
|
||||
return apiRequest<T>(this.endpoint, method, route, {
|
||||
headers: payload ? { 'Content-Type': payload.type } : {},
|
||||
body: payload?.bytes,
|
||||
query: payload ? { name: payload.name } : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import { apiRequest } from './http';
|
||||
import type { SoundStore, StoredSound, UploadedSound } from './SoundStore';
|
||||
|
||||
export class HttpSoundStore implements SoundStore {
|
||||
constructor(private readonly endpoint: string) {}
|
||||
|
||||
async upload(bytes: Blob | ArrayBuffer, type: string, name = ''): Promise<UploadedSound> {
|
||||
return this.request<UploadedSound>('POST', 'sounds', { bytes, type, name });
|
||||
}
|
||||
|
||||
async list(): Promise<StoredSound[]> {
|
||||
return this.request<StoredSound[]>('GET', 'sounds');
|
||||
}
|
||||
|
||||
async delete(file: string): Promise<void> {
|
||||
await this.request<void>('DELETE', `sounds/${encodeURIComponent(file)}`);
|
||||
}
|
||||
|
||||
private async request<T>(
|
||||
method: string,
|
||||
route: string,
|
||||
payload?: { bytes: Blob | ArrayBuffer; type: string; name: string },
|
||||
): Promise<T> {
|
||||
return apiRequest<T>(this.endpoint, method, route, {
|
||||
headers: payload ? { 'Content-Type': payload.type } : {},
|
||||
body: payload?.bytes,
|
||||
query: payload ? { name: payload.name } : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import { apiRequest } from './http';
|
||||
import type { StoredTexture, TextureStore, UploadedTexture } from './TextureStore';
|
||||
|
||||
export class HttpTextureStore implements TextureStore {
|
||||
constructor(private readonly endpoint: string) {}
|
||||
|
||||
async upload(bytes: Blob | ArrayBuffer, type: string, name = ''): Promise<UploadedTexture> {
|
||||
return this.request<UploadedTexture>('POST', 'textures', { bytes, type, name });
|
||||
}
|
||||
|
||||
async list(): Promise<StoredTexture[]> {
|
||||
return this.request<StoredTexture[]>('GET', 'textures');
|
||||
}
|
||||
|
||||
async delete(file: string): Promise<void> {
|
||||
await this.request<void>('DELETE', `textures/${encodeURIComponent(file)}`);
|
||||
}
|
||||
|
||||
private async request<T>(
|
||||
method: string,
|
||||
route: string,
|
||||
payload?: { bytes: Blob | ArrayBuffer; type: string; name: string },
|
||||
): Promise<T> {
|
||||
return apiRequest<T>(this.endpoint, method, route, {
|
||||
headers: payload ? { 'Content-Type': payload.type } : {},
|
||||
body: payload?.bytes,
|
||||
query: payload ? { name: payload.name } : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,9 @@
|
||||
export interface UploadedSound {
|
||||
/** Content-addressed name in `data/sounds/`: `{sha256}.{mp3|ogg|wav|m4a}` (see `isSoundFile`). */
|
||||
file: string;
|
||||
/** The name the file was uploaded under, '' when the server knows none. Never a path. */
|
||||
name: string;
|
||||
bytes: number;
|
||||
}
|
||||
|
||||
/** A sound the server holds, whether any project references it or not. */
|
||||
export interface StoredSound {
|
||||
file: string;
|
||||
name: string;
|
||||
bytes: number;
|
||||
updatedAt: string;
|
||||
}
|
||||
import type { FileStore, StoredFile, UploadedFile } from './FileStore';
|
||||
|
||||
/**
|
||||
* Raw bytes, like a font upload: the server derives name and format from the file itself. A sound
|
||||
* is never re-encoded on the way, so what the server stores is what was picked – and how long it
|
||||
* Stored in `data/sounds/` as `{sha256}.{mp3|ogg|wav|m4a}`, never re-encoded. How long a sound
|
||||
* plays is nowhere in here, because only a decoder knows that.
|
||||
*/
|
||||
export interface SoundStore {
|
||||
/** `name` is kept beside the file as its caption; the stored file is always the hash. */
|
||||
upload(bytes: Blob | ArrayBuffer, type: string, name?: string): Promise<UploadedSound>;
|
||||
list(): Promise<StoredSound[]>;
|
||||
delete(file: string): Promise<void>;
|
||||
}
|
||||
export type UploadedSound = UploadedFile;
|
||||
export type StoredSound = StoredFile;
|
||||
export type SoundStore = FileStore<UploadedSound, StoredSound>;
|
||||
@@ -1,38 +1,18 @@
|
||||
export interface UploadedTexture {
|
||||
/** Content-addressed name in `data/textures/`: `{sha256}.{png|jpg|webp|gif}` (see `isTextureFile`). */
|
||||
file: string;
|
||||
/**
|
||||
* The name the file was uploaded under, '' when the server knows none. A picture may be
|
||||
* re-encoded on the way, so this can be a `.png` name on a file stored as `.webp`.
|
||||
*/
|
||||
name: string;
|
||||
import type { FileStore, StoredFile, UploadedFile } from './FileStore';
|
||||
|
||||
/**
|
||||
* Stored in `data/textures/` as `{sha256}.{png|jpg|webp|gif}`. A picture may be re-encoded on the
|
||||
* way, so `name` can be a `.png` name on a file stored as `.webp`.
|
||||
*/
|
||||
export interface UploadedTexture extends UploadedFile {
|
||||
width: number;
|
||||
height: number;
|
||||
bytes: number;
|
||||
}
|
||||
|
||||
/** A file the server holds, whether any project references it or not. */
|
||||
export interface StoredTexture {
|
||||
file: string;
|
||||
name: string;
|
||||
export interface StoredTexture extends StoredFile {
|
||||
/** 0 × 0 when the server could not read the header; such a file cannot become an asset. */
|
||||
width: number;
|
||||
height: number;
|
||||
bytes: number;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* An upload is raw bytes rather than a multipart form: name, size and format are derived from the
|
||||
* image itself on the server, so there is nothing else to send.
|
||||
*/
|
||||
export interface TextureStore {
|
||||
/**
|
||||
* `type` only fills the Content-Type header – the server trusts the bytes, not the label – and
|
||||
* uploading the same picture twice yields the same file, the hash of its content. `name` is the
|
||||
* caption kept beside it; uploading the same bytes under a new name renames what pickers show.
|
||||
*/
|
||||
upload(bytes: Blob | ArrayBuffer, type: string, name?: string): Promise<UploadedTexture>;
|
||||
list(): Promise<StoredTexture[]>;
|
||||
delete(file: string): Promise<void>;
|
||||
}
|
||||
export type TextureStore = FileStore<UploadedTexture, StoredTexture>;
|
||||
Reference in new issue
Block a user