medieval-kit/my-registry/build.ts
CRT 50c7fc60b9 Translate the whole repository to English
The kit is going public, so everything a reader sees is now English:
27 model sources, the core vocabulary, meta.ts (titles, descriptions and
every slider label), the viewer UI, all script output, REFERENCE.md and
the READMEs.

Rules applied: only comments, doc comments and human-facing strings were
touched. Identifiers, numbers, logic, registry item ids, part names and
material slot names are unchanged — those are a published contract and
were already English.

Verified rather than assumed: for all 53 changed .ts files the code
skeleton (source with comments stripped and string contents replaced by a
placeholder) is byte-identical before and after. Full gate is green —
typecheck, registry conformance, ~510 model checks, GLB round trip and a
production build.

Also fixed here:
- index.html and viewer.html were missed by the translation pass because
  the file list only covered .ts/.md/.css. Both had lang="tr" and Turkish
  titles.
- The viewer formatted triangle counts with toLocaleString('tr-TR'), which
  prints 15.425 rather than 15,425.
- REFERENCE.md's model table header did not match what
  scripts/catalog-table.ts emits, so the next regeneration would have
  silently changed it. Aligned to the generator.
- Removed scripts/probe.ts and scripts/zdiag.ts; they were throwaway
  diagnostics that got committed by accident.
2026-08-21 14:35:13 +07:00

202 lines
6.9 KiB
TypeScript

/**
* The script that builds your own registry.
*
* It scans every folder under `models/`:
* - contains `model.ts` -> vibe3d:model
* - does not -> vibe3d:lib (shared support item)
*
* Dependencies are derived from the source: if a model imports from under
* `../core/`, `@medieval-kit/core` automatically becomes a dependency. There is
* no hand-maintained list, so the list cannot go stale either.
*
* Run: bun my-registry/build.ts
* Validate: bunx vibe3d registry validate my-registry/dist/registry.json
*/
import { createHash } from 'node:crypto'
import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises'
import { dirname, join, relative, resolve, sep } from 'node:path'
import { fileURLToPath } from 'node:url'
const registryRoot = dirname(fileURLToPath(import.meta.url))
const sourceRoot = join(registryRoot, 'models')
const outputRoot = join(registryRoot, 'dist')
const NAMESPACE = '@medieval-kit'
const THREE_RANGE = 'three@>=0.185.0'
/** Catalogue data per model. The source files themselves are read from disk. */
import { MODEL_META } from './meta.ts'
const LIB_DESCRIPTION: Record<string, { title: string; description: string }> = {
core: {
title: 'Medieval Kit Core',
description:
'The shared foundation of the kit: deterministic randomness, vertex-colored materials ' +
'and stave/band/head geometry generators.',
},
}
function sha256(content: string): string {
return createHash('sha256').update(content).digest('hex')
}
function posix(path: string): string {
return path.split(sep).join('/')
}
async function collectTypeScript(root: string): Promise<string[]> {
const files: string[] = []
const visit = async (directory: string): Promise<void> => {
for (const entry of await readdir(directory, { withFileTypes: true })) {
const path = join(directory, entry.name)
if (entry.isDirectory()) await visit(path)
else if (entry.isFile() && entry.name.endsWith('.ts')) files.push(path)
}
}
await visit(root)
return files.sort()
}
async function registryFiles(itemId: string, paths: readonly string[]) {
const directory = join(sourceRoot, itemId)
return Promise.all(paths.map(async (path) => {
const content = await readFile(path, 'utf8')
return {
path: posix(relative(registryRoot, path)),
// {models} and {vibe3d} are replaced with the paths from the consumer's
// models.json — the registry does not have to assume its folder layout.
target: `{models}/medieval-kit/${itemId}/${posix(relative(directory, path))}`,
content,
hash: sha256(content),
}
}))
}
/** Every import of the form `from '../<folder>/'` is a registry dependency. */
function dependenciesFrom(contents: readonly string[], selfId: string): string[] {
const found = new Set<string>()
const pattern = /from\s+['"]\.\.\/([a-z0-9][a-z0-9-]*)\//g
for (const content of contents) {
for (const match of content.matchAll(pattern)) {
const id = match[1]
if (id && id !== selfId) found.add(`${NAMESPACE}/${id}`)
}
}
return [...found].sort()
}
async function main(): Promise<void> {
const entries = await readdir(sourceRoot, { withFileTypes: true })
const models: string[] = []
const libs: string[] = []
for (const entry of entries) {
if (!entry.isDirectory()) continue
// Folders starting with a dot are not items. Tools (editor, VCS, agent
// state) may leave folders here; registry names, on the other hand, have to
// be `[a-z0-9-]+`, so such a folder would break the build.
if (entry.name.startsWith('.')) continue
// The criterion for being a model: model.ts at the folder ROOT. Searching
// the subtree is wrong: `core/model.ts` is a shared helper, not a model.
const paths = await collectTypeScript(join(sourceRoot, entry.name))
const root = join(sourceRoot, entry.name, 'model.ts')
if (paths.includes(root)) models.push(entry.name)
else libs.push(entry.name)
}
models.sort()
libs.sort()
const items: unknown[] = []
for (const id of libs) {
const paths = await collectTypeScript(join(sourceRoot, id))
const contents = await Promise.all(paths.map((path) => readFile(path, 'utf8')))
const meta = LIB_DESCRIPTION[id] ?? { title: id, description: 'Shared kit source.' }
items.push({
name: id,
type: 'vibe3d:lib',
title: meta.title,
description: meta.description,
dependencies: [THREE_RANGE],
registryDependencies: dependenciesFrom(contents, id),
files: await registryFiles(id, paths),
})
}
for (const id of models) {
const paths = await collectTypeScript(join(sourceRoot, id))
const contents = await Promise.all(paths.map((path) => readFile(path, 'utf8')))
const meta = MODEL_META[id]
if (!meta) throw new Error(`no MODEL_META entry for ${id}`)
items.push({
name: id,
type: 'vibe3d:model',
title: meta.title,
description: meta.description,
dependencies: [THREE_RANGE],
registryDependencies: dependenciesFrom(contents, id),
files: await registryFiles(id, paths),
meta: {
title: meta.title,
description: meta.description,
category: meta.category,
tags: meta.tags,
controls: meta.controls,
materialSlots: meta.materialSlots,
parts: meta.parts,
sockets: [],
},
})
}
// defaultItem is required: `vibe3d add @medieval-kit` (with no item name) installs this.
items.push({
name: 'kit',
type: 'vibe3d:kit',
title: 'Medieval Kit',
description: 'The complete lowpoly medieval kit.',
dependencies: [],
registryDependencies: models.map((id) => `${NAMESPACE}/${id}`),
files: [],
})
const registry = {
$schema: 'https://vibe3d.dev/schema/registry.json',
schemaVersion: 1,
namespace: NAMESPACE,
name: 'Medieval Kit',
description: 'Lowpoly medieval procedural model library for Three.js.',
license: 'MIT',
defaultItem: 'kit',
compatibility: {
vibe3d: '^0.0.1',
engine: 'three',
three: '>=0.185.0',
// scifi-kit requires ['webgpu','tsl']; this kit runs on classic WebGL.
capabilities: [],
},
items,
}
await mkdir(outputRoot, { recursive: true })
await writeFile(
resolve(outputRoot, 'registry.json'),
`${JSON.stringify(registry, null, 2)}\n`,
'utf8',
)
const fileCount = items.reduce<number>(
(total, item) => total + (item as { files: unknown[] }).files.length,
0,
)
console.log(`${items.length} items built · ${models.length} models · ${libs.length} libs · ${fileCount} files`)
for (const id of [...libs, ...models]) {
const item = items.find((candidate) => (candidate as { name: string }).name === id) as {
registryDependencies: string[]
}
const deps = item.registryDependencies.length > 0 ? ` -> ${item.registryDependencies.join(', ')}` : ''
console.log(` ${NAMESPACE}/${id}${deps}`)
}
}
await main()