Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
import { app } from './store'
import { stlBuffersByModelId } from './sessionAssets'
import type { AppState, KeyDef, KeycapModel, Template, CustomFont } from './types'
import { getPublicPath } from '../utils/paths'
const CANONICAL_PROJECT_STL_PREFIX = '/keycap_generator/stls/'
export function applyLoadedProject(project: AppState) {
stlBuffersByModelId.set({})
app.set(project)
}
function prepareForExport(state: AppState): Omit<AppState, 'ui'> {
// UI state is runtime-only and not exported.
const { ui, ...rest } = state
return {
...rest,
keycapModels: state.keycapModels.map(m => {
if (m.source.kind === 'upload') {
return { ...m, source: { ...m.source, stl: m.source.stl ? { ...m.source.stl } : null } }
}
const stlsIndex = m.source.url.indexOf('stls/')
const canonicalUrl =
stlsIndex !== -1 ? `${CANONICAL_PROJECT_STL_PREFIX}${m.source.url.slice(stlsIndex + 5)}` : m.source.url
return { ...m, source: { ...m.source, url: canonicalUrl, stl: { ...m.source.stl } } }
}),
}
}
type RawProjectV1 = {
version: 1
keycapModels: KeycapModel[]
templates: Template[]
keys: KeyDef[]
customFonts: CustomFont[]
}
function parseProjectV1(raw: unknown): AppState | null {
if (typeof raw !== 'object' || raw === null) return null
const project = raw as RawProjectV1
if (project.version !== 1) return null
if (!Array.isArray(project.keycapModels) || !Array.isArray(project.templates) || !Array.isArray(project.keys))
return null
if (!Array.isArray(project.customFonts)) return null
return {
version: 1,
keycapModels: project.keycapModels,
templates: project.templates,
keys: project.keys,
customFonts: project.customFonts,
ui: {
selectedKeycapModelId: project.keycapModels[0]?.id ?? null,
selectedTemplateId: project.templates[0]?.id ?? null,
selectedKeyId: project.keys[0]?.id ?? null,
},
}
}
export function downloadStateFile(state: AppState) {
const prepared = prepareForExport(state)
const json = JSON.stringify(prepared, null, 2)
const blob = new Blob([json], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'keycap-generator-project.json'
a.click()
setTimeout(() => URL.revokeObjectURL(url), 500)
}
function parseProjectJsonText(jsonText: string): AppState | null {
try {
return parseProjectV1(JSON.parse(jsonText) as unknown)
} catch {
return null
}
}
export type ProjectReadResult = { project: AppState } | { error: string }
export async function readProjectFromFile(ev: Event): Promise<ProjectReadResult | null> {
const input = ev.currentTarget as HTMLInputElement
const file = input.files?.[0]
input.value = ''
if (!file) return null
try {
const text = await file.text()
const parsed = parseProjectJsonText(text)
if (!parsed) return { error: 'Invalid project file.' }
return { project: parsed }
} catch (error) {
return { error: `Failed to read project file: ${error instanceof Error ? error.message : 'Unknown error'}` }
}
}
export async function readPresetProject(presetName: string): Promise<ProjectReadResult> {
try {
const response = await fetch(getPublicPath(`presets/${presetName}.json`))
if (!response.ok) {
return { error: `Failed to load preset: ${response.statusText}` }
}
const text = await response.text()
const parsed = parseProjectJsonText(text)
if (!parsed) return { error: 'Invalid preset file.' }
return { project: parsed }
} catch (error) {
return { error: `Failed to load preset: ${error instanceof Error ? error.message : 'Unknown error'}` }
}
}
8d9aed881This file is pinned to the selected release. Download the original to open it in your design tool.