Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
import { derived, writable } from 'svelte/store'
import type { AppState, KeyDef, KeycapModel, SymbolContent, SymbolDef, Template, CustomFont } from './types'
import { newId } from '../utils/id'
import { getPublicPath } from '../utils/paths'
export const SLOTS = [
{ name: 'alpha', symbol: 'A' },
{ name: 'beta', symbol: 'B' },
{ name: 'gamma', symbol: 'C' },
{ name: 'delta', symbol: 'D' },
{ name: 'epsilon', symbol: 'E' },
{ name: 'zeta', symbol: 'F' },
{ name: 'eta', symbol: 'G' },
{ name: 'theta', symbol: 'H' },
{ name: 'iota', symbol: 'I' },
{ name: 'kappa', symbol: 'J' },
] as const
export const MAX_SLOTS = SLOTS.length
export function getSlotName(index: number): string {
return SLOTS[index]?.name ?? `slot${index + 1}`
}
export function getSlotSymbol(index: number): string {
return SLOTS[index]?.symbol ?? String(index + 1)
}
function defaultSymbol(): SymbolDef {
return {
id: newId('sym'),
slotName: '',
x: 0,
y: 0,
fontName: 'Dongle',
fontSizeMm: 11,
color: '#ffffff',
rotationDeg: 0,
}
}
function defaultKeycapModel(): KeycapModel {
const defaultRegistryPath = 'JoeScotto_Scooped_MX/1.00u (Concave).stl'
const defaultServerId = defaultRegistryPath.replace(/[^a-zA-Z0-9]/g, '_')
return {
id: newId('model'),
name: '1.00u',
widthMm: 0, // 0 indicates dimensions not yet loaded
heightMm: 0,
source: {
kind: 'server',
serverId: defaultServerId,
url: getPublicPath(`stls/${defaultRegistryPath}`),
stl: { fileName: '1.00u.stl', pathHint: `server:${defaultServerId}` },
},
rotationX: 0,
rotationY: 0,
rotationZ: 0,
extrusionDepthMm: 1.2,
}
}
function defaultTemplate(keycapModelId: string): Template {
return {
id: newId('tpl'),
name: 'Regular key',
keycapModelId,
symbols: [defaultSymbol()],
}
}
function defaultState(): AppState {
return {
version: 1,
keycapModels: [],
templates: [],
keys: [],
customFonts: [],
ui: {
selectedKeycapModelId: null,
selectedTemplateId: null,
selectedKeyId: null,
},
}
}
export const app = writable<AppState>(defaultState())
export const selectedTemplate = derived(
app,
$app => $app.templates.find(t => t.id === $app.ui.selectedTemplateId) ?? null
)
export const selectedKey = derived(app, $app => $app.keys.find(k => k.id === $app.ui.selectedKeyId) ?? null)
export function isProjectEmpty(state: AppState): boolean {
return (
state.keycapModels.length === 0 &&
state.templates.length === 0 &&
state.keys.length === 0 &&
state.customFonts.length === 0
)
}
export const actions = {
selectKeycapModel(modelId: string | null) {
app.update(s => ({ ...s, ui: { ...s.ui, selectedKeycapModelId: modelId } }))
},
selectTemplate(templateId: string | null) {
app.update(s => ({ ...s, ui: { ...s.ui, selectedTemplateId: templateId } }))
},
selectKey(keyId: string | null) {
app.update(s => ({ ...s, ui: { ...s.ui, selectedKeyId: keyId } }))
},
createTemplate() {
app.update(s => {
const modelId = s.ui.selectedKeycapModelId ?? s.keycapModels[0]?.id
if (!modelId) return s
const tpl = defaultTemplate(modelId)
return {
...s,
templates: [...s.templates, tpl],
ui: { ...s.ui, selectedTemplateId: tpl.id },
}
})
},
renameTemplate(templateId: string, name: string) {
app.update(s => ({
...s,
templates: s.templates.map(t => (t.id === templateId ? { ...t, name } : t)),
}))
},
deleteTemplate(templateId: string) {
app.update(s => {
const keysToRemove = s.keys.filter(k => k.templateId === templateId).map(k => k.id)
const nextTemplates = s.templates.filter(t => t.id !== templateId)
const nextKeys = s.keys.filter(k => k.templateId !== templateId)
const nextSelectedTemplateId =
s.ui.selectedTemplateId === templateId ? (nextTemplates[0]?.id ?? null) : s.ui.selectedTemplateId
const nextSelectedKeyId = keysToRemove.includes(s.ui.selectedKeyId ?? '')
? (nextKeys[0]?.id ?? null)
: s.ui.selectedKeyId
return {
...s,
templates: nextTemplates,
keys: nextKeys,
ui: {
...s.ui,
selectedTemplateId: nextSelectedTemplateId,
selectedKeyId: nextSelectedKeyId,
},
}
})
},
setTemplateKeycapModel(templateId: string, keycapModelId: string) {
app.update(s => ({
...s,
templates: s.templates.map(t => (t.id === templateId ? { ...t, keycapModelId } : t)),
}))
},
createKeycapModel() {
const model = defaultKeycapModel()
app.update(s => ({
...s,
keycapModels: [...s.keycapModels, model],
ui: { ...s.ui, selectedKeycapModelId: model.id },
}))
},
renameKeycapModel(modelId: string, name: string) {
app.update(s => ({
...s,
keycapModels: s.keycapModels.map(m => (m.id === modelId ? { ...m, name } : m)),
}))
},
updateKeycapModel(
modelId: string,
patch: Partial<
Pick<KeycapModel, 'widthMm' | 'heightMm' | 'rotationX' | 'rotationY' | 'rotationZ' | 'extrusionDepthMm'>
>
) {
app.update(s => ({
...s,
keycapModels: s.keycapModels.map(m => (m.id === modelId ? { ...m, ...patch } : m)),
}))
},
setKeycapModelSource(modelId: string, source: KeycapModel['source']) {
app.update(s => ({
...s,
keycapModels: s.keycapModels.map(m => (m.id === modelId ? { ...m, source } : m)),
}))
},
deleteKeycapModel(modelId: string) {
app.update(s => {
const templatesToRemove = s.templates.filter(t => t.keycapModelId === modelId).map(t => t.id)
const keysToRemove = s.keys.filter(k => templatesToRemove.includes(k.templateId)).map(k => k.id)
const nextModels = s.keycapModels.filter(m => m.id !== modelId)
const nextTemplates = s.templates.filter(t => t.keycapModelId !== modelId)
const nextKeys = s.keys.filter(k => !templatesToRemove.includes(k.templateId))
const nextSelectedModelId =
s.ui.selectedKeycapModelId === modelId ? (nextModels[0]?.id ?? null) : s.ui.selectedKeycapModelId
const nextSelectedTemplateId = templatesToRemove.includes(s.ui.selectedTemplateId ?? '')
? (nextTemplates[0]?.id ?? null)
: s.ui.selectedTemplateId
const nextSelectedKeyId = keysToRemove.includes(s.ui.selectedKeyId ?? '')
? (nextKeys[0]?.id ?? null)
: s.ui.selectedKeyId
return {
...s,
keycapModels: nextModels,
templates: nextTemplates,
keys: nextKeys,
ui: {
...s.ui,
selectedKeycapModelId: nextSelectedModelId,
selectedTemplateId: nextSelectedTemplateId,
selectedKeyId: nextSelectedKeyId,
},
}
})
},
addSymbol(templateId: string) {
app.update(s => {
const tpl = s.templates.find(t => t.id === templateId)
if (!tpl) return s
if (tpl.symbols.length >= MAX_SLOTS) return s
const sym = defaultSymbol()
const nextTemplates = s.templates.map(t => (t.id === templateId ? { ...t, symbols: [...t.symbols, sym] } : t))
return { ...s, templates: nextTemplates }
})
},
deleteSymbol(templateId: string, symbolId: string) {
app.update(s => {
const tpl = s.templates.find(t => t.id === templateId)
if (!tpl) return s
const nextTemplates = s.templates.map(t =>
t.id === templateId ? { ...t, symbols: t.symbols.filter(sym => sym.id !== symbolId) } : t
)
const nextKeys = s.keys.map(k => {
if (k.templateId !== templateId) return k
const { [symbolId]: _, ...rest } = k.contentBySymbolId
return { ...k, contentBySymbolId: rest }
})
return { ...s, templates: nextTemplates, keys: nextKeys }
})
},
updateSymbol(templateId: string, symbolId: string, patch: Partial<SymbolDef>) {
app.update(s => {
const { slotName, ...restPatch } = patch
return {
...s,
templates: s.templates.map(t =>
t.id === templateId
? {
...t,
symbols: t.symbols.map(sym => (sym.id === symbolId ? { ...sym, ...restPatch } : sym)),
}
: t
),
}
})
},
createKey() {
app.update(s => {
const tplId = s.ui.selectedTemplateId ?? s.templates[0]?.id
if (!tplId) return s
const tpl = s.templates.find(t => t.id === tplId)
if (!tpl) return s
const key: KeyDef = {
id: newId('key'),
name: `Key ${s.keys.length + 1}`,
templateId: tpl.id,
contentBySymbolId: {},
}
return { ...s, keys: [...s.keys, key], ui: { ...s.ui, selectedKeyId: key.id } }
})
},
renameKey(keyId: string, name: string) {
app.update(s => ({ ...s, keys: s.keys.map(k => (k.id === keyId ? { ...k, name } : k)) }))
},
deleteKey(keyId: string) {
app.update(s => {
const nextKeys = s.keys.filter(k => k.id !== keyId)
const nextSelectedKeyId = s.ui.selectedKeyId === keyId ? (nextKeys[0]?.id ?? null) : s.ui.selectedKeyId
return { ...s, keys: nextKeys, ui: { ...s.ui, selectedKeyId: nextSelectedKeyId } }
})
},
setKeyTemplate(keyId: string, templateId: string) {
app.update(s => {
const tpl = s.templates.find(t => t.id === templateId)
if (!tpl) return s
return {
...s,
keys: s.keys.map(k => {
if (k.id !== keyId) return k
// Keep content for symbols that exist in the new template
const newContentBySymbolId: Record<string, SymbolContent> = {}
for (const sym of tpl.symbols) {
if (k.contentBySymbolId[sym.id]) {
newContentBySymbolId[sym.id] = k.contentBySymbolId[sym.id]
}
}
return {
...k,
templateId,
contentBySymbolId: newContentBySymbolId,
}
}),
}
})
},
setKeyText(keyId: string, symbolId: string, text: string) {
app.update(s => ({
...s,
keys: s.keys.map(k => {
if (k.id !== keyId) return k
const newContentBySymbolId = { ...k.contentBySymbolId }
if (text.trim()) {
newContentBySymbolId[symbolId] = { kind: 'text', value: text }
} else {
delete newContentBySymbolId[symbolId]
}
return {
...k,
contentBySymbolId: newContentBySymbolId,
}
}),
}))
},
setKeyIcon(keyId: string, symbolId: string, iconName: string | null) {
app.update(s => ({
...s,
keys: s.keys.map(k => {
if (k.id !== keyId) return k
const newContentBySymbolId = { ...k.contentBySymbolId }
if (iconName) {
newContentBySymbolId[symbolId] = { kind: 'icon', iconName }
} else {
delete newContentBySymbolId[symbolId]
}
return {
...k,
contentBySymbolId: newContentBySymbolId,
}
}),
}))
},
setKeyContent(keyId: string, symbolId: string, content: SymbolContent | null) {
app.update(s => ({
...s,
keys: s.keys.map(k => {
if (k.id !== keyId) return k
const newContentBySymbolId = { ...k.contentBySymbolId }
if (content) {
newContentBySymbolId[symbolId] = content
} else {
delete newContentBySymbolId[symbolId]
}
return {
...k,
contentBySymbolId: newContentBySymbolId,
}
}),
}))
},
addCustomFont(font: CustomFont) {
app.update(s => {
if (s.customFonts.some(f => f.id === font.id || f.name === font.name)) {
return s
}
return { ...s, customFonts: [...s.customFonts, font] }
})
},
}
679b3bf6bThis file is pinned to the selected release. Download the original to open it in your design tool.