diff --git a/src/components/Editors/Jigsaw/ComponentLib/ComponentLib.vue b/src/components/Editors/Jigsaw/ComponentLib/ComponentLib.vue new file mode 100644 index 000000000..3f18edb02 --- /dev/null +++ b/src/components/Editors/Jigsaw/ComponentLib/ComponentLib.vue @@ -0,0 +1,22 @@ + + + diff --git a/src/components/Editors/Jigsaw/EventLine.vue b/src/components/Editors/Jigsaw/EventLine.vue new file mode 100644 index 000000000..1c96cbd55 --- /dev/null +++ b/src/components/Editors/Jigsaw/EventLine.vue @@ -0,0 +1,48 @@ + + + + + diff --git a/src/components/Editors/Jigsaw/FlowArea/Grid.vue b/src/components/Editors/Jigsaw/FlowArea/Grid.vue new file mode 100644 index 000000000..28f3c48fa --- /dev/null +++ b/src/components/Editors/Jigsaw/FlowArea/Grid.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/src/components/Editors/Jigsaw/FlowArea/GridElement.vue b/src/components/Editors/Jigsaw/FlowArea/GridElement.vue new file mode 100644 index 000000000..a52c1015f --- /dev/null +++ b/src/components/Editors/Jigsaw/FlowArea/GridElement.vue @@ -0,0 +1,34 @@ + + + diff --git a/src/components/Editors/Jigsaw/JigsawNode.vue b/src/components/Editors/Jigsaw/JigsawNode.vue new file mode 100644 index 000000000..588dcba32 --- /dev/null +++ b/src/components/Editors/Jigsaw/JigsawNode.vue @@ -0,0 +1,198 @@ + + + + + diff --git a/src/components/Editors/Jigsaw/README.md b/src/components/Editors/Jigsaw/README.md new file mode 100644 index 000000000..e36533a2c --- /dev/null +++ b/src/components/Editors/Jigsaw/README.md @@ -0,0 +1,37 @@ +# Flow Editor + +Rethinking Minecraft Add-On creation. + +## What is Flow Editor? + +Flow is an experimental visual entity editor for Minecraft Add-Ons. Components are placed on a grid and component groups and events are inferred naturally by the way the components are connected. This allows for a more intuitive and visual way to create Minecraft Add-Ons. + +## Technical Details + +### Internal Representation + +Flow grids will have a fixed size of horizontal cells and a theoretically infinite number of vertical cells. We are storing components within an one-dimensional array of cells. Each cell can contain a single component or an "event line" to connect components together. + +### Adding components + +Components can be dragged from an "inventory" onto the grid. A component group is simply defined by having multiple components right next to each other. Events are connecting component groups together. A red line means "remove this group" and a green line means "add this group". + +### Editing components + +We need to write a small library which can turn a JSON schema into a simple, visual user interface. It will open upon clicking on a component in the grid. + +### Turning flow into code + +In order to turn the visual representation of an entity into code, we first need to create a data structure that stores all component groups. This is done in the following way: + +- Iterate over each cell line by line + - If the current cell is empty, jump to the next cell + - Otherwise, check if the cell above contains a component and the cell to the left contains a component + - If it does, merge the two components into one group and add the new component to the group as well + - If it doesn't, check if the cell above contains a component + - If it does, add the current component to the group above + - If it doesn't, check if the cell to the left contains a component + - If it does, add the current component to the group to the left + - Otherwise, add the current component to a new group + +TODO: Event parsing diff --git a/src/components/Editors/Jigsaw/Tab.ts b/src/components/Editors/Jigsaw/Tab.ts new file mode 100644 index 000000000..ec2cbd149 --- /dev/null +++ b/src/components/Editors/Jigsaw/Tab.ts @@ -0,0 +1,144 @@ +import { FileTab, TReadOnlyMode } from '/@/components/TabSystem/FileTab' +import JigsawTabComponent from './Tab.vue' +import { App } from '/@/App' +import { TabSystem } from '/@/components/TabSystem/TabSystem' +import json5 from 'json5' +import { settingsState } from '/@/components/Windows/Settings/SettingsState' +import { debounce } from 'lodash-es' +import { InformationWindow } from '../../Windows/Common/Information/InformationWindow' +import { AnyFileHandle } from '../../FileSystem/Types' + +const throttledCacheUpdate = debounce<(tab: JigsawTab) => Promise | void>( + async (tab) => { + const fileContent = JSON.stringify({ + // TODO: Get JSON from jigsaw tab + }) + const app = await App.getApp() + + app.project.fileChange.dispatch(tab.getPath(), await tab.getFile()) + + await app.project.packIndexer.updateFile( + tab.getPath(), + fileContent, + tab.isForeignFile, + true + ) + await app.project.jsonDefaults.updateDynamicSchemas(tab.getPath()) + }, + 600 +) + +export class JigsawTab extends FileTab { + component = JigsawTabComponent + position = { + x: 0, + y: 0, + } + + constructor( + parent: TabSystem, + fileHandle: AnyFileHandle, + readonlyMode?: TReadOnlyMode + ) { + super(parent, fileHandle, readonlyMode) + + this.fired.then(async () => { + const app = await App.getApp() + await app.projectManager.projectReady.fired + + app.project.tabActionProvider.addTabActions(this) + }) + } + + get app() { + return this.parent.app + } + get project() { + return this.parent.project + } + + static is(fileHandle: AnyFileHandle) { + return fileHandle.name.endsWith('.flow') + } + async setup() { + let json: unknown + try { + const fileStr = await this.fileHandle + .getFile() + .then((file) => file.text()) + + if (fileStr === '') json = {} + else json = json5.parse(fileStr) + } catch { + new InformationWindow({ + name: 'windows.invalidJson.title', + description: 'windows.invalidJson.description', + }) + this.close() + return + } + + await super.setup() + } + async getFile() { + return new File( + [ + JSON.stringify({ + // TODO: Get JSON from jigsaw tab + }), + ], + this.name + ) + } + + updateCache() { + throttledCacheUpdate(this) + } + + async onActivate() {} + async onDeactivate() {} + + loadEditor() {} + setReadOnly(val: TReadOnlyMode) { + this.readOnlyMode = val + } + + async save() { + this.isTemporary = false + + const app = await App.getApp() + const fileContent = JSON.stringify( + { + // TODO: Stringify jigsaw editor here + }, + null, + '\t' + ) + + await app.fileSystem.write(this.fileHandle, fileContent) + } + + async paste() {} + + async copy() {} + + async cut() {} + + async close() { + const didClose = await super.close() + + // We need to clear the lightning cache store from temporary data if the user doesn't save changes + if (!this.isForeignFile && didClose && this.isUnsaved) { + const file = await this.fileHandle.getFile() + const fileContent = await file.text() + await this.project.packIndexer.updateFile( + this.getPath(), + fileContent + ) + } + + return didClose + } + + protected _save(): void | Promise {} +} diff --git a/src/components/Editors/Jigsaw/Tab.vue b/src/components/Editors/Jigsaw/Tab.vue new file mode 100644 index 000000000..ad335c7ca --- /dev/null +++ b/src/components/Editors/Jigsaw/Tab.vue @@ -0,0 +1,246 @@ + + + + + diff --git a/src/components/TabSystem/TabProvider.ts b/src/components/TabSystem/TabProvider.ts index 195df07e7..e8836bb49 100644 --- a/src/components/TabSystem/TabProvider.ts +++ b/src/components/TabSystem/TabProvider.ts @@ -1,5 +1,6 @@ import { ImageTab } from '../Editors/Image/ImageTab' import { TargaTab } from '../Editors/Image/TargaTab' +import { JigsawTab } from '../Editors/Jigsaw/Tab' import { SoundTab } from '../Editors/Sound/SoundTab' import { TextTab } from '../Editors/Text/TextTab' import { TreeTab } from '../Editors/TreeEditor/Tab' @@ -8,6 +9,7 @@ import { FileTab } from './FileTab' export class TabProvider { protected static _tabs = new Set([ TextTab, + JigsawTab, TreeTab, ImageTab, TargaTab,