Fonts: a new asset type, picked from the server or dropped in, worn by text objects (#74)
game-maker / suite (push) Successful in 9m10s

A font asset holds a file and nothing else, the twin of a texture: the server keeps it
content-addressed under data/fonts/{sha256}.{ext}, with the format read from the file's own
four-byte signature (finfo reports fonts inconsistently across libmagic versions). A text
wears one through its ordinary Font property, so overrides, reset, undo and the Properties
node come for free; no font means the editor's own typeface.

The picker lists everything the server holds and renders each row in the face it offers.
The family name is gm-font-<hash16>, never the name inside the file, so two "Regular" cannot
collide. WorldViewport.syncText is the single place that draws it, which covers scene, game
preview and play at once.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Opus 5 committed 2026-09-13 20:09:59 +02:00
1 parent 28c0789b19
commit 86b5363a01
35 files changed
+1451 -85

No files matched your search

+12 -1
View File
@@ -100,6 +100,16 @@ export interface ShaderAsset extends AssetBase {
graph: Graph;
}
/**
* `file` names the typeface in `data/fonts/` the way a texture names its image: content-addressed
* `{sha256}.{ext}`, empty when the upload failed. A text object wears it through its `font`
* property; the family the browser knows it under is derived from the file name, see `model/font`.
*/
export interface FontAsset extends AssetBase {
type: 'font';
file: string;
}
export type Asset =
| SpriteAsset
| CameraAsset
@@ -111,7 +121,8 @@ export type Asset =
| TextureAsset
| AnimationAsset
| StateMachineAsset
| ShaderAsset;
| ShaderAsset
| FontAsset;
/** The assets edited as a node graph – what the graph canvas and the graph commands work on. */
export type GraphAsset = BehaviorAsset | StateMachineAsset | ShaderAsset;
+6 -2
View File
@@ -10,6 +10,7 @@ export const ASSET_TYPES = [
'animation',
'statemachine',
'shader',
'font',
] as const;
export type AssetType = (typeof ASSET_TYPES)[number];
@@ -26,15 +27,17 @@ export const ASSET_TYPE_LABELS: Record<AssetType, string> = {
animation: 'Animation',
statemachine: 'State machine',
shader: 'Shader',
font: 'Font',
};
/**
* Textures and animations are not placeable: they are pictures a sprite wears through `texture`,
* as a shader is worn through `shader`. A state machine is not either: a behavior refers to it by id.
* as a shader is worn through `shader` and a font through `font`. A state machine is not either:
* a behavior refers to it by id.
*/
export type PlaceableAssetType = Exclude<
AssetType,
'scene' | 'texture' | 'animation' | 'statemachine' | 'shader'
'scene' | 'texture' | 'animation' | 'statemachine' | 'shader' | 'font'
>;
const NOT_PLACEABLE: ReadonlySet<AssetType> = new Set([
@@ -43,6 +46,7 @@ const NOT_PLACEABLE: ReadonlySet<AssetType> = new Set([
'animation',
'statemachine',
'shader',
'font',
]);
export function isPlaceableType(type: AssetType): type is PlaceableAssetType {
+29
View File
@@ -0,0 +1,29 @@
import type { FontAsset } from './Asset';
import { findAsset, type Project } from './Project';
/** Mirrors `FontStore::isValidName` on the server, where the name is derived from the bytes. */
const FILE_PATTERN = /^[a-f0-9]{64}\.(ttf|otf|woff|woff2)$/;
export function isFontFile(name: string): boolean {
return FILE_PATTERN.test(name);
}
/**
* The CSS family a file is registered under. Derived from the content-addressed name, so the same
* bytes are always one family and two uploads of one typeface cannot fight over a name – the name
* inside the font is never used for this, only for the asset's label.
*/
export function fontFamilyOf(file: string): string {
return `gm-font-${file.slice(0, 16)}`;
}
/** The file a `font` property points at, empty when the id is unset or no longer resolves. */
export function fontFileOf(project: Project, id: string): string {
if (id === '') return '';
const asset = findAsset(project, id);
return asset?.type === 'font' ? asset.file : '';
}
export function isFontAsset(asset: { type: string }): asset is FontAsset {
return asset.type === 'font';
}