Complete refactor (#123)

This commit is contained in:
Misode
2020-11-23 14:29:16 +01:00
committed by GitHub
parent 0ad33cd88f
commit 982b4728e7
45 changed files with 1162 additions and 1113 deletions

View File

@@ -1,18 +0,0 @@
import { ModelListener, DataModel } from '@mcschema/core';
export abstract class AbstractView implements ModelListener {
model: DataModel
constructor(model: DataModel) {
this.model = model
this.model.addListener(this)
}
setModel(model: DataModel) {
this.model.removeListener(this)
this.model = model
this.model.addListener(this)
}
invalidated(model: DataModel): void {}
}

View File

@@ -1,28 +0,0 @@
import {
DataModel,
Errors,
} from '@mcschema/core'
import { AbstractView } from './AbstractView'
import { locale } from './locales'
export class ErrorsView extends AbstractView {
target: HTMLElement
constructor(model: DataModel, target: HTMLElement) {
super(model)
this.target = target
}
errors(errors: Errors): void {
this.target.style.display = errors.count() > 0 ? 'flex' : 'none'
this.target.children[0].innerHTML = errors.getAll().map(err =>
`<div class="error">
<svg class="error-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="18" height="18"><path fill-rule="evenodd" d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8zm9 3a1 1 0 11-2 0 1 1 0 012 0zm-.25-6.25a.75.75 0 00-1.5 0v3.5a.75.75 0 001.5 0v-3.5z"></path></svg>
<span class="error-path">${err.path.toString()}</span>
<span>-</span>
<span class="error-message">${locale(err.error, err.params)}</span>
</div>`
).join('')
}
}

View File

@@ -1,12 +1,5 @@
import { ModelPath } from '@mcschema/core'
const dec2hex = (dec: number) => ('0' + dec.toString(16)).substr(-2)
export function hexId(length = 12) {
var arr = new Uint8Array(length / 2)
window.crypto.getRandomValues(arr)
return Array.from(arr, dec2hex).join('')
}
import { hexId } from './Utils'
type Registry = {
[id: string]: (el: Element) => void
@@ -24,7 +17,7 @@ export interface Mounter {
registerChange(callback: (el: Element) => void): string
registerClick(callback: (el: Element) => void): string
nodeInjector(path: ModelPath, mounter: Mounter): string
mount(el: HTMLElement): void
mount(el: Element): void
}
export class Mounter implements Mounter {
@@ -79,7 +72,7 @@ export class Mounter implements Mounter {
return this.registerEvent('click', callback)
}
mount(el: HTMLElement): void {
mount(el: Element): void {
for (const id in this.registry) {
const element = el.querySelector(`[data-id="${id}"]`)
if (element !== null) this.registry[id](element)

View File

@@ -1,4 +1,5 @@
import { CollectionRegistry } from '@mcschema/core'
import { checkVersion } from './App'
import config from '../config.json'
const localStorageCache = (version: string) => `cache_${version}`
@@ -20,6 +21,10 @@ export const RegistryFetcher = async (target: CollectionRegistry, versionId: str
await Promise.all(config.registries.map(async r => {
const id = typeof r === 'string' ? r : r.id
if (typeof r !== 'string' && r.minVersion) {
if (!checkVersion(versionId, r.minVersion)) return
}
if (!cache.registries) {
cache.registries = {}
}
@@ -28,9 +33,9 @@ export const RegistryFetcher = async (target: CollectionRegistry, versionId: str
return
}
const url = typeof r === 'string'
? mcdata(version.mcdata_ref, r)
: `${baseUrl}/${version.mcdata_ref}/${r.path}`
const url = typeof r !== 'string' && r.path
? `${baseUrl}/${version.mcdata_ref}/${r.path}/data.min.json`
: mcdata(version.mcdata_ref, typeof r === 'string' ? r : r.id)
try {
const res = await fetch(url)
@@ -40,7 +45,7 @@ export const RegistryFetcher = async (target: CollectionRegistry, versionId: str
cache.registries[id] = data.values
cacheDirty = true
} catch (e) {
console.error(`Error occurred while fetching registry "${id}":`, e)
console.warn(`Error occurred while fetching registry "${id}":`, e)
}
}))

41
src/app/Router.ts Normal file
View File

@@ -0,0 +1,41 @@
import { App } from './App';
import { View } from './views/View';
import { Home } from './views/Home'
import { Generator } from './views/Generator'
import config from '../config.json'
const categories = config.models.filter(m => m.category === true)
const router = async () => {
const urlParts = location.pathname.split('/').filter(e => e)
const target = document.getElementById('app')!
const view = new View()
if (urlParts.length === 0){
App.model.set({ id: '', name: 'Data Pack', category: true})
target.innerHTML = Home(view)
} else if (urlParts.length === 1 && categories.map(m => m.id).includes(urlParts[0])) {
App.model.set(categories.find(m => m.id === urlParts[0])!)
target.innerHTML = Home(view)
} else {
App.model.set(config.models.find(m => m.id === urlParts.join('/'))!)
target.innerHTML = Generator(view)
}
view.mounted(target)
}
window.addEventListener("popstate", router);
document.addEventListener("DOMContentLoaded", () => {
document.body.addEventListener("click", e => {
if (e.target instanceof Element
&& e.target.hasAttribute('data-link')
&& e.target.hasAttribute('href')
) {
e.preventDefault();
history.pushState(null, '', e.target.getAttribute('href'));
router();
}
});
router();
});

View File

@@ -1,45 +0,0 @@
import { DataModel, Path, ModelPath } from '@mcschema/core'
import { AbstractView } from './AbstractView'
import { transformOutput } from './hooks/transformOutput'
type SourceViewOptions = {
indentation?: number | string,
rows?: number
}
/**
* JSON representation view of the model.
* Renders the result in a <textarea>.
*/
export class SourceView extends AbstractView {
target: HTMLTextAreaElement
options?: SourceViewOptions
/**
* @param model data model this view represents and listens to
* @param target DOM element to render the view
* @param options optional options for the view
*/
constructor(model: DataModel, target: HTMLTextAreaElement, options?: SourceViewOptions) {
super(model)
this.target = target
this.options = options
this.target.addEventListener('change', evt => this.updateModel())
}
invalidated() {
const transformed = this.model.schema.hook(transformOutput, new ModelPath(this.model), this.model.data)
this.target.value = JSON.stringify(transformed, null, this.options?.indentation)
}
updateModel() {
let parsed = {}
try {
parsed = JSON.parse(this.target.value)
} catch (err) {
this.model.error(new Path().push('JSON'), err.message)
return
}
this.model.reset(parsed)
}
}

25
src/app/Tracker.ts Normal file
View File

@@ -0,0 +1,25 @@
const event = (category: string, action: string, label?: string) =>
ga('send', 'event', category, action, label)
const dimension = (index: number, value: string) =>
ga('set', `dimension${index}`, value);
export const Tracker = {
setTheme: (theme: string) => event('Generator', 'set-theme', theme),
setVersion: (version: string) => event('Generator', 'set-version', version),
setPreview: (name: string) => event('Preview', 'set-preview', name),
setLanguage: (language: string) => event('Generator', 'set-language', language),
reset: () => event('Generator', 'reset'),
undo: (hotkey = false) => event('Generator', 'undo', hotkey ? 'Hotkey' : 'Menu'),
redo: (hotkey = false) => event('Generator', 'redo', hotkey ? 'Hotkey' : 'Menu'),
copy: () => event('JsonOutput', 'copy'),
download: () => event('JsonOutput', 'download'),
share: () => event('JsonOutput', 'share'),
toggleErrors: (visible: boolean) => event('Errors', 'toggle', visible ? 'visible' : 'hidden'),
hidePreview: () => event('Preview', 'hide-preview'),
dimTheme: (theme: string) => dimension(1, theme),
dimLanguage: (language: string) => dimension(3, language),
dimVersion: (version: string) => dimension(4, version),
dimPreview: (preview: string) => dimension(5, preview),
}

View File

@@ -1,57 +0,0 @@
import { DataModel, ModelPath } from '@mcschema/core'
import { AbstractView } from './AbstractView'
import { Mounter } from './Mounter'
import { renderHtml } from './hooks/renderHtml'
type Registry = {
[id: string]: (el: Element) => void
}
type TreeViewOptions = {
showErrors?: boolean
observer?: (el: HTMLElement) => void
nodeInjector?: (path: ModelPath, mounter: Mounter) => string
}
/**
* DOM representation view of the model.
*/
export class TreeView extends AbstractView {
target: HTMLElement
registry: Registry = {}
showErrors: boolean
observer: (el: HTMLElement) => void
nodeInjector: (path: ModelPath, mounter: Mounter) => string
/**
* @param model data model this view represents and listens to
* @param target DOM element to render the view
*/
constructor(model: DataModel, target: HTMLElement, options?: TreeViewOptions) {
super(model)
this.target = target
this.showErrors = options?.showErrors ?? false
this.observer = options?.observer ?? (() => {})
this.nodeInjector = options?.nodeInjector ?? (() => '')
}
/**
* @override
*/
invalidated() {
const mounter = new Mounter({nodeInjector: this.nodeInjector})
const path = new ModelPath(this.model)
const rendered = this.model.schema.hook(renderHtml, path, this.model.data, mounter)
const category = this.model.schema.category(path)
if (rendered[1]) {
this.target.innerHTML = `<div class="node ${this.model.schema.type(path)}-node" ${category ? `data-category="${category}"` : ''}>
<div class="node-header">${rendered[1]}</div>
<div class="node-body">${rendered[2]}</div>
</div>`
} else {
this.target.innerHTML = rendered[2]
}
mounter.mount(this.target);
this.observer(this.target)
}
}

View File

@@ -1,26 +1,17 @@
import Split from 'split.js'
import { Base, CollectionRegistry, DataModel, ModelPath, Path, SchemaRegistry } from '@mcschema/core'
import { CollectionRegistry, DataModel, ObjectNode, PathError, SchemaRegistry } from '@mcschema/core';
import * as java16 from '@mcschema/java-1.16'
import * as java17 from '@mcschema/java-1.17'
import { VisualizerView } from './visualization/VisualizerView'
import { RegistryFetcher } from './RegistryFetcher'
import { TreeView } from './TreeView'
import { SourceView } from './SourceView'
import { ErrorsView } from './ErrorsView'
import config from '../config.json'
import { BiomeNoiseVisualizer } from './visualization/BiomeNoiseVisualizer'
import { Mounter } from './Mounter'
import { getLanguage, hasLocale, locale, registerLocale, setLanguage } from './locales'
import { LocalStorageProperty } from './state/LocalStorageProperty';
import { Property } from './state/Property';
import { Preview } from './preview/Preview';
import { RegistryFetcher } from './RegistryFetcher';
import { BiomeNoisePreview } from './preview/BiomeNoisePreview';
import { NoiseSettingsPreview } from './preview/NoiseSettingsPreview';
import config from '../config.json';
import { locale, Locales } from './Locales';
import { Tracker } from './Tracker';
type ModelConfig = {
id: string
name: string
schema?: string
minVersion?: string
children?: ModelConfig[]
}
const versionSchemas: {
const Versions: {
[versionId: string]: {
getCollections: () => CollectionRegistry,
getSchemas: (collections: CollectionRegistry) => SchemaRegistry,
@@ -30,473 +21,109 @@ const versionSchemas: {
'1.17': java17
}
const LOCAL_STORAGE_THEME = 'theme'
const LOCAL_STORAGE_LANGUAGE = 'language'
const LOCAL_STORAGE_VERSION = 'schema_version'
const publicPath = '/';
const modelFromPath = (p: string) => p.replace(publicPath, '').replace(/\/$/, '')
const modelConfig = (id: string): ModelConfig => config.models.find(m => m.id === id) ?? config.models.filter(m => m.children).reduce((acc: any, cur: any) => [...acc, ...cur.children], []).find(m => m.id === id)
const addChecked = (el: HTMLElement) => {
el.classList.add('check')
setTimeout(() => {
el.classList.remove('check')
}, 2000)
export const Previews: {
[key: string]: Preview
} = {
'biome_noise': new BiomeNoisePreview(),
'noise_settings': new NoiseSettingsPreview()
}
const treeViewObserver = (el: HTMLElement) => {
el.querySelectorAll('.node[data-help]').forEach(e => {
const div = document.createElement('div')
div.className = 'node-icon'
div.addEventListener('click', evt => {
div.getElementsByTagName('span')[0].classList.add('show')
document.body.addEventListener('click', evt => {
div.getElementsByTagName('span')[0].classList.remove('show')
}, { capture: true, once: true })
})
div.insertAdjacentHTML('beforeend', `<span class="icon-popup">${e.getAttribute('data-help')}</span><svg class="node-help" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8zm6.5-.25A.75.75 0 017.25 7h1a.75.75 0 01.75.75v2.75h.25a.75.75 0 010 1.5h-2a.75.75 0 010-1.5h.25v-2h-.25a.75.75 0 01-.75-.75zM8 6a1 1 0 100-2 1 1 0 000 2z"></path></svg>`)
e.querySelector('.node-header')?.appendChild(div)
})
el.querySelectorAll('.node[data-error]').forEach(e => {
const div = document.createElement('div')
div.className = 'node-icon'
div.addEventListener('click', evt => {
div.getElementsByTagName('span')[0].classList.add('show')
document.body.addEventListener('click', evt => {
div.getElementsByTagName('span')[0].classList.remove('show')
}, { capture: true, once: true })
})
div.insertAdjacentHTML('beforeend', `<span class="icon-popup">${e.getAttribute('data-error')}</span><svg class="node-error" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8zm9 3a1 1 0 11-2 0 1 1 0 012 0zm-.25-6.25a.75.75 0 00-1.5 0v3.5a.75.75 0 001.5 0v-3.5z"></path></svg>`)
e.querySelector('.node-header')?.appendChild(div)
})
el.querySelectorAll('.collapse.closed, button.add').forEach(e => {
e.insertAdjacentHTML('afterbegin', `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.5 8a6.5 6.5 0 1113 0 6.5 6.5 0 01-13 0zM8 0a8 8 0 100 16A8 8 0 008 0zm.75 4.75a.75.75 0 00-1.5 0v2.5h-2.5a.75.75 0 000 1.5h2.5v2.5a.75.75 0 001.5 0v-2.5h2.5a.75.75 0 000-1.5h-2.5v-2.5z"></path></svg>`)
})
el.querySelectorAll('.collapse.open, button.remove').forEach(e => {
e.insertAdjacentHTML('afterbegin', `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M6.5 1.75a.25.25 0 01.25-.25h2.5a.25.25 0 01.25.25V3h-3V1.75zm4.5 0V3h2.25a.75.75 0 010 1.5H2.75a.75.75 0 010-1.5H5V1.75C5 .784 5.784 0 6.75 0h2.5C10.216 0 11 .784 11 1.75zM4.496 6.675a.75.75 0 10-1.492.15l.66 6.6A1.75 1.75 0 005.405 15h5.19c.9 0 1.652-.681 1.741-1.576l.66-6.6a.75.75 0 00-1.492-.149l-.66 6.6a.25.25 0 01-.249.225h-5.19a.25.25 0 01-.249-.225l-.66-6.6z"></path></svg>`)
})
export const Models: {
[key: string]: DataModel
} = {}
config.models.filter(m => m.schema)
.forEach(m => Models[m.id] = new DataModel(ObjectNode({})))
export const App = {
version: new LocalStorageProperty('schema_version', config.versions[config.versions.length - 1].id)
.watch(Tracker.dimVersion),
theme: new LocalStorageProperty('theme', 'light')
.watch(Tracker.dimTheme),
language: new LocalStorageProperty('language', 'en')
.watch(Tracker.dimLanguage),
model: new Property<typeof config.models[0] | null>(null),
errorsVisible: new Property(false),
jsonError: new Property<string | null>(null),
preview: new Property<Preview | null>(null)
.watch(p => Tracker.dimPreview(p?.getName() ?? 'none')),
schemasLoaded: new Property(false),
localesLoaded: new Property(false),
loaded: new Property(false),
mobilePanel: new Property('tree'),
}
const treeViewNodeInjector = (path: ModelPath, mounter: Mounter) => {
let res = VisualizerView.visualizers
.filter(v => v.active(path))
.map(v => {
const id = mounter.registerClick(() => {
ga('send', 'event', 'Preview', 'set-preview', v.getName())
views.visualizer.set(v, path)
views.visualizer.model.invalidate()
})
return `<button data-id=${id}>${locale('visualize')} <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.5 8a6.5 6.5 0 1113 0 6.5 6.5 0 01-13 0zM8 0a8 8 0 100 16A8 8 0 008 0zM6.379 5.227A.25.25 0 006 5.442v5.117a.25.25 0 00.379.214l4.264-2.559a.25.25 0 000-.428L6.379 5.227z"></path></svg></button>`
})
.join('')
if (views.visualizer.active && views.visualizer.visualizer?.getName() === 'biome-noise') {
if (path.pop().endsWith(new Path(['generator', 'biome_source', 'biomes']))) {
const biomeVisualizer = views.visualizer.visualizer as BiomeNoiseVisualizer
const biome = path.push('biome').get()
const id = mounter.registerChange(el => {
biomeVisualizer.setBiomeColor(biome, (el as HTMLInputElement).value)
views.visualizer.visualizer!.state = {}
views.visualizer.invalidated()
})
res += `<input type="color" value="${biomeVisualizer.getBiomeHex(biome)}" data-id=${id}></input>`
}
}
return res
}
const fetchLocale = async (id: string) => {
const response = await fetch(publicPath + `locales/${id}.json`)
registerLocale(id, await response.json())
}
setLanguage(localStorage.getItem(LOCAL_STORAGE_LANGUAGE)?.toLowerCase())
const homeLink = document.getElementById('home-link')!
const homeGenerators = document.getElementById('home-generators')!
const categoryGenerators = document.getElementById('category-generators')!
const selectedModel = document.getElementById('selected-model')!
const languageSelector = document.getElementById('language-selector')!
const languageSelectorMenu = document.getElementById('language-selector-menu')!
const themeSelector = document.getElementById('theme-selector')!
const treeViewEl = document.getElementById('tree-view')!
const sourceViewEl = document.getElementById('source-view')!
const errorsViewEl = document.getElementById('errors-view')!
const homeViewEl = document.getElementById('home-view')!
const errorsToggle = document.getElementById('errors-toggle')!
const sourceViewOutput = (document.getElementById('source-view-output') as HTMLTextAreaElement)
const treeViewOutput = document.getElementById('tree-view-output')!
const sourceControlsToggle = document.getElementById('source-controls-toggle')!
const sourceControlsMenu = document.getElementById('source-controls-menu')!
const sourceControlsCopy = document.getElementById('source-controls-copy')!
const sourceControlsDownload = document.getElementById('source-controls-download')!
const sourceControlsShare = document.getElementById('source-controls-share')!
const sourceToggle = document.getElementById('source-toggle')!
const treeControlsToggle = document.getElementById('tree-controls-toggle')!
const treeControlsMenu = document.getElementById('tree-controls-menu')!
const treeVersionToggle = document.getElementById('tree-version-toggle')!
const treeVersionMenu = document.getElementById('tree-version-menu')!
const treeVersionLabel = document.getElementById('tree-version-label')!
const treeControlsReset = document.getElementById('tree-controls-reset')!
const treeControlsUndo = document.getElementById('tree-controls-undo')!
const treeControlsRedo = document.getElementById('tree-controls-redo')!
const visualizerContent = document.getElementById('visualizer-content')!
const githubLink = document.getElementById('github-link')!
Split([treeViewEl, sourceViewEl], {
sizes: [66, 34]
App.version.watchRun(async (value) => {
App.schemasLoaded.set(false)
await updateSchemas(value)
App.schemasLoaded.set(true)
})
Split([sourceViewOutput, visualizerContent], {
sizes: [60, 40],
direction: 'vertical'
App.theme.watchRun((value) => document.documentElement.setAttribute('data-theme', value))
App.language.watchRun(async (value) => {
App.localesLoaded.set(false)
await updateLocale(value)
App.localesLoaded.set(true)
})
const dummyModel = new DataModel(Base)
const views = {
'tree': new TreeView(dummyModel, treeViewOutput, {
showErrors: true,
observer: treeViewObserver,
nodeInjector: treeViewNodeInjector
}),
'source': new SourceView(dummyModel, sourceViewOutput, {
indentation: 2
}),
'errors': new ErrorsView(dummyModel, errorsViewEl),
'visualizer': new VisualizerView(dummyModel, visualizerContent)
}
let version = localStorage.getItem(LOCAL_STORAGE_VERSION) ?? config.versions[0].id
treeVersionLabel.textContent = version
let COLLECTIONS = versionSchemas[version].getCollections()
Promise.all([
fetchLocale(getLanguage()),
...(getLanguage() === 'en' ? [] : [fetchLocale('en')]),
RegistryFetcher(COLLECTIONS, version)
]).then(() => {
let SCHEMAS = versionSchemas[version].getSchemas(COLLECTIONS)
let models: { [key: string]: DataModel } = {}
const buildModel = (model: any) => {
if (model.schema) {
const schema = SCHEMAS.get(model.schema)
if (schema) {
models[model.id] = new DataModel(schema)
}
} else if (model.children) {
model.children.forEach(buildModel)
}
}
config.models.forEach(buildModel)
let selected = ''
Object.values(models).forEach(m => m.validate(true))
const updateModel = () => {
let title = ''
if (models[selected] === undefined) {
title = locale('title.home')
} else {
title = locale('title.generator', [locale(selected)])
Object.values(views).forEach(v => v.setModel(models[selected]))
models[selected].invalidate()
}
selectedModel.textContent = title
document.title = title
App.localesLoaded.watch((value) => {
if (value) {
document.querySelectorAll('[data-i18n]').forEach(el => {
el.textContent = locale(el.attributes.getNamedItem('data-i18n')!.value)
})
treeVersionMenu.innerHTML = ''
const m = modelConfig(selected)
const minVersion = config.versions.findIndex(v => v.id === (m?.minVersion ?? '1.16'))
config.versions.forEach((v, i) => {
if (i > minVersion) return
const entry = document.createElement('button')
entry.classList.add('btn')
entry.textContent = v.id
entry.addEventListener('click', () => {
updateVersion(v.id)
ga('send', 'event', 'Generator', 'set-version', version)
})
treeVersionMenu.append(entry)
})
}
const updateLanguage = (id: string, store = false) => {
setLanguage(id)
ga('set', 'dimension4', id);
if (store) {
localStorage.setItem(LOCAL_STORAGE_LANGUAGE, id)
}
languageSelectorMenu.innerHTML = ''
config.languages.forEach(lang => {
languageSelectorMenu.insertAdjacentHTML('beforeend',
`<div class="btn${lang.code === getLanguage() ? ' selected' : ''}">${lang.name}</div>`)
languageSelectorMenu.lastChild?.addEventListener('click', evt => {
updateLanguage(lang.code, true)
languageSelectorMenu.style.visibility = 'hidden'
ga('send', 'event', 'Generator', 'set-language', lang.code)
})
})
if (hasLocale(id)) {
updateModel()
} else {
fetchLocale(id).then(r => {
updateModel()
})
}
}
const updateVersion = async (id: string) => {
localStorage.setItem(LOCAL_STORAGE_VERSION, id)
if (id === version) return
const newCollections = versionSchemas[id].getCollections()
await RegistryFetcher(newCollections, id)
const newSchemas = versionSchemas[id].getSchemas(newCollections)
const fixModel = (model: ModelConfig) => {
if (model.schema) {
const minVersion = config.versions.findIndex(v => v.id === (model.minVersion ?? '1.16'))
const targetVersion = config.versions.findIndex(v => v.id === id)
if (minVersion >= targetVersion) {
const schema = newSchemas.get(model.schema)
if (models[model.id] === undefined) {
models[model.id] = new DataModel(schema)
} else {
models[model.id].schema = schema
}
models[model.id].validate()
models[model.id].invalidate()
} else {
delete models[model.id]
}
} else if (model.children) {
model.children.forEach(fixModel)
}
}
config.models.forEach(fixModel)
treeVersionLabel.textContent = id
version = id
ga('set', 'dimension3', version);
}
homeLink.addEventListener('click', evt => {
reload(publicPath)
})
window.onpopstate = (evt: PopStateEvent) => {
reload(location.pathname)
}
sourceToggle.addEventListener('click', evt => {
if (sourceViewEl.classList.contains('active')) {
sourceViewEl.classList.remove('active')
sourceToggle.classList.remove('toggled')
} else {
sourceViewEl.classList.add('active')
sourceToggle.classList.add('toggled')
}
})
languageSelector.addEventListener('click', evt => {
languageSelectorMenu.style.visibility = 'visible'
document.body.addEventListener('click', evt => {
languageSelectorMenu.style.visibility = 'hidden'
}, { capture: true, once: true })
})
const updateTheme = (theme: string | null) => {
ga('set', 'dimension1', theme ?? 'default');
if (theme === null) return
if (theme === 'dark') {
document.documentElement.setAttribute('data-theme', 'dark')
themeSelector.classList.add('toggled')
localStorage.setItem(LOCAL_STORAGE_THEME, 'dark')
} else {
document.documentElement.setAttribute('data-theme', 'light')
themeSelector.classList.remove('toggled')
localStorage.setItem(LOCAL_STORAGE_THEME, 'light')
}
}
updateTheme(localStorage.getItem(LOCAL_STORAGE_THEME))
themeSelector.addEventListener('click', evt => {
if (document.documentElement.getAttribute('data-theme') === 'dark') {
updateTheme('light')
} else {
updateTheme('dark')
}
ga('send', 'event', 'Generator', 'set-theme', document.documentElement.getAttribute('data-theme'))
})
sourceControlsToggle.addEventListener('click', evt => {
sourceControlsMenu.style.visibility = 'visible'
document.body.addEventListener('click', evt => {
sourceControlsMenu.style.visibility = 'hidden'
}, { capture: true, once: true })
})
sourceControlsCopy.addEventListener('click', evt => {
sourceViewOutput.select()
document.execCommand('copy');
addChecked(sourceControlsCopy)
ga('send', 'event', 'JsonOutput', 'copy')
})
sourceControlsDownload.addEventListener('click', evt => {
const fileContents = encodeURIComponent(JSON.stringify(models[selected].data, null, 2) + "\n")
const dataString = "data:text/json;charset=utf-8," + fileContents
const downloadAnchor = document.getElementById('source-controls-download-anchor')!
downloadAnchor.setAttribute("href", dataString)
downloadAnchor.setAttribute("download", "data.json")
downloadAnchor.click()
ga('send', 'event', 'JsonOutput', 'download')
})
sourceControlsShare.addEventListener('click', evt => {
const data = btoa(JSON.stringify(JSON.parse(views.source.target.value)));
const url = window.location.origin + window.location.pathname + '?q=' + data
const shareInput = document.getElementById('source-controls-share-input') as HTMLInputElement
shareInput.value = url
shareInput.style.display = 'inline-block'
document.body.addEventListener('click', evt => {
shareInput.style.display = 'none'
}, { capture: true, once: true })
shareInput.select()
document.execCommand('copy');
ga('send', 'event', 'JsonOutput', 'share')
})
treeControlsToggle.addEventListener('click', evt => {
treeControlsMenu.style.visibility = 'visible'
document.body.addEventListener('click', evt => {
treeControlsMenu.style.visibility = 'hidden'
}, { capture: true, once: true })
})
treeVersionToggle.addEventListener('click', evt => {
treeVersionMenu.style.visibility = 'visible'
document.body.addEventListener('click', evt => {
treeVersionMenu.style.visibility = 'hidden'
}, { capture: true, once: true })
})
treeControlsReset.addEventListener('click', evt => {
models[selected].reset(models[selected].schema.default(), true)
addChecked(treeControlsReset)
ga('send', 'event', 'Generator', 'reset')
})
treeControlsUndo.addEventListener('click', evt => {
models[selected].undo()
ga('send', 'event', 'Generator', 'undo', 'Menu')
})
treeControlsRedo.addEventListener('click', evt => {
models[selected].redo()
ga('send', 'event', 'Generator', 'redo', 'Menu')
})
document.addEventListener('keyup', evt => {
if (evt.ctrlKey && evt.key === 'z') {
models[selected].undo()
ga('send', 'event', 'Generator', 'undo', 'Hotkey')
} else if (evt.ctrlKey && evt.key === 'y') {
models[selected].redo()
ga('send', 'event', 'Generator', 'redo', 'Hotkey')
}
})
errorsToggle.addEventListener('click', evt => {
if (errorsViewEl.classList.contains('active')) {
errorsViewEl.classList.remove('active')
errorsToggle.classList.remove('toggled')
} else {
errorsViewEl.classList.add('active')
errorsToggle.classList.add('toggled')
}
ga('send', 'event', 'Errors', 'toggle', errorsViewEl.classList.contains('active') ? 'visible' : 'hidden')
})
githubLink.addEventListener('click', () => {
window.open('https://github.com/misode/misode.github.io', '_blank')
})
const reload = (target: string, track=true) => {
if (!target.endsWith('/')) {
target = `${target}/`
}
if (target.startsWith('/dev/')) {
reload(target.slice(4))
return
}
if (track) {
ga('set', 'page', target)
ga('send', 'pageview');
history.pushState(target, 'Change Page', target)
}
selected = modelFromPath(target) ?? ''
const params = new URLSearchParams(window.location.search);
const panels = [treeViewEl, sourceViewEl, errorsViewEl]
if (['', 'worldgen'].includes(selected)) {
homeViewEl.style.display = '';
(document.querySelector('.gutter') as HTMLElement).style.display = 'none';
(document.querySelector('.content') as HTMLElement).style.overflowY = 'initial'
panels.forEach(v => v.style.display = 'none')
const addGen = (output: HTMLElement) => (m: any) => {
output.insertAdjacentHTML('beforeend',
`<a class="generators-card${m.id === selected ? ' selected' : ''}" href="${publicPath + m.id}">
${locale(m.name)}
${m.schema ? '' : '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M6.22 3.22a.75.75 0 011.06 0l4.25 4.25a.75.75 0 010 1.06l-4.25 4.25a.75.75 0 01-1.06-1.06L9.94 8 6.22 4.28a.75.75 0 010-1.06z"></path></svg>'}
</a>`)
output.lastChild?.addEventListener('click', evt => {
reload(publicPath + m.id)
evt.preventDefault()
})
}
homeGenerators.innerHTML = ''
categoryGenerators.innerHTML = ''
config.models.forEach(addGen(homeGenerators))
config.models.find(m => m.id === selected)?.children?.forEach(addGen(categoryGenerators))
} else {
homeViewEl.style.display = 'none';
(document.querySelector('.gutter') as HTMLElement).style.display = ''
panels.forEach(v => v.style.display = '')
if (models[selected] === undefined) {
const m = modelConfig(selected)
const loadedVersion = config.versions.findIndex(v => v.id === version)
const minVersion = config.versions.findIndex(v => v.id === m.minVersion)
if (minVersion < loadedVersion) {
updateVersion(m.minVersion!).then(() => {
updateModel()
})
}
}
if (params.has('q')) {
const data = atob(params.get('q')!)
models[selected].reset(JSON.parse(data))
}
}
updateLanguage(getLanguage())
}
reload(location.pathname, false)
document.body.style.visibility = 'initial'
App.loaded.set(value && App.schemasLoaded.get())
})
App.schemasLoaded.watch((value) => {
App.loaded.set(value && App.localesLoaded.get())
})
App.mobilePanel.watchRun((value) => {
document.body.setAttribute('data-panel', value)
})
async function updateSchemas(version: string) {
const collections = Versions[version].getCollections()
await RegistryFetcher(collections, version)
const schemas = Versions[version].getSchemas(collections)
config.models
.filter(m => m.schema)
.filter(m => checkVersion(App.version.get(), m.minVersion ?? config.versions[0].id))
.forEach(m => {
const model = Models[m.id]
const schema = schemas.get(m.schema!)
if (schema) {
model.schema = schema
if (JSON.stringify(model.data) === '{}') {
model.data = schema.default()
model.history = [JSON.stringify(model.data)]
model.historyIndex = 0
model.silentInvalidate()
}
}
})
}
async function updateLocale(language: string) {
if (Locales[language]) return
const data = await (await fetch(`/locales/${language}.json`)).json()
Locales[language] = data
}
export function checkVersion(versionId: string, minVersionId: string) {
const version = config.versions.findIndex(v => v.id === versionId)
const minVersion = config.versions.findIndex(v => v.id === minVersionId)
return minVersion <= version
}
document.addEventListener('keyup', (evt) => {
if (evt.ctrlKey && evt.key === 'z') {
Tracker.undo(true)
Models[App.model.get()!.id].undo()
} else if (evt.ctrlKey && evt.key === 'y') {
Tracker.redo(true)
Models[App.model.get()!.id].redo()
}
})

View File

@@ -0,0 +1,22 @@
import { Property } from '../state/Property';
import { View } from '../views/View';
import { Octicon } from './Octicon';
export const Dropdown = (view: View, icon: keyof typeof Octicon, entries: [string, string][], state: Property<string>, watcher?: (value: string) => void) => {
const dropdown = view.register(el => {
el.addEventListener('change', () => {
state.set((el as HTMLSelectElement).value)
})
state.watchRun(v => (el as HTMLSelectElement).value = v, 'dropdown')
watcher?.(state.get())
})
return `
<div class="dropdown">
<select data-id="${dropdown}">
${entries.map(e => `
<option value=${e[0]}>${e[1]}</option>
`).join('')}
</select>
${Octicon[icon]}
</div>`
}

View File

@@ -0,0 +1,53 @@
import { DataModel } from '@mcschema/core';
import { App } from '../App';
import { locale } from '../Locales';
import { View } from '../views/View';
import { Octicon } from './Octicon';
import { Toggle } from './Toggle';
import { htmlEncode } from '../Utils'
import { Tracker } from '../Tracker';
export const Errors = (view: View, model: DataModel) => {
const getContent = () => {
if (App.jsonError.get()) {
return `<div class="error-list">
<div class="error">
${htmlEncode(App.jsonError.get()!)}
</div>
</div>
<div class="toggle" style="cursor: initial;">
${Octicon.issue_opened}
</div>`
}
if (model.errors.count() === 0) return ''
return `${App.errorsVisible.get() ? `
<div class="error-list">
${model.errors.getAll().map(e => `
<div class="error">
<span class="error-path">${e.path.toString()}</span>
<span>-</span>
<span class="error-message">${htmlEncode(locale(e.error, e.params))}</span>
</div>
`).join('')}
</div>
` : ''}
${Toggle(view, [[true, 'chevron_down'], [false, 'issue_opened']], App.errorsVisible, Tracker.toggleErrors)}`
}
const errors = view.register(el => {
model.addListener({
errors() {
view.mount(el, getContent(), false)
}
})
App.jsonError.watch(() => {
view.mount(el, getContent(), false)
})
App.errorsVisible.watch(() => {
view.mount(el, getContent(), false)
}, 'errors')
})
return `
<div class="errors" data-id="${errors}">
${getContent()}
</div>`
}

View File

@@ -0,0 +1,28 @@
import { App } from '../App';
import { View } from '../views/View';
import { Dropdown } from './Dropdown';
import { Octicon } from './Octicon';
import { Toggle } from './Toggle';
import { languages } from '../../config.json'
import { Tracker } from '../Tracker';
export const Header = (view: View, title: string, homeLink = '/', panelToggleVisible = false) => `
<header>
<div class="header-title">
<a data-link href="${homeLink}" class="home-link">${Octicon.three_bars}</a>
<h2>${title}</h2>
</div>
<nav>
${panelToggleVisible ? Toggle(view, [['tree', 'code'], ['source', 'note']], App.mobilePanel) : ''}
<ul>
<li>${Dropdown(view, 'globe', languages.map(l => [l.code, l.name]), App.language, Tracker.setLanguage)}</li>
<li>${Toggle(view, [['dark', 'sun'], ['light', 'moon']], App.theme, Tracker.setTheme)}</li>
<li class="dimmed">
<a href="https://github.com/misode/misode.github.io" target="_blank">
${Octicon.mark_github}
</a>
</li>
</ul>
</nav>
</header>
`

View File

@@ -0,0 +1,25 @@
export const Octicon = {
arrow_left: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.78 12.53a.75.75 0 01-1.06 0L2.47 8.28a.75.75 0 010-1.06l4.25-4.25a.75.75 0 011.06 1.06L4.81 7h7.44a.75.75 0 010 1.5H4.81l2.97 2.97a.75.75 0 010 1.06z"></path></svg>',
arrow_right: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8.22 2.97a.75.75 0 011.06 0l4.25 4.25a.75.75 0 010 1.06l-4.25 4.25a.75.75 0 01-1.06-1.06l2.97-2.97H3.75a.75.75 0 010-1.5h7.44L8.22 4.03a.75.75 0 010-1.06z"></path></svg>',
chevron_down: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M12.78 6.22a.75.75 0 010 1.06l-4.25 4.25a.75.75 0 01-1.06 0L3.22 7.28a.75.75 0 011.06-1.06L8 9.94l3.72-3.72a.75.75 0 011.06 0z"></path></svg>',
chevron_right: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M6.22 3.22a.75.75 0 011.06 0l4.25 4.25a.75.75 0 010 1.06l-4.25 4.25a.75.75 0 01-1.06-1.06L9.94 8 6.22 4.28a.75.75 0 010-1.06z"></path></svg>',
clippy: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M5.75 1a.75.75 0 00-.75.75v3c0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75v-3a.75.75 0 00-.75-.75h-4.5zm.75 3V2.5h3V4h-3zm-2.874-.467a.75.75 0 00-.752-1.298A1.75 1.75 0 002 3.75v9.5c0 .966.784 1.75 1.75 1.75h8.5A1.75 1.75 0 0014 13.25v-9.5a1.75 1.75 0 00-.874-1.515.75.75 0 10-.752 1.298.25.25 0 01.126.217v9.5a.25.25 0 01-.25.25h-8.5a.25.25 0 01-.25-.25v-9.5a.25.25 0 01.126-.217z"></path></svg>',
code: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M4.72 3.22a.75.75 0 011.06 1.06L2.06 8l3.72 3.72a.75.75 0 11-1.06 1.06L.47 8.53a.75.75 0 010-1.06l4.25-4.25zm6.56 0a.75.75 0 10-1.06 1.06L13.94 8l-3.72 3.72a.75.75 0 101.06 1.06l4.25-4.25a.75.75 0 000-1.06l-4.25-4.25z"></path></svg>',
download: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.47 10.78a.75.75 0 001.06 0l3.75-3.75a.75.75 0 00-1.06-1.06L8.75 8.44V1.75a.75.75 0 00-1.5 0v6.69L4.78 5.97a.75.75 0 00-1.06 1.06l3.75 3.75zM3.75 13a.75.75 0 000 1.5h8.5a.75.75 0 000-1.5h-8.5z"></path></svg>',
globe: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.543 7.25h2.733c.144-2.074.866-3.756 1.58-4.948.12-.197.237-.381.353-.552a6.506 6.506 0 00-4.666 5.5zm2.733 1.5H1.543a6.506 6.506 0 004.666 5.5 11.13 11.13 0 01-.352-.552c-.715-1.192-1.437-2.874-1.581-4.948zm1.504 0h4.44a9.637 9.637 0 01-1.363 4.177c-.306.51-.612.919-.857 1.215a9.978 9.978 0 01-.857-1.215A9.637 9.637 0 015.78 8.75zm4.44-1.5H5.78a9.637 9.637 0 011.363-4.177c.306-.51.612-.919.857-1.215.245.296.55.705.857 1.215A9.638 9.638 0 0110.22 7.25zm1.504 1.5c-.144 2.074-.866 3.756-1.58 4.948-.12.197-.237.381-.353.552a6.506 6.506 0 004.666-5.5h-2.733zm2.733-1.5h-2.733c-.144-2.074-.866-3.756-1.58-4.948a11.738 11.738 0 00-.353-.552 6.506 6.506 0 014.666 5.5zM8 0a8 8 0 100 16A8 8 0 008 0z"></path></svg>',
history: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.643 3.143L.427 1.927A.25.25 0 000 2.104V5.75c0 .138.112.25.25.25h3.646a.25.25 0 00.177-.427L2.715 4.215a6.5 6.5 0 11-1.18 4.458.75.75 0 10-1.493.154 8.001 8.001 0 101.6-5.684zM7.75 4a.75.75 0 01.75.75v2.992l2.028.812a.75.75 0 01-.557 1.392l-2.5-1A.75.75 0 017 8.25v-3.5A.75.75 0 017.75 4z"></path></svg>',
info: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8zm6.5-.25A.75.75 0 017.25 7h1a.75.75 0 01.75.75v2.75h.25a.75.75 0 010 1.5h-2a.75.75 0 010-1.5h.25v-2h-.25a.75.75 0 01-.75-.75zM8 6a1 1 0 100-2 1 1 0 000 2z"></path></svg>',
issue_opened: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8zm9 3a1 1 0 11-2 0 1 1 0 012 0zm-.25-6.25a.75.75 0 00-1.5 0v3.5a.75.75 0 001.5 0v-3.5z"></path></svg>',
kebab_horizontal: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path d="M8 9a1.5 1.5 0 100-3 1.5 1.5 0 000 3zM1.5 9a1.5 1.5 0 100-3 1.5 1.5 0 000 3zm13 0a1.5 1.5 0 100-3 1.5 1.5 0 000 3z"></path></svg>',
link: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg>',
mark_github: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path></svg>',
moon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M9.598 1.591a.75.75 0 01.785-.175 7 7 0 11-8.967 8.967.75.75 0 01.961-.96 5.5 5.5 0 007.046-7.046.75.75 0 01.175-.786zm1.616 1.945a7 7 0 01-7.678 7.678 5.5 5.5 0 107.678-7.678z"></path></svg>',
note: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M0 3.75C0 2.784.784 2 1.75 2h12.5c.966 0 1.75.784 1.75 1.75v8.5A1.75 1.75 0 0114.25 14H1.75A1.75 1.75 0 010 12.25v-8.5zm1.75-.25a.25.25 0 00-.25.25v8.5c0 .138.112.25.25.25h12.5a.25.25 0 00.25-.25v-8.5a.25.25 0 00-.25-.25H1.75zM3.5 6.25a.75.75 0 01.75-.75h7a.75.75 0 010 1.5h-7a.75.75 0 01-.75-.75zm.75 2.25a.75.75 0 000 1.5h4a.75.75 0 000-1.5h-4z"></path></svg>',
play: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.5 8a6.5 6.5 0 1113 0 6.5 6.5 0 01-13 0zM8 0a8 8 0 100 16A8 8 0 008 0zM6.379 5.227A.25.25 0 006 5.442v5.117a.25.25 0 00.379.214l4.264-2.559a.25.25 0 000-.428L6.379 5.227z"></path></svg>',
plus_circle: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.5 8a6.5 6.5 0 1113 0 6.5 6.5 0 01-13 0zM8 0a8 8 0 100 16A8 8 0 008 0zm.75 4.75a.75.75 0 00-1.5 0v2.5h-2.5a.75.75 0 000 1.5h2.5v2.5a.75.75 0 001.5 0v-2.5h2.5a.75.75 0 000-1.5h-2.5v-2.5z"></path></svg>',
sun: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8 10.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5zM8 12a4 4 0 100-8 4 4 0 000 8zM8 0a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0V.75A.75.75 0 018 0zm0 13a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 018 13zM2.343 2.343a.75.75 0 011.061 0l1.06 1.061a.75.75 0 01-1.06 1.06l-1.06-1.06a.75.75 0 010-1.06zm9.193 9.193a.75.75 0 011.06 0l1.061 1.06a.75.75 0 01-1.06 1.061l-1.061-1.06a.75.75 0 010-1.061zM16 8a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0116 8zM3 8a.75.75 0 01-.75.75H.75a.75.75 0 010-1.5h1.5A.75.75 0 013 8zm10.657-5.657a.75.75 0 010 1.061l-1.061 1.06a.75.75 0 11-1.06-1.06l1.06-1.06a.75.75 0 011.06 0zm-9.193 9.193a.75.75 0 010 1.06l-1.06 1.061a.75.75 0 11-1.061-1.06l1.06-1.061a.75.75 0 011.061 0z"></path></svg>',
tag: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M2.5 7.775V2.75a.25.25 0 01.25-.25h5.025a.25.25 0 01.177.073l6.25 6.25a.25.25 0 010 .354l-5.025 5.025a.25.25 0 01-.354 0l-6.25-6.25a.25.25 0 01-.073-.177zm-1.5 0V2.75C1 1.784 1.784 1 2.75 1h5.025c.464 0 .91.184 1.238.513l6.25 6.25a1.75 1.75 0 010 2.474l-5.026 5.026a1.75 1.75 0 01-2.474 0l-6.25-6.25A1.75 1.75 0 011 7.775zM6 5a1 1 0 100 2 1 1 0 000-2z"></path></svg>',
trashcan: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M6.5 1.75a.25.25 0 01.25-.25h2.5a.25.25 0 01.25.25V3h-3V1.75zm4.5 0V3h2.25a.75.75 0 010 1.5H2.75a.75.75 0 010-1.5H5V1.75C5 .784 5.784 0 6.75 0h2.5C10.216 0 11 .784 11 1.75zM4.496 6.675a.75.75 0 10-1.492.15l.66 6.6A1.75 1.75 0 005.405 15h5.19c.9 0 1.652-.681 1.741-1.576l.66-6.6a.75.75 0 00-1.492-.149l-.66 6.6a.25.25 0 01-.249.225h-5.19a.25.25 0 01-.249-.225l-.66-6.6z"></path></svg>',
three_bars: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1 2.75A.75.75 0 011.75 2h12.5a.75.75 0 110 1.5H1.75A.75.75 0 011 2.75zm0 5A.75.75 0 011.75 7h12.5a.75.75 0 110 1.5H1.75A.75.75 0 011 7.75zM1.75 12a.75.75 0 100 1.5h12.5a.75.75 0 100-1.5H1.75z"></path></svg>',
x: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z"></path></svg>',
}

View File

@@ -0,0 +1,10 @@
import Split from 'split.js'
import { View } from '../views/View';
export const SplitGroup = (view: View, options: Split.Options, entries: string[]) => `
<div class="split-group ${options.direction ?? 'horizontal'}" data-id=${view.register(el => {
Split([].slice.call(el.children), { snapOffset: 0, ...options })
})}>
${entries.join('')}
</div>
`

View File

@@ -0,0 +1,16 @@
import { Property } from '../state/Property';
import { View } from '../views/View';
import { Octicon } from './Octicon';
export const Toggle = <T>(view: View, entries: [T, keyof typeof Octicon][], state: Property<T>, watcher?: (value: T) => void) => {
const activeOcticon = () => Octicon[(entries.find(e => e[0] === state.get()) ?? entries[0])[1]]
const toggle = view.register(el => {
el.addEventListener('click', () => {
const i = entries.findIndex(e => e[0] === state.get())
state.set(entries[(i + 1) % entries.length][0])
})
state.watch(_ => el.innerHTML = activeOcticon(), 'toggle')
watcher?.(state.get())
})
return `<div class="toggle" data-id="${toggle}">${activeOcticon()}</div>`
}

View File

@@ -0,0 +1,64 @@
import { DataModel } from '@mcschema/core';
import { App, Previews } from '../../App';
import { BiomeNoisePreview } from '../../preview/BiomeNoisePreview';
import { Tracker } from '../../Tracker';
import { View } from '../../views/View';
import { Octicon } from '../Octicon';
export const PreviewPanel = (view: View, model: DataModel) => {
const canvas = view.register(el => {
const redraw = () => {
const preview = App.preview.get()
if (preview && preview.path && preview.path.withModel(model).get()) {
const ctx = (el as HTMLCanvasElement).getContext('2d')!
const img = ctx.createImageData(200, 100)
const newState = preview.path.withModel(model).get()
preview.state = JSON.parse(JSON.stringify(newState))
preview.draw(model, img)
ctx.putImageData(img, 0, 0)
} else {
App.preview.set(null)
}
}
model.addListener({
invalidated: redraw
})
App.preview.watchRun((value) => {
if (value) {
redraw()
}
}, 'preview-panel')
;(Previews.biome_noise as BiomeNoisePreview).biomeColors.watch(() => {
if (App.preview.get()?.getName() === 'biome-noise') {
redraw()
}
}, 'preview-panel')
let dragStart: number[] | undefined
(el as HTMLCanvasElement).addEventListener('mousedown', evt => {
dragStart = [evt.offsetX, evt.offsetY]
})
;(el as HTMLCanvasElement).addEventListener('mousemove', evt => {
if (dragStart === undefined) return
if (App.preview.get()?.onDrag) {
App.preview.get()?.onDrag(dragStart[0], dragStart[1], evt.offsetX, evt.offsetY)
redraw()
}
dragStart = [evt.offsetX, evt.offsetY]
})
;(el as HTMLCanvasElement).addEventListener('mouseup', evt => {
dragStart = undefined
})
})
return `<div class="panel preview-panel">
<div class="panel-controls">
<div class="btn" data-id="${view.onClick(() => {
Tracker.hidePreview(); App.preview.set(null)
})}">
${Octicon.x}
</div>
</div>
<canvas width="200" height="100" data-id="${canvas}">
</div>`
}

View File

@@ -0,0 +1,78 @@
import { DataModel, ModelPath, Path } from '@mcschema/core';
import { Tracker } from '../../Tracker';
import { transformOutput } from '../../hooks/transformOutput';
import { View } from '../../views/View';
import { Octicon } from '../Octicon';
import { App } from '../../App';
export const SourcePanel = (view: View, model: DataModel) => {
const updateContent = (el: HTMLTextAreaElement) => {
const data = model.schema.hook(transformOutput, new ModelPath(model), model.data);
el.value = JSON.stringify(data, null, 2)
}
const source = view.register(el => {
updateContent(el as HTMLTextAreaElement)
model.addListener({
invalidated() {
App.jsonError.set(null)
updateContent(el as HTMLTextAreaElement)
}
})
el.addEventListener('change', () => {
const rawSource = (el as HTMLTextAreaElement).value
try {
model.reset(JSON.parse(rawSource))
App.jsonError.set(null)
} catch (err) {
App.jsonError.set(err.message)
}
})
})
const copySource = (el: Element) => {
el.closest('.panel')?.getElementsByTagName('textarea')[0].select()
document.execCommand('copy');
Tracker.copy()
}
const downloadSource = (el: Element) => {
const fileContents = encodeURIComponent(JSON.stringify(model.data, null, 2) + "\n")
const downloadAnchor = el.lastElementChild as HTMLAnchorElement
downloadAnchor.setAttribute('href', 'data:text/json;charset=utf-8,' + fileContents)
downloadAnchor.setAttribute("download", "data.json")
downloadAnchor.click()
Tracker.download()
}
const shareSource = (el: Element) => {
el.closest('.panel-controls')?.querySelector('input')
Tracker.share()
}
const toggleMenu = (el: Element) => {
el.classList.toggle('active')
document.body.addEventListener('click', evt => {
el.classList.remove('active')
}, { capture: true, once: true })
}
return `<div class="panel source-panel">
<div class="panel-controls">
<input style="display: none;">
<div class="btn" data-id="${view.onClick(copySource)}">
${Octicon.clippy}
<span data-i18n="copy"></span>
</div>
<div class="panel-menu">
<div class="btn" data-id="${view.onClick(toggleMenu)}">
${Octicon.kebab_horizontal}
</div>
<div class="panel-menu-list btn-group">
<div class="btn" data-id="${view.onClick(downloadSource)}">
${Octicon.download}<span data-i18n="download"></span>
<a style="diplay: none;"></a>
</div>
<div class="btn" data-id="${view.onClick(shareSource)}">
${Octicon.link}<span data-i18n="share"></span>
</div>
</div>
</div>
</div>
<textarea class="source" data-id="${source}" spellcheck="false" autocorrect="off" autocapitalize="off"></textarea>
</div>`
}

View File

@@ -0,0 +1,161 @@
import { DataModel, ModelPath, Path } from '@mcschema/core';
import { App, checkVersion, Previews } from '../../App';
import { Tracker } from '../../Tracker'
import { View } from '../../views/View';
import { Octicon } from '../Octicon';
import { Mounter } from '../../Mounter';
import { renderHtml } from '../../hooks/renderHtml';
import config from '../../../config.json'
import { locale } from '../../Locales';
import { BiomeNoisePreview } from '../../preview/BiomeNoisePreview';
const createPopupIcon = (type: string, icon: keyof typeof Octicon, popup: string) => {
const div = document.createElement('div')
div.className = `node-icon ${type}`
div.addEventListener('click', evt => {
div.getElementsByTagName('span')[0].classList.add('show')
document.body.addEventListener('click', evt => {
div.getElementsByTagName('span')[0].classList.remove('show')
}, { capture: true, once: true })
})
div.insertAdjacentHTML('beforeend', `<span class="icon-popup">${popup}</span>${Octicon[icon]}`)
return div
}
const treeViewObserver = (el: Element) => {
el.querySelectorAll('.node[data-help]').forEach(e => {
e.querySelector('.node-header')?.appendChild(
createPopupIcon('node-help', 'info', e.getAttribute('data-help') ?? ''))
})
el.querySelectorAll('.node[data-error]').forEach(e => {
e.querySelector('.node-header')?.appendChild(
createPopupIcon('node-error', 'issue_opened', e.getAttribute('data-error') ?? ''))
})
el.querySelectorAll('.collapse.closed, button.add').forEach(e => {
e.insertAdjacentHTML('afterbegin', Octicon.plus_circle)
})
el.querySelectorAll('.collapse.open, button.remove').forEach(e => {
e.insertAdjacentHTML('afterbegin', Octicon.trashcan)
})
}
const treeViewNodeInjector = (path: ModelPath, mounter: Mounter) => {
let res = Object.keys(Previews).map(k => Previews[k])
.filter(v => v.active(path))
.map(v => {
const id = mounter.registerClick(() => {
Tracker.setPreview(v.getName())
v.path = path
App.preview.set(v)
})
return `<button data-id=${id}>${locale('preview')} ${Octicon.play}</button>`
}).join('')
if (path.pop().endsWith(new Path(['generator', 'biome_source', 'biomes']))) {
const biomePreview = Previews.biome_noise as BiomeNoisePreview
const biome = path.push('biome').get()
const id = mounter.registerChange(el => {
biomePreview.setBiomeColor(biome, (el as HTMLInputElement).value)
biomePreview.state = {}
})
res += `<input type="color" value="${biomePreview.getBiomeHex(biome)}" data-id=${id}></input>`
}
return res
}
export const TreePanel = (view: View, model: DataModel) => {
const mounter = new Mounter({ nodeInjector: treeViewNodeInjector })
const getContent = () => {
if (App.loaded.get()) {
const path = new ModelPath(model)
const rendered = model.schema.hook(renderHtml, path, model.data, mounter)
const category = model.schema.category(path)
if (rendered[1]) {
return `<div class="node ${model.schema.type(path)}-node" ${category ? `data-category="${category}"` : ''}>
<div class="node-header">${rendered[1]}</div>
<div class="node-body">${rendered[2]}</div>
</div>`
}
return rendered[2]
}
return '<div class="spinner"></div>'
}
const mountContent = (el: Element) => {
el.innerHTML = getContent()
treeViewObserver(el)
mounter.mount(el)
}
const tree = view.register(el => {
App.loaded.watchRun((value) => {
if (!value) {
// If loading is taking more than 100 ms, show spinner
new Promise(r => setTimeout(r, 100)).then(() => {
if (!App.loaded.get()) {
mountContent(el)
}
})
} else {
mountContent(el)
}
})
model.addListener({
invalidated() {
mountContent(el)
}
})
;(Previews.biome_noise as BiomeNoisePreview).biomeColors.watch(() => {
mountContent(el)
}, 'tree-panel')
})
const toggleMenu = (el: Element) => {
el.classList.toggle('active')
document.body.addEventListener('click', evt => {
el.classList.remove('active')
}, { capture: true, once: true })
}
return `<div class="panel tree-panel">
<div class="panel-controls">
<div class="btn" data-id="${view.onClick(() => {
Tracker.reset(); model.reset(model.schema.default())
})}">
${Octicon.history}
<span data-i18n="reset"></span>
</div>
<div class="panel-menu">
<div class="btn" data-id="${view.onClick(toggleMenu)}">
${Octicon.tag}
<span data-id="${view.register(el => App.version.watch(v => el.textContent = v, 'tree-controls'))}">
${App.version.get()}
</span>
</div>
<div class="panel-menu-list btn-group">
${config.versions
.filter(v => checkVersion(v.id, App.model.get()!.minVersion ?? '1.16'))
.reverse()
.map(v => `
<div class="btn" data-id="${view.onClick(() => {
Tracker.setVersion(v.id); App.version.set(v.id)
})}">
${v.id}
</div>
`).join('')}
</div>
</div>
<div class="panel-menu">
<div class="btn" data-id="${view.onClick(toggleMenu)}">
${Octicon.kebab_horizontal}
</div>
<div class="panel-menu-list btn-group">
<div class="btn" data-id="${view.onClick(() => {Tracker.undo(); model.undo()})}">
${Octicon.arrow_left}<span data-i18n="undo"></span>
</div>
<div class="btn" data-id="${view.onClick(() => {Tracker.redo(); model.redo()})}">
${Octicon.arrow_right}<span data-i18n="redo"></span>
</div>
</div>
</div>
</div>
<div class="tree" data-id="${tree}"></div>
</div>`
}

View File

@@ -1,7 +1,7 @@
import { Hook, ModelPath, Path, StringHookParams, ValidationOption, EnumOption, INode, DataModel, MapNode, StringNode } from '@mcschema/core'
import { locale, pathLocale, segmentedLocale } from '../locales'
import { locale, segmentedLocale } from '../Locales'
import { Mounter } from '../Mounter'
import { hexId } from '../utils'
import { hexId, htmlEncode } from '../Utils'
/**
* Secondary model used to remember the keys of a map
@@ -248,6 +248,12 @@ function hashString(str: string) {
return hash;
}
function pathLocale(path: Path, params?: string[]): string {
// return path.getContext().slice(-5).join('.')
return segmentedLocale(path.getContext(), params)
?? path.getContext()[path.getContext().length - 1] ?? ''
}
function error(p: ModelPath, exact = true) {
const errors = p.model.errors.get(p, exact)
if (errors.length === 0) return ''
@@ -259,8 +265,3 @@ function help(path: ModelPath) {
if (message === undefined) return ''
return `data-help="${htmlEncode(message)}"`
}
function htmlEncode(str: string) {
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;').replace(/'/g, '&#x27;').replace(/\//g, '&#x2F;')
}

View File

@@ -1,29 +1,14 @@
import { ModelPath, Path } from '@mcschema/core'
import English from '../locales/en.json'
import { App } from './App'
interface Locale {
[key: string]: string
}
const Locales: {
export const Locales: {
[key: string]: Locale
} = {}
let language = 'en'
export function registerLocale(code: string, locale: Locale) {
Locales[code] = locale
}
export function hasLocale(code: string) {
return Locales[code] !== undefined
}
export function setLanguage(code: string | undefined) {
language = code ?? language
}
export function getLanguage() {
return language
} = {
'en': English
}
export function resolveLocaleParams(value: string, params?: string[]): string | undefined {
@@ -34,17 +19,17 @@ export function resolveLocaleParams(value: string, params?: string[]): string |
}
export function locale(key: string, params?: string[]): string {
const value: string | undefined = Locales[language][key] ?? Locales.en[key]
const value: string | undefined = Locales[App.language.get()]?.[key] ?? Locales.en[key]
return resolveLocaleParams(value, params) ?? key
}
export function segmentedLocale(segments: string[], params?: string[], depth = 5, minDepth = 1): string | undefined {
return [language, 'en'].reduce((prev: string | undefined, code) => {
return [App.language.get(), 'en'].reduce((prev: string | undefined, code) => {
if (prev !== undefined) return prev
const array = segments.slice(-depth);
while (array.length >= minDepth) {
const locale = resolveLocaleParams(Locales[code][array.join('.')], params)
const locale = resolveLocaleParams(Locales[code]?.[array.join('.')], params)
if (locale !== undefined) return locale
array.shift()
}
@@ -52,9 +37,3 @@ export function segmentedLocale(segments: string[], params?: string[], depth = 5
return undefined
}, undefined)
}
export function pathLocale(path: Path, params?: string[]): string {
// return path.getContext().slice(-5).join('.')
return segmentedLocale(path.getContext(), params)
?? path.getContext()[path.getContext().length - 1] ?? ''
}

View File

@@ -1,23 +1,24 @@
import { DataModel, Path, ModelPath } from "@mcschema/core"
import { Property } from "../state/Property"
import { NormalNoise } from './NormalNoise'
import { Visualizer } from './Visualizer'
import { VisualizerView } from './VisualizerView'
import { Preview } from './Preview'
const LOCAL_STORAGE_BIOME_COLORS = 'biome_colors'
export class BiomeNoiseVisualizer extends Visualizer {
export class BiomeNoisePreview extends Preview {
static readonly noiseMaps = ['altitude', 'temperature', 'humidity', 'weirdness']
private seed: string
private noise: NormalNoise[]
private offsetX: number = 0
private offsetY: number = 0
private viewScale: number = 0
private biomeColors: { [id: string]: number[] }
seed: string
offsetX: number = 0
offsetY: number = 0
viewScale: number = 0
biomeColors: Property<{ [id: string]: number[] }>
constructor() {
super()
this.seed = this.hexId()
this.biomeColors = JSON.parse(localStorage.getItem(LOCAL_STORAGE_BIOME_COLORS) ?? '{}')
this.biomeColors = new Property({})
this.biomeColors.set(JSON.parse(localStorage.getItem(LOCAL_STORAGE_BIOME_COLORS) ?? '{}'))
this.noise = []
}
@@ -31,7 +32,7 @@ export class BiomeNoiseVisualizer extends Visualizer {
}
draw(model: DataModel, img: ImageData) {
this.noise = BiomeNoiseVisualizer.noiseMaps.map((id, i) => {
this.noise = BiomeNoisePreview.noiseMaps.map((id, i) => {
const config = this.state[`${id}_noise`]
return new NormalNoise(this.seed + i, config.firstOctave, config.amplitudes)
})
@@ -49,7 +50,7 @@ export class BiomeNoiseVisualizer extends Visualizer {
const xx = (x - this.offsetX) * s - 100 * s
const yy = (y - this.offsetY) * s - 50 * s
const b = this.closestBiome(xx, yy)
const color = biomeColorCache[b]
const color = biomeColorCache[b] ?? [128, 128, 128]
data[i] = color[0]
data[i + 1] = color[1]
data[i + 2] = color[2]
@@ -63,20 +64,8 @@ export class BiomeNoiseVisualizer extends Visualizer {
this.offsetY += toY - fromY
}
addControls(el: HTMLElement, view: VisualizerView) {
el.insertAdjacentHTML('beforeend', `<button class="btn" id="visualizer-controls-toggle"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8 2a.75.75 0 01.75.75v4.5h4.5a.75.75 0 010 1.5h-4.5v4.5a.75.75 0 01-1.5 0v-4.5h-4.5a.75.75 0 010-1.5h4.5v-4.5A.75.75 0 018 2z"></path></svg></button><button class="btn" id="visualizer-controls-toggle"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M2 8a.75.75 0 01.75-.75h10.5a.75.75 0 010 1.5H2.75A.75.75 0 012 8z"></path></svg></button>`)
el.childNodes[0].addEventListener('click', () => {
this.viewScale -= 0.5
view.redraw()
})
el.childNodes[1].addEventListener('click', () => {
this.viewScale += 0.5
view.redraw()
})
}
private closestBiome(x: number, y: number): string {
if (!this.state.biomes) return ''
if (!this.state.biomes || this.state.biomes.length === 0) return ''
const noise = this.noise.map(n => n.getValue(x, y))
let minDist = Infinity
let minBiome = ''
@@ -95,7 +84,7 @@ export class BiomeNoiseVisualizer extends Visualizer {
}
getBiomeColor(biome: string): number[] {
const color = this.biomeColors[biome]
const color = this.biomeColors.get()[biome]
if (color === undefined) {
return this.colorFromBiome(biome)
}
@@ -103,8 +92,9 @@ export class BiomeNoiseVisualizer extends Visualizer {
}
setBiomeColor(biome: string, value: string) {
this.biomeColors[biome] = [parseInt(value.slice(1, 3), 16), parseInt(value.slice(3, 5), 16), parseInt(value.slice(5, 7), 16)]
localStorage.setItem(LOCAL_STORAGE_BIOME_COLORS, JSON.stringify(this.biomeColors))
const color = [parseInt(value.slice(1, 3), 16), parseInt(value.slice(3, 5), 16), parseInt(value.slice(5, 7), 16)]
this.biomeColors.set({...this.biomeColors.get(), [biome]: color})
localStorage.setItem(LOCAL_STORAGE_BIOME_COLORS, JSON.stringify(this.biomeColors.get()))
}
getBiomeHex(biome: string): string {

View File

@@ -1,10 +1,10 @@
import SimplexNoise from 'simplex-noise'
import { DataModel, Path, ModelPath } from "@mcschema/core"
import { Visualizer } from './Visualizer'
import { Preview } from './Preview'
const debug = false
export class NoiseSettingsVisualizer extends Visualizer {
export class NoiseSettingsPreview extends Preview {
private noise: SimplexNoise
private offsetX: number

View File

@@ -1,8 +1,8 @@
import { DataModel, ModelPath } from "@mcschema/core"
import { VisualizerView } from "./VisualizerView"
export abstract class Visualizer {
export abstract class Preview {
state: any
path?: ModelPath
dirty(path: ModelPath): boolean {
return JSON.stringify(this.state) !== JSON.stringify(path.get())
@@ -13,6 +13,4 @@ export abstract class Visualizer {
abstract draw(model: DataModel, img: ImageData): void
onDrag(fromX: number, fromY: number, toX: number, toY: number): void {}
addControls(el: HTMLElement, view: VisualizerView): void {}
}

View File

@@ -0,0 +1,14 @@
import { Property } from './Property'
export class LocalStorageProperty extends Property<string> {
constructor(private id: string, fallback: string) {
super(localStorage.getItem(id) ?? fallback)
}
set(value: string) {
super.set(value)
localStorage.setItem(this.id, value)
}
get(): string {
return this.value
}
}

40
src/app/state/Property.ts Normal file
View File

@@ -0,0 +1,40 @@
import { hexId } from "../Utils"
type PropertyWatcher<T> = (value: T, oldValue: T | null) => void
type NamedPropertyWatcher<T> = {
name: string
watcher: PropertyWatcher<T>
}
export class Property<T> {
private watchers: NamedPropertyWatcher<T>[] = []
constructor(public value: T) {}
set(value: T) {
if (this.value === value) return
const oldValue = this.value
this.value = value
this.watchers.forEach(w => w.watcher(this.value, oldValue))
}
get(): T {
return this.value
}
watchRun(watcher: PropertyWatcher<T>, name?: string) {
watcher(this.value, null)
return this.watch(watcher, name)
}
watch(watcher: PropertyWatcher<T>, name?: string) {
name = name ?? hexId()
const w = this.watchers.find(w => w.name === name)
if (w) {
w.watcher = watcher
} else {
this.watchers.push({ name, watcher })
}
return this
}
}

View File

@@ -5,3 +5,8 @@ export function hexId(length = 12) {
window.crypto.getRandomValues(arr)
return Array.from(arr, dec2hex).join('')
}
export function htmlEncode(str: string) {
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;').replace(/'/g, '&#x27;').replace(/\//g, '&#x2F;')
}

View File

@@ -0,0 +1,65 @@
import { App, checkVersion, Models } from '../App'
import { View } from './View'
import { Header } from '../components/Header'
import { SplitGroup } from '../components/SplitGroup'
import { Errors } from '../components/Errors'
import { TreePanel } from '../components/panels/TreePanel'
import { SourcePanel } from '../components/panels/SourcePanel'
import { PreviewPanel } from '../components/panels/PreviewPanel'
export const Generator = (view: View): string => {
const model = Models[App.model.get()!.id]
model.listeners = []
const getSideContent = () => {
return App.preview.get() ?
SplitGroup(view, { direction: 'vertical', sizes: [60, 40] }, [
SourcePanel(view, model),
PreviewPanel(view, model)
])
: SourcePanel(view, model)
}
const validatePreview = () => {
const preview = App.preview.get()
const path = preview?.path?.withModel(model)
if (!(path && path.get() && preview?.active(path))) {
App.preview.set(null)
}
}
model.addListener({
invalidated: validatePreview
})
App.schemasLoaded.watch((value) => {
if (value) {
model.validate()
model.invalidate()
validatePreview()
}
}, 'generator')
App.localesLoaded.watch((value) => {
if (value && App.schemasLoaded.get()) {
model.invalidate()
}
}, 'generator')
App.version.watchRun((value) => {
const minVersion = App.model.get()!.minVersion
if (minVersion && !checkVersion(value, minVersion)) {
App.version.set(minVersion)
}
}, 'generator')
const sideContent = view.register(el => {
App.preview.watch((value, oldValue) => {
if (!value || !oldValue) {
view.mount(el, getSideContent(), false)
}
}, 'generator')
})
const homeLink = typeof App.model.get()!.category === 'string' ? `/${App.model.get()!.category}/` : undefined
return `${Header(view, `${App.model.get()!.name} Generator`, homeLink, true)}
<div class="content">
${SplitGroup(view, { direction: "horizontal", sizes: [66, 34] }, [
TreePanel(view, model),
`<div class="content-output" data-id="${sideContent}">${getSideContent()}</div>`
])}
</div>
${Errors(view, model)}`
}

34
src/app/views/Home.ts Normal file
View File

@@ -0,0 +1,34 @@
import { App } from '../App'
import { Header } from '../components/Header'
import { View } from './View'
import { Octicon } from '../components/Octicon'
import config from '../../config.json'
const GeneratorCard = (url: string, name: string, arrow?: boolean, active?: boolean) => `
<li>
<a data-link href="/${url}/" class="generators-card${active ? ' selected' : ''}">
${name}
${arrow ? Octicon.chevron_right : ''}
</a>
</li>
`
export const Home = (view: View): string => {
const filteredModels = config.models.filter(m => m.category === App.model.get()!.id)
return `
${Header(view, 'Data Pack Generators')}
<div class="home">
<ul class="generators-list">
${config.models
.filter(m => typeof m.category !== 'string')
.map(m => GeneratorCard(m.id, m.name, m.category === true, App.model.get()!.id === m.id))
.join('')}
</ul>
${filteredModels.length === 0 ? '' : `
<ul class="generators-list">
${filteredModels.map(m => GeneratorCard(m.id, m.name)).join('')}
</ul>
`}
</div>
`
}

54
src/app/views/View.ts Normal file
View File

@@ -0,0 +1,54 @@
import { locale } from "../Locales"
import { hexId } from "../Utils"
export class View {
private registry: { [id: string]: (el: Element) => void } = {}
render(): string {
return ''
}
register(callback: (el: Element) => void): string {
const id = hexId()
this.registry[id] = callback
return id
}
on(type: string, callback: (el: Element) => void): string {
return this.register(el => {
el.addEventListener(type, evt => {
callback(el)
evt.stopPropagation()
})
})
}
onChange(callback: (el: Element) => void): string {
return this.on('change', callback)
}
onClick(callback: (el: Element) => void): string {
return this.on('click', callback)
}
mounted(el: Element, clear = true): void {
for (const id in this.registry) {
const element = el.querySelector(`[data-id="${id}"]`)
if (element !== null) {
this.registry[id](element)
delete this.registry[id]
}
}
if (clear) {
this.registry = {}
}
document.querySelectorAll('[data-i18n]').forEach(el => {
el.textContent = locale(el.attributes.getNamedItem('data-i18n')!.value)
})
}
mount(el: Element, html: string, clear = true) {
el.innerHTML = html
this.mounted(el, clear)
}
}

View File

@@ -1,93 +0,0 @@
import { DataModel, ModelPath } from "@mcschema/core"
import { AbstractView } from "../AbstractView"
import { BiomeNoiseVisualizer } from "./BiomeNoiseVisualizer"
import { NoiseSettingsVisualizer } from "./NoiseSettingsVisualizer"
import { Visualizer } from "./Visualizer"
export class VisualizerView extends AbstractView {
ctx: CanvasRenderingContext2D
visualizer?: Visualizer
active: boolean
path?: ModelPath
el: HTMLElement
canvas: HTMLCanvasElement
sourceView: HTMLElement
gutter: HTMLElement
controls: HTMLElement
lastHeight?: string
dragStart?: number[]
constructor(model: DataModel, el: HTMLElement) {
super(model)
this.el = el
this.canvas = el.querySelector('canvas') as HTMLCanvasElement
this.ctx = this.canvas.getContext('2d')!
this.active = false
this.gutter = el.parentElement!.querySelector('.gutter') as HTMLElement
this.sourceView = el.parentElement!.getElementsByTagName('textarea')[0] as HTMLElement
this.controls = el.querySelector('.visualizer-controls') as HTMLElement
this.canvas.addEventListener('mousedown', evt => {
this.dragStart = [evt.offsetX, evt.offsetY]
})
this.canvas.addEventListener('mousemove', evt => {
if (this.dragStart === undefined) return
if (this.visualizer?.onDrag) {
this.visualizer.onDrag(this.dragStart[0], this.dragStart[1], evt.offsetX, evt.offsetY)
this.redraw()
}
this.dragStart = [evt.offsetX, evt.offsetY]
})
this.canvas.addEventListener('mouseup', evt => {
this.dragStart = undefined
})
}
redraw() {
if (this.active && this.visualizer) {
this.visualizer.state = {}
this.invalidated()
}
}
invalidated() {
this.path = this.path?.withModel(this.model)
let newState: any
if (this.active && this.visualizer && this.path
&& this.visualizer.active(this.path)
&& (newState = this.path.get())) {
if (newState && this.visualizer.dirty(this.path)) {
const img = this.ctx.createImageData(200, 100)
this.visualizer.state = JSON.parse(JSON.stringify(newState))
this.visualizer.draw(this.model, img)
this.ctx.putImageData(img, 0, 0)
}
this.el.style.display = 'block'
this.gutter.style.display = 'block'
if (this.lastHeight) {
this.sourceView.style.height = this.lastHeight
this.lastHeight = undefined
}
} else {
this.el.style.display = 'none'
this.gutter.style.display = 'none'
this.lastHeight = this.sourceView.style.height
this.sourceView.style.height = '100%'
}
}
set(visualizer: Visualizer, path: ModelPath) {
this.active = true
this.visualizer = visualizer
this.path = path
this.visualizer.state = undefined
this.controls.innerHTML = ''
this.visualizer.addControls(this.controls, this)
this.redraw()
}
static visualizers: Visualizer[] = [
new BiomeNoiseVisualizer(),
new NoiseSettingsVisualizer()
]
}

View File

@@ -42,13 +42,13 @@
}
],
"versions": [
{
"id": "1.17",
"mcdata_ref": "master"
},
{
"id": "1.16",
"mcdata_ref": "1.16.4"
},
{
"id": "1.17",
"mcdata_ref": "master"
}
],
"models": [
@@ -91,48 +91,55 @@
{
"id": "worldgen",
"name": "Worldgen",
"children": [
{
"id": "worldgen/biome",
"name": "Biome",
"schema": "biome"
},
{
"id": "worldgen/carver",
"name": "Carver",
"schema": "configured_carver"
},
{
"id": "worldgen/feature",
"name": "Feature",
"schema": "configured_feature"
},
{
"id": "worldgen/noise-settings",
"name": "Noise Settings",
"schema": "noise_settings"
},
{
"id": "worldgen/structure-feature",
"name": "Structure Feature",
"schema": "configured_structure_feature"
},
{
"id": "worldgen/surface-builder",
"name": "Surface Builder",
"schema": "configured_surface_builder"
},
{
"id": "worldgen/processor-list",
"name": "Processor List",
"schema": "processor_list"
},
{
"id": "worldgen/template-pool",
"name": "Template Pool",
"schema": "template_pool"
}
]
"category": true
},
{
"id": "worldgen/biome",
"name": "Biome",
"category": "worldgen",
"schema": "biome"
},
{
"id": "worldgen/carver",
"name": "Carver",
"category": "worldgen",
"schema": "configured_carver"
},
{
"id": "worldgen/feature",
"name": "Feature",
"category": "worldgen",
"schema": "configured_feature"
},
{
"id": "worldgen/noise-settings",
"name": "Noise Settings",
"category": "worldgen",
"schema": "noise_settings"
},
{
"id": "worldgen/structure-feature",
"name": "Structure Feature",
"category": "worldgen",
"schema": "configured_structure_feature"
},
{
"id": "worldgen/surface-builder",
"name": "Surface Builder",
"category": "worldgen",
"schema": "configured_surface_builder"
},
{
"id": "worldgen/processor-list",
"name": "Processor List",
"category": "worldgen",
"schema": "processor_list"
},
{
"id": "worldgen/template-pool",
"name": "Template Pool",
"category": "worldgen",
"schema": "template_pool"
}
],
"registries": [
@@ -144,10 +151,10 @@
"item",
"loot_condition_type",
"loot_function_type",
"loot_nbt_provider_type",
"loot_number_provider_type",
{ "id": "loot_nbt_provider_type", "minVersion": "1.17" },
{ "id": "loot_number_provider_type", "minVersion": "1.17" },
"loot_pool_entry_type",
"loot_score_provider_type",
{ "id": "loot_score_provider_type", "minVersion": "1.17" },
"mob_effect",
"rule_test",
"pos_rule_test",
@@ -168,9 +175,6 @@
"worldgen/surface_builder",
"worldgen/tree_decorator_type",
"worldgen/trunk_placer_type",
{
"id": "worldgen/biome",
"path": "/processed/reports/biomes/data.min.json"
}
{ "id": "worldgen/biome", "path": "processed/reports/biomes" }
]
}

View File

@@ -8,11 +8,12 @@
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-73024255-2', 'auto');
ga('set', 'page', location.pathname.replace(/^\/dev/, ''));
ga('set', 'page', location.pathname);
ga('set', 'dimension1', localStorage.getItem('theme') ?? 'default');
ga('set', 'dimension2', 'v2');
ga('set', 'dimension3', localStorage.getItem('schema_version') ?? '1.17');
ga('set', 'dimension4', localStorage.getItem('language') ?? 'en');
ga('set', 'dimension5', 'none');
ga('send', 'pageview');
</script>
<!-- End Google Analytics -->
@@ -26,101 +27,7 @@
<link rel="stylesheet" href="<%= htmlWebpackPlugin.files.publicPath %>styles/global.css">
<link rel="stylesheet" href="<%= htmlWebpackPlugin.files.publicPath %>styles/nodes.css">
</head>
<body style="visibility: hidden;">
<div class="container">
<div class="header">
<div class="header-title">
<div id="home-link" class="home-link nav-item">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="32" height="32"><path fill-rule="evenodd" d="M1 2.75A.75.75 0 011.75 2h12.5a.75.75 0 110 1.5H1.75A.75.75 0 011 2.75zm0 5A.75.75 0 011.75 7h12.5a.75.75 0 110 1.5H1.75A.75.75 0 011 7.75zM1.75 12a.75.75 0 100 1.5h12.5a.75.75 0 100-1.5H1.75z"></path></svg>
</div>
<h2 id="selected-model"></h2>
</div>
<div class="nav">
<div id="source-toggle" class="nav-item toggle">
<svg id="source-toggle-tree" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="24" height="24"><path fill-rule="evenodd" d="M0 3.75C0 2.784.784 2 1.75 2h12.5c.966 0 1.75.784 1.75 1.75v8.5A1.75 1.75 0 0114.25 14H1.75A1.75 1.75 0 010 12.25v-8.5zm1.75-.25a.25.25 0 00-.25.25v8.5c0 .138.112.25.25.25h12.5a.25.25 0 00.25-.25v-8.5a.25.25 0 00-.25-.25H1.75zM3.5 6.25a.75.75 0 01.75-.75h7a.75.75 0 010 1.5h-7a.75.75 0 01-.75-.75zm.75 2.25a.75.75 0 000 1.5h4a.75.75 0 000-1.5h-4z"></path></svg>
<svg id="source-toggle-source" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="24" height="24"><path fill-rule="evenodd" d="M4.72 3.22a.75.75 0 011.06 1.06L2.06 8l3.72 3.72a.75.75 0 11-1.06 1.06L.47 8.53a.75.75 0 010-1.06l4.25-4.25zm6.56 0a.75.75 0 10-1.06 1.06L13.94 8l-3.72 3.72a.75.75 0 101.06 1.06l4.25-4.25a.75.75 0 000-1.06l-4.25-4.25z"></path></svg>
</div>
<div class="nav-item nav-selector">
<svg id="language-selector" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="24" height="24"><path fill-rule="evenodd" d="M1.543 7.25h2.733c.144-2.074.866-3.756 1.58-4.948.12-.197.237-.381.353-.552a6.506 6.506 0 00-4.666 5.5zm2.733 1.5H1.543a6.506 6.506 0 004.666 5.5 11.13 11.13 0 01-.352-.552c-.715-1.192-1.437-2.874-1.581-4.948zm1.504 0h4.44a9.637 9.637 0 01-1.363 4.177c-.306.51-.612.919-.857 1.215a9.978 9.978 0 01-.857-1.215A9.637 9.637 0 015.78 8.75zm4.44-1.5H5.78a9.637 9.637 0 011.363-4.177c.306-.51.612-.919.857-1.215.245.296.55.705.857 1.215A9.638 9.638 0 0110.22 7.25zm1.504 1.5c-.144 2.074-.866 3.756-1.58 4.948-.12.197-.237.381-.353.552a6.506 6.506 0 004.666-5.5h-2.733zm2.733-1.5h-2.733c-.144-2.074-.866-3.756-1.58-4.948a11.738 11.738 0 00-.353-.552 6.506 6.506 0 014.666 5.5zM8 0a8 8 0 100 16A8 8 0 008 0z"></path></svg>
<div class="nav-selector-menu btn-group" id="language-selector-menu"></div>
</div>
<div id="theme-selector" class="nav-item toggle">
<svg id="theme-selection-light" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="24" height="24"><path fill-rule="evenodd" d="M8 10.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5zM8 12a4 4 0 100-8 4 4 0 000 8zM8 0a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0V.75A.75.75 0 018 0zm0 13a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 018 13zM2.343 2.343a.75.75 0 011.061 0l1.06 1.061a.75.75 0 01-1.06 1.06l-1.06-1.06a.75.75 0 010-1.06zm9.193 9.193a.75.75 0 011.06 0l1.061 1.06a.75.75 0 01-1.06 1.061l-1.061-1.06a.75.75 0 010-1.061zM16 8a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0116 8zM3 8a.75.75 0 01-.75.75H.75a.75.75 0 010-1.5h1.5A.75.75 0 013 8zm10.657-5.657a.75.75 0 010 1.061l-1.061 1.06a.75.75 0 11-1.06-1.06l1.06-1.06a.75.75 0 011.06 0zm-9.193 9.193a.75.75 0 010 1.06l-1.06 1.061a.75.75 0 11-1.061-1.06l1.06-1.061a.75.75 0 011.061 0z"></path></svg>
<svg id="theme-selection-dark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="24" height="24"><path fill-rule="evenodd" d="M9.598 1.591a.75.75 0 01.785-.175 7 7 0 11-8.967 8.967.75.75 0 01.961-.96 5.5 5.5 0 007.046-7.046.75.75 0 01.175-.786zm1.616 1.945a7 7 0 01-7.678 7.678 5.5 5.5 0 107.678-7.678z"></path></svg>
</div>
<div id="github-link" class="nav-item">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="24" height="24"><path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path></svg>
</div>
</div>
</div>
<div class="content">
<div class="tree" id="tree-view">
<div class="tree-controls">
<button class="btn" id="tree-controls-toggle"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path d="M8 9a1.5 1.5 0 100-3 1.5 1.5 0 000 3zM1.5 9a1.5 1.5 0 100-3 1.5 1.5 0 000 3zm13 0a1.5 1.5 0 100-3 1.5 1.5 0 000 3z"></path></svg></button>
<button class="btn" id="tree-version-toggle">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M2.5 7.775V2.75a.25.25 0 01.25-.25h5.025a.25.25 0 01.177.073l6.25 6.25a.25.25 0 010 .354l-5.025 5.025a.25.25 0 01-.354 0l-6.25-6.25a.25.25 0 01-.073-.177zm-1.5 0V2.75C1 1.784 1.784 1 2.75 1h5.025c.464 0 .91.184 1.238.513l6.25 6.25a1.75 1.75 0 010 2.474l-5.026 5.026a1.75 1.75 0 01-2.474 0l-6.25-6.25A1.75 1.75 0 011 7.775zM6 5a1 1 0 100 2 1 1 0 000-2z"></path></svg>
<span id="tree-version-label"></span>
</button>
<button class="btn" id="tree-controls-reset">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.643 3.143L.427 1.927A.25.25 0 000 2.104V5.75c0 .138.112.25.25.25h3.646a.25.25 0 00.177-.427L2.715 4.215a6.5 6.5 0 11-1.18 4.458.75.75 0 10-1.493.154 8.001 8.001 0 101.6-5.684zM7.75 4a.75.75 0 01.75.75v2.992l2.028.812a.75.75 0 01-.557 1.392l-2.5-1A.75.75 0 017 8.25v-3.5A.75.75 0 017.75 4z"></path></svg>
<span data-i18n="reset"></span>
</button>
</div>
<div class="tree-controls-menu btn-group" id="tree-controls-menu">
<button class="btn" id="tree-controls-undo">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.78 12.53a.75.75 0 01-1.06 0L2.47 8.28a.75.75 0 010-1.06l4.25-4.25a.75.75 0 011.06 1.06L4.81 7h7.44a.75.75 0 010 1.5H4.81l2.97 2.97a.75.75 0 010 1.06z"></path></svg>
<span data-i18n="undo"></span>
</button>
<button class="btn" id="tree-controls-redo">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8.22 2.97a.75.75 0 011.06 0l4.25 4.25a.75.75 0 010 1.06l-4.25 4.25a.75.75 0 01-1.06-1.06l2.97-2.97H3.75a.75.75 0 010-1.5h7.44L8.22 4.03a.75.75 0 010-1.06z"></path></svg>
<span data-i18n="redo"></span>
</button>
</div>
<div class="tree-controls-menu btn-group" id="tree-version-menu"></div>
<div class="tree-content">
<div id="tree-view-output"></div>
</div>
</div>
<div class="source" id="source-view">
<div class="source-controls">
<button class="btn" id="source-controls-toggle"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path d="M8 9a1.5 1.5 0 100-3 1.5 1.5 0 000 3zM1.5 9a1.5 1.5 0 100-3 1.5 1.5 0 000 3zm13 0a1.5 1.5 0 100-3 1.5 1.5 0 000 3z"></path></svg></button>
<button class="btn" id="source-controls-copy">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M5.75 1a.75.75 0 00-.75.75v3c0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75v-3a.75.75 0 00-.75-.75h-4.5zm.75 3V2.5h3V4h-3zm-2.874-.467a.75.75 0 00-.752-1.298A1.75 1.75 0 002 3.75v9.5c0 .966.784 1.75 1.75 1.75h8.5A1.75 1.75 0 0014 13.25v-9.5a1.75 1.75 0 00-.874-1.515.75.75 0 10-.752 1.298.25.25 0 01.126.217v9.5a.25.25 0 01-.25.25h-8.5a.25.25 0 01-.25-.25v-9.5a.25.25 0 01.126-.217z"></path></svg>
<span data-i18n="copy"></span>
</button>
<input type="text" id="source-controls-share-input" style="display: none;"></input>
</div>
<div class="source-controls-menu btn-group" id="source-controls-menu">
<button class="btn" id="source-controls-download">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.47 10.78a.75.75 0 001.06 0l3.75-3.75a.75.75 0 00-1.06-1.06L8.75 8.44V1.75a.75.75 0 00-1.5 0v6.69L4.78 5.97a.75.75 0 00-1.06 1.06l3.75 3.75zM3.75 13a.75.75 0 000 1.5h8.5a.75.75 0 000-1.5h-8.5z"></path></svg>
<span data-i18n="download"></span>
<a id="source-controls-download-anchor" style="display: none;"></a>
</button>
<button class="btn" id="source-controls-share">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg>
<span data-i18n="share"></span>
</button>
</div>
<div class="source-content">
<textarea id="source-view-output" spellcheck="false" autocorrect="off" autocapitalize="off"></textarea>
<div class="visualizer-content" id="visualizer-content">
<div class="visualizer-controls" id="visualizer-controls"></div>
<canvas width="200" height="100" id="visualizer-output"></canvas>
</div>
</div>
</div>
<div id="home-view" class="home">
<div id="home-generators" class="generators-list"></div>
<div id="category-generators" class="generators-list"></div>
</div>
</div>
<div id="errors-view" class="errors">
<div class="error-list"></div>
<div id="errors-toggle" class="toggle">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="32" height="32"><path fill-rule="evenodd" d="M12.78 6.22a.75.75 0 010 1.06l-4.25 4.25a.75.75 0 01-1.06 0L3.22 7.28a.75.75 0 011.06-1.06L8 9.94l3.72-3.72a.75.75 0 011.06 0z"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="24" height="24"><path fill-rule="evenodd" d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8zm9 3a1 1 0 11-2 0 1 1 0 012 0zm-.25-6.25a.75.75 0 00-1.5 0v3.5a.75.75 0 001.5 0v-3.5z"></path></svg>
</div>
</div>
</div>
<body>
<div id="app" class="container"></div>
</body>
</html>

View File

@@ -7,7 +7,7 @@
"worldgen/feature": "Merkmal",
"worldgen/carver": "Borer",
"worldgen/biome": "Biom",
"visualize": "Visualisieren",
"preview": "Visualisieren",
"title.home": "Datenpaketgeneratoren",
"title.generator": "%0%-Generator",
"share": "Teilen",

View File

@@ -13,7 +13,7 @@
"share": "Share",
"title.generator": "%0% Generator",
"title.home": "Data Pack Generators",
"visualize": "Visualize",
"preview": "Visualize",
"undo": "Undo",
"world": "World Settings",
"worldgen/biome": "Biome",

View File

@@ -11,7 +11,7 @@
"share": "Partager",
"title.generator": "Générateur de %0%",
"title.home": "Générateur de data-pack",
"visualize": "Visualiser",
"preview": "Visualiser",
"world": "Paramètres du monde",
"worldgen/biome": "Biome",
"worldgen/carver": "Sculpteur",

View File

@@ -11,7 +11,7 @@
"share": "共有",
"title.generator": "%0%ジェネレーター",
"title.home": "データパックジェネレーター",
"visualize": "可視化",
"preview": "可視化",
"world": "ワールド設定 (World Settings)",
"worldgen/biome": "バイオーム (Biome)",
"worldgen/carver": "地形彫刻 (Carver)",

View File

@@ -11,7 +11,7 @@
"share": "Podziel się",
"title.generator": "Generator %0%",
"title.home": "Generatory Data Packów",
"visualize": "Wizualizuj",
"preview": "Wizualizuj",
"world": "Ustawienia Świata",
"worldgen/biome": "Biom",
"worldgen/carver": "Rzeźbiarz",

View File

@@ -14,7 +14,7 @@
"title.generator": "%0% 生成器",
"title.home": "数据包生成器",
"undo": "撤销",
"visualize": "可视化",
"preview": "可视化",
"world": "世界设置",
"worldgen/biome": "生物群系",
"worldgen/carver": "地形雕刻器",

View File

@@ -14,7 +14,7 @@
"title.generator": "%0% 生成器",
"title.home": "資料包生成器",
"undo": "復原",
"visualize": "可視化",
"preview": "可視化",
"world": "世界設定",
"worldgen/biome": "生態域",
"worldgen/carver": "地形雕刻器",

View File

@@ -1,6 +1,7 @@
https://misode.github.io
https://misode.github.io/loot-table/
https://misode.github.io/predicate/
https://misode.github.io/item-modifier/
https://misode.github.io/advancement/
https://misode.github.io/dimension/
https://misode.github.io/dimension-type/

View File

@@ -38,18 +38,19 @@
padding: 0;
}
a svg {
pointer-events: none;
}
body {
font-size: 18px;
font-family: Arial, Helvetica, sans-serif;
}
.container {
overflow-x: hidden;
background-color: var(--background);
transition: background-color var(--style-transition);
}
.header {
header {
display: flex;
justify-content: space-between;
align-items: center;
@@ -65,114 +66,133 @@ body {
}
.header-title h2 {
margin-right: 10px;
color: var(--nav);
transition: color var(--style-transition);
}
.home-link.nav-item {
.home-link {
margin: 0 8px 0 0;
height: 32px;
fill: var(--nav);
transition: fill var(--style-transition);
}
.home-link svg {
width: 32px;
height: 32px;
padding: 2px;
}
.nav {
nav ul {
display: flex;
align-items: center;
}
.nav-item {
nav > .toggle,
nav li {
display: flex;
align-items: center;
cursor: pointer;
margin: 0 16px;
user-select: none;
}
.home-link:hover svg,
header .toggle:hover svg,
nav li:hover svg {
fill: var(--nav-hover);
}
nav > .toggle {
display: none;
}
nav li.dimmed svg {
fill: var(--nav-faded);
}
nav li.dimmed:hover svg {
fill: var(--nav-faded-hover);
}
nav > .toggle svg,
nav li > *,
nav li svg {
width: 24px;
height: 24px;
fill: var(--nav);
transition: fill var(--style-transition);
}
.nav-item:hover {
fill: var(--nav-hover);
nav > .toggle span {
color: var(--nav);
margin-left: 5px;
}
.nav-item span {
font-size: 1.1em;
}
.nav-item#source-toggle {
display: none;
}
.nav-item#github-link {
fill: var(--nav-faded);
}
.nav-item#github-link:hover {
fill: var(--nav-faded-hover);
}
.toggle:not(.toggled) svg:nth-child(1) {
display: none;
}
.toggle.toggled svg:nth-child(2) {
display: none;
}
.nav-selector {
.dropdown {
position: relative;
}
.nav-selector-menu {
display: flex;
flex-direction: column;
visibility: hidden;
.dropdown > * {
position: absolute;
top: 100%;
left: 0;
margin-top: 10px;
z-index: 10;
border-radius: 3px;
background-color: var(--nav-menu);
width: 24px;
height: 24px;
}
.nav-selector-menu .btn {
white-space: nowrap;
.dropdown > *:not(select) {
pointer-events: none;
}
.dropdown select {
cursor: pointer;
border: none;
background: none;
color: transparent;
outline: none;
}
.dropdown option {
color: var(--text);
background-color: var(--background);
font-size: 130%;
border: none;
}
.content {
display: flex;
height: calc(100vh - 56px);
overflow-y: hidden;
color: var(--text);
fill: var(--text);
}
.split-group {
display: flex;
width: 100%;
height: 100%;
}
.split-group.vertical {
flex-direction: column;
}
.panel {
position: relative;
height: 100%;
overflow: hidden;
}
.tree {
position: relative;
}
.tree-content {
overflow-y: auto;
display: flow-root;
padding: 16px 16px 50vh;
height: 100%;
padding: 1rem;
padding-bottom: 50vh;
overflow: auto;
}
.source {
position: relative;
}
.source-content {
display: flex;
flex-direction: column;
height: 100%;
}
.source textarea {
width: 100%;
height: 100%;
padding: 1.3rem 0.4rem;
padding: 32px 8px;
border: none;
white-space: pre;
overflow-wrap: normal;
@@ -188,39 +208,48 @@ body {
transition: background-color var(--style-transition), color var(--style-transition)
}
.source textarea::selection {
.source::selection {
background-color: var(--selection);
}
.tree-controls,
.source-controls,
.visualizer-controls {
.panel-controls {
display: flex;
flex-direction: row-reverse;
position: absolute;
right: 17px;
top: 0;
padding: 5px;
right: 22px;
top: 5px;;
z-index: 1;
}
.tree-controls .btn:not(:first-child),
.source-controls .btn:not(:first-child),
.visualizer-controls .btn:not(:first-child) {
.preview-panel .panel-controls {
right: 5px;
}
.panel-controls > *:not(:last-child) {
margin-right: 5px;
}
.tree-controls-menu,
.source-controls-menu {
display: flex;
visibility: hidden;
flex-direction: column;
position: absolute;
right: 17px;
top: 37px;
padding: 5px;
.panel-menu {
position: relative;
}
.source-controls input {
.panel-menu > .btn {
height: 100%;
}
.panel-menu-list {
display: none;
flex-direction: column;
position: absolute;
right: 0;
top: 100%;
margin-top: 5px;
}
.panel-menu .btn.active ~ .panel-menu-list {
display: flex;
}
.panel-controls input {
margin-right: 5px;
background: var(--background);
color: var(--text);
@@ -229,7 +258,7 @@ body {
transition: background-color var(--style-transition), color var(--style-transition);
}
.source-controls input::selection {
.panel-controls input::selection {
background-color: var(--selection);
}
@@ -250,17 +279,7 @@ body {
cursor: ew-resize;
}
.visualizer-content {
width: 100%;
max-width: 100%;
position: relative;
}
.visualizer-controls {
right: 0;
}
.visualizer-content canvas {
.preview-panel canvas {
width: 100%;
height: 100%;
background-color: var(--nav-faded);
@@ -283,6 +302,7 @@ body {
cursor: pointer;
outline: none;
font-size: 1rem;
white-space: nowrap;
background-color: var(--btn-background);
color: var(--btn-text);
fill: var(--btn-text);
@@ -315,6 +335,7 @@ body {
.errors {
position: fixed;
display: flex;
bottom: 17px;
right: 17px;
margin: 5px;
@@ -325,31 +346,27 @@ body {
transition: fill var(--style-transition);
}
.errors:not(.active) .error-list {
display: none;
}
.error {
display: flex;
align-items: center;
padding: 7px;
}
.error span {
padding-left: 11px;
.error span:not(:last-child) {
padding-right: 11px;
}
.errors .toggle {
padding: 6px;
width: 36px;
height: 36px;
align-self: flex-end;
cursor: pointer;
user-select: none;
}
.errors.active .toggle {
padding: 2px;
.errors svg {
width: 24px;
height: 24px;
}
.home {
@@ -361,6 +378,7 @@ body {
display: flex;
flex-direction: column;
margin: 0 20px;
list-style-type: none;
}
.generators-card {
@@ -369,6 +387,7 @@ body {
cursor: pointer;
user-select: none;
text-decoration: none;
text-transform: capitalize;
border-radius: 3px;
background-color: var(--nav-faded);
color: var(--text);
@@ -380,6 +399,10 @@ body {
transition: margin 0.2s;
}
.generators-card * {
pointer-events: none;
}
.generators-card:hover,
.generators-card.selected {
background-color: var(--nav-faded-hover);
@@ -391,17 +414,54 @@ body {
margin-left: 10px;
}
.spinner {
margin: 40px auto 0;
width: 80px;
height: 80px;
}
.spinner:after {
content: "";
display: block;
width: 64px;
height: 64px;
margin: 8px;
border-radius: 50%;
border: 6px solid var(--border);
border-color: var(--border) transparent var(--border) transparent;
animation: spinner 1.2s linear infinite, fadein 0.4s;
transition: border-color var(--style-transition);
}
@keyframes spinner {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes fadein {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@media screen and (max-width: 580px) {
body {
overflow-y: hidden;
}
.header {
header {
flex-direction: column;
height: 92px;
}
.nav {
nav {
align-self: flex-end;
}
@@ -410,42 +470,36 @@ body {
height: calc(100vh - 92px);
}
.source,
.gutter.gutter-horizontal {
.tree {
padding-top: 50px;
}
.tree-panel + .gutter {
display: none;
}
.tree {
.content-output,
.tree-panel {
width: 100% !important;
height: 100%;
}
.nav-item#source-toggle {
nav > .toggle {
display: flex;
position: absolute;
left: 10px;
}
.source.active {
display: initial;
position: absolute;
width: 100% !important;
height: 100%;
body[data-panel="tree"] .content-output {
display: none;
}
.source.active .source-controls {
right: 17px;
top: 0px;
body[data-panel="source"] .tree-panel {
display: none;
}
.source.active .source-controls-menu {
right: 17px;
top: 37px;
}
.nav-selector-menu {
right: 0;
left: initial;
.home {
padding: 5px;
justify-content: center;
}
}
@@ -453,10 +507,19 @@ body {
.header-title h2 {
font-size: 18px;
}
.generators-list {
margin: 0 15px;
}
.generators-card {
font-size: 14px;
padding: 8px;
}
}
@media screen and (min-width: 581px) and (max-width: 640px) {
.header-title h2 {
font-size: 22px;
font-size: 23px;
}
}

View File

@@ -174,10 +174,6 @@ button.remove {
display: inline-block;
}
.node-icon svg {
cursor: pointer;
}
.node-icon .icon-popup {
visibility: hidden;
width: 240px;
@@ -209,19 +205,20 @@ button.remove {
visibility: visible;
}
.node-icon > svg {
.node-icon svg {
height: 34px;
width: 34px;
min-width: 34px;
margin-left: 6px;
cursor: pointer;
transition: fill var(--style-transition);
}
.node-header svg.node-help {
.node-icon.node-help svg {
fill: var(--node-border);
}
.node-header svg.node-error {
.node-icon.node-error svg {
fill: var(--node-remove);
}