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>
111 lines
3.8 KiB
TypeScript
111 lines
3.8 KiB
TypeScript
import { Container, Graphics, Text, TextStyle } from 'pixi.js';
|
||
import type { StoredFont } from '../../core/store/FontStore';
|
||
import { formatBytes, Theme, textStyle, truncate } from '../../ui';
|
||
import { fontLibrary } from '../../viewport/FontLibrary';
|
||
import {
|
||
currentPicker,
|
||
FilePicker,
|
||
openPicker,
|
||
type PickerItem,
|
||
type PickerRow,
|
||
RowList,
|
||
withAssetNames,
|
||
} from '../FilePicker';
|
||
import { type FontImportDeps, importStoredFont, pickAndImportFonts } from '../fontImport';
|
||
|
||
const ROW_HEIGHT = 52;
|
||
const ROW_PAD = 8;
|
||
/** Big enough to tell two typefaces apart at a glance, small enough for a long line to fit. */
|
||
const SAMPLE_SIZE = 22;
|
||
const SAMPLE_TEXT = 'Aa Bb Cc 0123 The quick brown fox';
|
||
|
||
type FontItem = PickerItem<StoredFont>;
|
||
interface FontRow extends PickerRow {
|
||
sample: Text;
|
||
}
|
||
|
||
/**
|
||
* A typeface has no thumbnail to show, so each row *is* the preview: the sample line is drawn in
|
||
* the face itself, and reflows from the fallback once the library has loaded it.
|
||
*/
|
||
class FontList extends RowList<FontItem, FontRow> {
|
||
protected readonly rowHeight = ROW_HEIGHT;
|
||
|
||
constructor() {
|
||
super();
|
||
this.watch(fontLibrary, () => this.redraw());
|
||
}
|
||
|
||
protected buildRow(): FontRow {
|
||
const box = new Container();
|
||
const background = new Graphics();
|
||
// Its own style object, never `textStyle('body')`: those are shared across the whole editor,
|
||
// so writing a family into one would restyle every label in the window.
|
||
const sample = new Text({ style: new TextStyle({ fontSize: SAMPLE_SIZE, fill: Theme.colors.text }) });
|
||
const caption = new Text({ style: textStyle('small') });
|
||
box.addChild(background, sample, caption);
|
||
return { box, background, sample, caption };
|
||
}
|
||
|
||
protected redrawRow(row: FontRow, item: FontItem, selected: boolean): void {
|
||
this.drawBox(row, this.w, ROW_HEIGHT, selected);
|
||
// Empty family = not loaded yet; the row then reads in the editor's own typeface and the
|
||
// library's change event brings it back here once the face has arrived.
|
||
row.sample.style.fontFamily = fontLibrary.family(item.file) || Theme.font;
|
||
row.sample.text = truncate(SAMPLE_TEXT, row.sample.style, this.w - ROW_PAD * 2);
|
||
row.sample.position.set(ROW_PAD, ROW_PAD - 2);
|
||
|
||
// The asset name wins where there is one – it is what the project calls this face. Otherwise
|
||
// the name it was uploaded under, which is all a bare hash leaves to go on.
|
||
row.caption.style = textStyle(item.assetName ? 'accent' : 'muted');
|
||
row.caption.text = truncate(
|
||
item.assetName ?? (item.name || formatBytes(item.bytes)),
|
||
row.caption.style,
|
||
this.w - ROW_PAD * 2,
|
||
);
|
||
row.caption.position.set(ROW_PAD, ROW_HEIGHT - 16);
|
||
}
|
||
}
|
||
|
||
export class FontPicker extends FilePicker<FontItem> {
|
||
declare protected readonly list: FontList;
|
||
|
||
constructor(private readonly deps: FontImportDeps) {
|
||
super(
|
||
new FontList(),
|
||
{
|
||
title: 'Choose a font',
|
||
loading: 'Loading fonts…',
|
||
empty: 'No fonts on the server yet – upload one.',
|
||
use: 'Use font',
|
||
noun: 'font',
|
||
},
|
||
() => pickAndImportFonts(deps),
|
||
);
|
||
}
|
||
|
||
rowGlobal(file: string): { x: number; y: number } | null {
|
||
return this.list.rowGlobal(file);
|
||
}
|
||
|
||
protected async fetchItems(): Promise<FontItem[]> {
|
||
return withAssetNames(await this.deps.fonts.list(), this.deps.state.project, 'font');
|
||
}
|
||
|
||
protected contentHeight(count: number): { unit: number; total: number } {
|
||
return { unit: ROW_HEIGHT, total: this.list.heightFor(count) };
|
||
}
|
||
|
||
protected adopt(item: FontItem): void {
|
||
importStoredFont(item, this.deps);
|
||
}
|
||
}
|
||
|
||
export function openFontPicker(deps: FontImportDeps): FontPicker {
|
||
return openPicker(new FontPicker(deps));
|
||
}
|
||
|
||
export function currentFontPicker(): FontPicker | null {
|
||
return currentPicker(FontPicker);
|
||
}
|