mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-04 06:28:28 +08:00
Ported various features.
This commit is contained in:
parent
515cba5555
commit
dc0615e1d2
@ -2,6 +2,7 @@ plugins {
|
||||
kotlin("multiplatform")
|
||||
}
|
||||
|
||||
val coroutinesVersion: String by project.ext
|
||||
val kotlinLoggingVersion: String by project.extra
|
||||
|
||||
kotlin {
|
||||
@ -14,6 +15,7 @@ kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
|
||||
api("io.github.microutils:kotlin-logging:$kotlinLoggingVersion")
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,17 @@
|
||||
package world.phantasmal.core.disposable
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
class DisposableSupervisedScope(
|
||||
private val kClass: KClass<*>,
|
||||
context: CoroutineContext,
|
||||
) : TrackedDisposable(), CoroutineScope by CoroutineScope(SupervisorJob() + context) {
|
||||
override fun internalDispose() {
|
||||
cancel("${kClass.simpleName} disposed.")
|
||||
super.internalDispose()
|
||||
}
|
||||
}
|
@ -103,7 +103,7 @@ val generateOpcodes = tasks.register("generateOpcodes") {
|
||||
fun opcodeToCode(writer: PrintWriter, opcode: Map<String, Any>) {
|
||||
val code = (opcode["code"] as String).drop(2).toInt(16)
|
||||
val codeStr = code.toString(16).toUpperCase().padStart(2, '0')
|
||||
val mnemonic = opcode["mnemonic"] as String? ?: "unknown_$codeStr"
|
||||
val mnemonic = opcode["mnemonic"] as String? ?: "unknown_${codeStr.toLowerCase()}"
|
||||
val description = opcode["description"] as String?
|
||||
val stack = opcode["stack"] as String?
|
||||
|
||||
|
@ -0,0 +1,3 @@
|
||||
package world.phantasmal.observable
|
||||
|
||||
fun <T> emitter(): Emitter<T> = SimpleEmitter()
|
@ -42,11 +42,11 @@ dependencies {
|
||||
implementation("org.jetbrains.kotlin:kotlin-serialization:$serializationVersion")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.1.1")
|
||||
implementation(npm("golden-layout", "^1.5.9"))
|
||||
implementation(npm("monaco-editor", "^0.21.2"))
|
||||
implementation(npm("monaco-editor", "0.20.0"))
|
||||
implementation(npm("three", "^0.122.0"))
|
||||
|
||||
implementation(devNpm("file-loader", "^6.0.0"))
|
||||
implementation(devNpm("monaco-editor-webpack-plugin", "^2.0.0"))
|
||||
implementation(devNpm("monaco-editor-webpack-plugin", "1.9.0"))
|
||||
|
||||
testImplementation(kotlin("test-js"))
|
||||
testImplementation(project(":test-utils"))
|
||||
|
@ -2,6 +2,7 @@ package world.phantasmal.web.application.widgets
|
||||
|
||||
import org.w3c.dom.Node
|
||||
import world.phantasmal.observable.value.falseVal
|
||||
import world.phantasmal.observable.value.list.listVal
|
||||
import world.phantasmal.observable.value.value
|
||||
import world.phantasmal.web.application.controllers.NavigationController
|
||||
import world.phantasmal.web.core.dom.externalLink
|
||||
@ -30,8 +31,8 @@ class NavigationWidget(private val ctrl: NavigationController) : Widget() {
|
||||
val serverSelect = Select(
|
||||
enabled = falseVal(),
|
||||
label = "Server:",
|
||||
items = listOf("Ephinea"),
|
||||
selected = "Ephinea",
|
||||
items = listVal("Ephinea"),
|
||||
selected = value("Ephinea"),
|
||||
tooltip = value("Only Ephinea is supported at the moment"),
|
||||
)
|
||||
addWidget(serverSelect.label!!)
|
||||
|
@ -19,9 +19,11 @@ private val NO_SCALE = Vector3(1.0, 1.0, 1.0)
|
||||
fun ninjaObjectToMesh(
|
||||
ninjaObject: NinjaObject<*>,
|
||||
textures: List<XvrTexture>,
|
||||
boundingVolumes: Boolean = false
|
||||
defaultMaterial: Material? = null,
|
||||
boundingVolumes: Boolean = false,
|
||||
): Mesh {
|
||||
val builder = MeshBuilder()
|
||||
defaultMaterial?.let { builder.defaultMaterial(defaultMaterial) }
|
||||
builder.textures(textures)
|
||||
NinjaToMeshConverter(builder).convert(ninjaObject)
|
||||
return builder.buildMesh(boundingVolumes)
|
||||
@ -31,9 +33,11 @@ fun ninjaObjectToInstancedMesh(
|
||||
ninjaObject: NinjaObject<*>,
|
||||
textures: List<XvrTexture>,
|
||||
maxInstances: Int,
|
||||
defaultMaterial: Material? = null,
|
||||
boundingVolumes: Boolean = false,
|
||||
): InstancedMesh {
|
||||
val builder = MeshBuilder()
|
||||
defaultMaterial?.let { builder.defaultMaterial(defaultMaterial) }
|
||||
builder.textures(textures)
|
||||
NinjaToMeshConverter(builder).convert(ninjaObject)
|
||||
return builder.buildInstancedMesh(maxInstances, boundingVolumes)
|
||||
|
@ -1,7 +1,10 @@
|
||||
package world.phantasmal.web.core.stores
|
||||
|
||||
import kotlinx.browser.window
|
||||
import kotlinx.coroutines.launch
|
||||
import org.w3c.dom.events.KeyboardEvent
|
||||
import world.phantasmal.core.disposable.Disposable
|
||||
import world.phantasmal.core.disposable.disposable
|
||||
import world.phantasmal.observable.value.MutableVal
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.eq
|
||||
@ -30,7 +33,7 @@ class UiStore(private val applicationUrl: ApplicationUrl) : Store() {
|
||||
* parameter values per [applicationUrl].
|
||||
*/
|
||||
private val parameters: MutableMap<String, Map<String, String>> = mutableMapOf()
|
||||
private val globalKeydownHandlers: MutableMap<String, (e: KeyboardEvent) -> Unit> =
|
||||
private val globalKeyDownHandlers: MutableMap<String, suspend (e: KeyboardEvent) -> Unit> =
|
||||
mutableMapOf()
|
||||
|
||||
/**
|
||||
@ -78,7 +81,7 @@ class UiStore(private val applicationUrl: ApplicationUrl) : Store() {
|
||||
.toMap()
|
||||
|
||||
addDisposables(
|
||||
window.disposableListener("keydown", ::dispatchGlobalKeydown),
|
||||
window.disposableListener("keydown", ::dispatchGlobalKeyDown),
|
||||
)
|
||||
|
||||
observe(applicationUrl.url) { setDataFromUrl(it) }
|
||||
@ -101,6 +104,21 @@ class UiStore(private val applicationUrl: ApplicationUrl) : Store() {
|
||||
}
|
||||
}
|
||||
|
||||
fun onGlobalKeyDown(
|
||||
tool: PwToolType,
|
||||
binding: String,
|
||||
handler: suspend (KeyboardEvent) -> Unit,
|
||||
): Disposable {
|
||||
val key = handlerKey(tool, binding)
|
||||
require(key !in globalKeyDownHandlers) {
|
||||
"""Binding "$binding" already exists for tool $tool."""
|
||||
}
|
||||
|
||||
globalKeyDownHandlers[key] = handler
|
||||
|
||||
return disposable { globalKeyDownHandlers.remove(key) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets [currentTool], [path], [parameters] and [features].
|
||||
*/
|
||||
@ -169,7 +187,7 @@ class UiStore(private val applicationUrl: ApplicationUrl) : Store() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun dispatchGlobalKeydown(e: KeyboardEvent) {
|
||||
private fun dispatchGlobalKeyDown(e: KeyboardEvent) {
|
||||
val bindingParts = mutableListOf<String>()
|
||||
|
||||
if (e.ctrlKey) bindingParts.add("Ctrl")
|
||||
@ -179,11 +197,11 @@ class UiStore(private val applicationUrl: ApplicationUrl) : Store() {
|
||||
|
||||
val binding = bindingParts.joinToString("-")
|
||||
|
||||
val handler = globalKeydownHandlers[handlerKey(currentTool.value, binding)]
|
||||
val handler = globalKeyDownHandlers[handlerKey(currentTool.value, binding)]
|
||||
|
||||
if (handler != null) {
|
||||
e.preventDefault()
|
||||
handler(e)
|
||||
scope.launch { handler(e) }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,57 @@
|
||||
package world.phantasmal.web.core.undo
|
||||
|
||||
import world.phantasmal.observable.value.*
|
||||
import world.phantasmal.web.core.actions.Action
|
||||
|
||||
/**
|
||||
* Simply contains a single action. [canUndo] and [canRedo] must be managed manually.
|
||||
*/
|
||||
class SimpleUndo(
|
||||
undoManager: UndoManager,
|
||||
private val description: String,
|
||||
undo: () -> Unit,
|
||||
redo: () -> Unit,
|
||||
) : Undo {
|
||||
private val action = object : Action {
|
||||
override val description: String = this@SimpleUndo.description
|
||||
|
||||
override fun execute() {
|
||||
redo()
|
||||
}
|
||||
|
||||
override fun undo() {
|
||||
undo()
|
||||
}
|
||||
}
|
||||
|
||||
override val canUndo: MutableVal<Boolean> = mutableVal(false)
|
||||
override val canRedo: MutableVal<Boolean> = mutableVal(false)
|
||||
|
||||
override val firstUndo: Val<Action?> = canUndo.map { if (it) action else null }
|
||||
override val firstRedo: Val<Action?> = canRedo.map { if (it) action else null }
|
||||
|
||||
init {
|
||||
undoManager.addUndo(this)
|
||||
}
|
||||
|
||||
override fun undo(): Boolean =
|
||||
if (canUndo.value) {
|
||||
action.undo()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
override fun redo(): Boolean =
|
||||
if (canRedo.value) {
|
||||
action.execute()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
override fun reset() {
|
||||
canUndo.value = false
|
||||
canRedo.value = false
|
||||
}
|
||||
}
|
@ -17,10 +17,6 @@ interface Undo {
|
||||
*/
|
||||
val firstRedo: Val<Action?>
|
||||
|
||||
/**
|
||||
* Ensures this undo is the current undo in its [UndoManager].
|
||||
*/
|
||||
fun makeCurrent()
|
||||
fun undo(): Boolean
|
||||
fun redo(): Boolean
|
||||
fun reset()
|
||||
|
@ -7,6 +7,7 @@ import world.phantasmal.observable.value.nullVal
|
||||
import world.phantasmal.web.core.actions.Action
|
||||
|
||||
class UndoManager {
|
||||
private val undos = mutableListOf<Undo>()
|
||||
private val _current = mutableVal<Undo>(NopUndo)
|
||||
|
||||
val current: Val<Undo> = _current
|
||||
@ -16,7 +17,13 @@ class UndoManager {
|
||||
val firstUndo: Val<Action?> = current.flatMap { it.firstUndo }
|
||||
val firstRedo: Val<Action?> = current.flatMap { it.firstRedo }
|
||||
|
||||
fun addUndo(undo: Undo) {
|
||||
undos.add(undo)
|
||||
}
|
||||
|
||||
fun setCurrent(undo: Undo) {
|
||||
require(undo in undos) { "Undo $undo is not managed by this UndoManager." }
|
||||
|
||||
_current.value = undo
|
||||
}
|
||||
|
||||
@ -26,8 +33,11 @@ class UndoManager {
|
||||
fun redo(): Boolean =
|
||||
current.value.redo()
|
||||
|
||||
fun makeNopCurrent() {
|
||||
setCurrent(NopUndo)
|
||||
/**
|
||||
* Resets all managed undos.
|
||||
*/
|
||||
fun reset() {
|
||||
undos.forEach { it.reset() }
|
||||
}
|
||||
|
||||
private object NopUndo : Undo {
|
||||
@ -36,10 +46,6 @@ class UndoManager {
|
||||
override val firstUndo = nullVal()
|
||||
override val firstRedo = nullVal()
|
||||
|
||||
override fun makeCurrent() {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
override fun undo(): Boolean = false
|
||||
|
||||
override fun redo(): Boolean = false
|
||||
|
@ -10,7 +10,7 @@ import world.phantasmal.web.core.actions.Action
|
||||
/**
|
||||
* Full-fledged linear undo/redo implementation.
|
||||
*/
|
||||
class UndoStack(private val manager: UndoManager) : Undo {
|
||||
class UndoStack(manager: UndoManager) : Undo {
|
||||
private val stack = mutableListVal<Action>()
|
||||
|
||||
/**
|
||||
@ -20,6 +20,10 @@ class UndoStack(private val manager: UndoManager) : Undo {
|
||||
private val index = mutableVal(0)
|
||||
private var undoingOrRedoing = false
|
||||
|
||||
init {
|
||||
manager.addUndo(this)
|
||||
}
|
||||
|
||||
override val canUndo: Val<Boolean> = index gt 0
|
||||
|
||||
override val canRedo: Val<Boolean> = map(stack, index) { stack, index -> index < stack.size }
|
||||
@ -28,10 +32,6 @@ class UndoStack(private val manager: UndoManager) : Undo {
|
||||
|
||||
override val firstRedo: Val<Action?> = index.map { stack.value.getOrNull(it) }
|
||||
|
||||
override fun makeCurrent() {
|
||||
manager.setCurrent(this)
|
||||
}
|
||||
|
||||
fun push(action: Action): Action {
|
||||
if (!undoingOrRedoing) {
|
||||
stack.splice(index.value, stack.value.size - index.value, action)
|
||||
|
@ -7,6 +7,7 @@ package world.phantasmal.web.externals.monacoEditor
|
||||
|
||||
import org.w3c.dom.HTMLElement
|
||||
import org.w3c.dom.Range
|
||||
import kotlin.js.Promise
|
||||
|
||||
external fun create(
|
||||
domElement: HTMLElement,
|
||||
@ -122,7 +123,7 @@ external interface IEditor {
|
||||
scrollType: ScrollType = definedExternally,
|
||||
)
|
||||
|
||||
fun trigger(source: String?, handlerId: String, payload: Any)
|
||||
fun trigger(source: String?, handlerId: String, payload: dynamic)
|
||||
fun getModel(): dynamic /* ITextModel? | IDiffEditorModel? */
|
||||
fun setModel(model: ITextModel?)
|
||||
}
|
||||
@ -180,6 +181,26 @@ external interface ICodeEditor : IEditor {
|
||||
fun getOffsetForColumn(lineNumber: Number, column: Number): Number
|
||||
fun render(forceRedraw: Boolean = definedExternally)
|
||||
fun applyFontInfo(target: HTMLElement)
|
||||
fun getSupportedActions(): Array<IEditorAction>
|
||||
fun getAction(id: String): IEditorAction
|
||||
fun addAction(descriptor: IActionDescriptor): IDisposable
|
||||
}
|
||||
|
||||
external interface IActionDescriptor {
|
||||
var id: String
|
||||
var label: String
|
||||
var keybindings: Array<Int>
|
||||
|
||||
fun run(editor: ICodeEditor, vararg args: dynamic): dynamic
|
||||
}
|
||||
|
||||
external interface IEditorAction {
|
||||
val id: String
|
||||
val label: String
|
||||
val alias: String
|
||||
|
||||
fun isSupported(): Boolean
|
||||
fun run(): Promise<Unit>
|
||||
}
|
||||
|
||||
external interface IStandaloneCodeEditor : ICodeEditor {
|
||||
@ -540,8 +561,8 @@ external interface ITextModel {
|
||||
var uri: Uri
|
||||
var id: String
|
||||
fun getOptions(): TextModelResolvedOptions
|
||||
fun getVersionId(): Number
|
||||
fun getAlternativeVersionId(): Number
|
||||
fun getVersionId(): Int
|
||||
fun getAlternativeVersionId(): Int
|
||||
fun setValue(newValue: String)
|
||||
fun getValue(
|
||||
eol: EndOfLinePreference = definedExternally,
|
||||
|
@ -181,3 +181,203 @@ open external class Uri : UriComponents {
|
||||
fun revive(data: Uri? = definedExternally): Uri?
|
||||
}
|
||||
}
|
||||
|
||||
external object KeyCode {
|
||||
/**
|
||||
* Placed first to cover the 0 value of the enum.
|
||||
*/
|
||||
val Unknown: Int /* = 0 */
|
||||
val Backspace: Int /* = 1 */
|
||||
val Tab: Int /* = 2 */
|
||||
val Enter: Int /* = 3 */
|
||||
val Shift: Int /* = 4 */
|
||||
val Ctrl: Int /* = 5 */
|
||||
val Alt: Int /* = 6 */
|
||||
val PauseBreak: Int /* = 7 */
|
||||
val CapsLock: Int /* = 8 */
|
||||
val Escape: Int /* = 9 */
|
||||
val Space: Int /* = 10 */
|
||||
val PageUp: Int /* = 11 */
|
||||
val PageDown: Int /* = 12 */
|
||||
val End: Int /* = 13 */
|
||||
val Home: Int /* = 14 */
|
||||
val LeftArrow: Int /* = 15 */
|
||||
val UpArrow: Int /* = 16 */
|
||||
val RightArrow: Int /* = 17 */
|
||||
val DownArrow: Int /* = 18 */
|
||||
val Insert: Int /* = 19 */
|
||||
val Delete: Int /* = 20 */
|
||||
val KEY_0: Int /* = 21 */
|
||||
val KEY_1: Int /* = 22 */
|
||||
val KEY_2: Int /* = 23 */
|
||||
val KEY_3: Int /* = 24 */
|
||||
val KEY_4: Int /* = 25 */
|
||||
val KEY_5: Int /* = 26 */
|
||||
val KEY_6: Int /* = 27 */
|
||||
val KEY_7: Int /* = 28 */
|
||||
val KEY_8: Int /* = 29 */
|
||||
val KEY_9: Int /* = 30 */
|
||||
val KEY_A: Int /* = 31 */
|
||||
val KEY_B: Int /* = 32 */
|
||||
val KEY_C: Int /* = 33 */
|
||||
val KEY_D: Int /* = 34 */
|
||||
val KEY_E: Int /* = 35 */
|
||||
val KEY_F: Int /* = 36 */
|
||||
val KEY_G: Int /* = 37 */
|
||||
val KEY_H: Int /* = 38 */
|
||||
val KEY_I: Int /* = 39 */
|
||||
val KEY_J: Int /* = 40 */
|
||||
val KEY_K: Int /* = 41 */
|
||||
val KEY_L: Int /* = 42 */
|
||||
val KEY_M: Int /* = 43 */
|
||||
val KEY_N: Int /* = 44 */
|
||||
val KEY_O: Int /* = 45 */
|
||||
val KEY_P: Int /* = 46 */
|
||||
val KEY_Q: Int /* = 47 */
|
||||
val KEY_R: Int /* = 48 */
|
||||
val KEY_S: Int /* = 49 */
|
||||
val KEY_T: Int /* = 50 */
|
||||
val KEY_U: Int /* = 51 */
|
||||
val KEY_V: Int /* = 52 */
|
||||
val KEY_W: Int /* = 53 */
|
||||
val KEY_X: Int /* = 54 */
|
||||
val KEY_Y: Int /* = 55 */
|
||||
val KEY_Z: Int /* = 56 */
|
||||
val Meta: Int /* = 57 */
|
||||
val ContextMenu: Int /* = 58 */
|
||||
val F1: Int /* = 59 */
|
||||
val F2: Int /* = 60 */
|
||||
val F3: Int /* = 61 */
|
||||
val F4: Int /* = 62 */
|
||||
val F5: Int /* = 63 */
|
||||
val F6: Int /* = 64 */
|
||||
val F7: Int /* = 65 */
|
||||
val F8: Int /* = 66 */
|
||||
val F9: Int /* = 67 */
|
||||
val F10: Int /* = 68 */
|
||||
val F11: Int /* = 69 */
|
||||
val F12: Int /* = 70 */
|
||||
val F13: Int /* = 71 */
|
||||
val F14: Int /* = 72 */
|
||||
val F15: Int /* = 73 */
|
||||
val F16: Int /* = 74 */
|
||||
val F17: Int /* = 75 */
|
||||
val F18: Int /* = 76 */
|
||||
val F19: Int /* = 77 */
|
||||
val NumLock: Int /* = 78 */
|
||||
val ScrollLock: Int /* = 79 */
|
||||
|
||||
/**
|
||||
* Used for miscellaneous characters; it can vary by keyboard.
|
||||
* For the US standard keyboard, the ';:' key
|
||||
*/
|
||||
val US_SEMICOLON: Int /* = 80 */
|
||||
|
||||
/**
|
||||
* For any country/region, the '+' key
|
||||
* For the US standard keyboard, the '=+' key
|
||||
*/
|
||||
val US_EQUAL: Int /* = 81 */
|
||||
|
||||
/**
|
||||
* For any country/region, the ',' key
|
||||
* For the US standard keyboard, the ',<' key
|
||||
*/
|
||||
val US_COMMA: Int /* = 82 */
|
||||
|
||||
/**
|
||||
* For any country/region, the '-' key
|
||||
* For the US standard keyboard, the '-_' key
|
||||
*/
|
||||
val US_MINUS: Int /* = 83 */
|
||||
|
||||
/**
|
||||
* For any country/region, the '.' key
|
||||
* For the US standard keyboard, the '.>' key
|
||||
*/
|
||||
val US_DOT: Int /* = 84 */
|
||||
|
||||
/**
|
||||
* Used for miscellaneous characters; it can vary by keyboard.
|
||||
* For the US standard keyboard, the '/?' key
|
||||
*/
|
||||
val US_SLASH: Int /* = 85 */
|
||||
|
||||
/**
|
||||
* Used for miscellaneous characters; it can vary by keyboard.
|
||||
* For the US standard keyboard, the '`~' key
|
||||
*/
|
||||
val US_BACKTICK: Int /* = 86 */
|
||||
|
||||
/**
|
||||
* Used for miscellaneous characters; it can vary by keyboard.
|
||||
* For the US standard keyboard, the '[{' key
|
||||
*/
|
||||
val US_OPEN_SQUARE_BRACKET: Int /* = 87 */
|
||||
|
||||
/**
|
||||
* Used for miscellaneous characters; it can vary by keyboard.
|
||||
* For the US standard keyboard, the '\|' key
|
||||
*/
|
||||
val US_BACKSLASH: Int /* = 88 */
|
||||
|
||||
/**
|
||||
* Used for miscellaneous characters; it can vary by keyboard.
|
||||
* For the US standard keyboard, the ']}' key
|
||||
*/
|
||||
val US_CLOSE_SQUARE_BRACKET: Int /* = 89 */
|
||||
|
||||
/**
|
||||
* Used for miscellaneous characters; it can vary by keyboard.
|
||||
* For the US standard keyboard, the ''"' key
|
||||
*/
|
||||
val US_QUOTE: Int /* = 90 */
|
||||
|
||||
/**
|
||||
* Used for miscellaneous characters; it can vary by keyboard.
|
||||
*/
|
||||
val OEM_8: Int /* = 91 */
|
||||
|
||||
/**
|
||||
* Either the angle bracket key or the backslash key on the RT 102-key keyboard.
|
||||
*/
|
||||
val OEM_102: Int /* = 92 */
|
||||
val NUMPAD_0: Int /* = 93 */
|
||||
val NUMPAD_1: Int /* = 94 */
|
||||
val NUMPAD_2: Int /* = 95 */
|
||||
val NUMPAD_3: Int /* = 96 */
|
||||
val NUMPAD_4: Int /* = 97 */
|
||||
val NUMPAD_5: Int /* = 98 */
|
||||
val NUMPAD_6: Int /* = 99 */
|
||||
val NUMPAD_7: Int /* = 100 */
|
||||
val NUMPAD_8: Int /* = 101 */
|
||||
val NUMPAD_9: Int /* = 102 */
|
||||
val NUMPAD_MULTIPLY: Int /* = 103 */
|
||||
val NUMPAD_ADD: Int /* = 104 */
|
||||
val NUMPAD_SEPARATOR: Int /* = 105 */
|
||||
val NUMPAD_SUBTRACT: Int /* = 106 */
|
||||
val NUMPAD_DECIMAL: Int /* = 107 */
|
||||
val NUMPAD_DIVIDE: Int /* = 108 */
|
||||
|
||||
/**
|
||||
* Cover all key codes when IME is processing input.
|
||||
*/
|
||||
val KEY_IN_COMPOSITION: Int /* = 109 */
|
||||
val ABNT_C1: Int /* = 110 */
|
||||
val ABNT_C2: Int /* = 111 */
|
||||
|
||||
/**
|
||||
* Placed last to cover the length of the enum.
|
||||
* Please do not depend on this value!
|
||||
*/
|
||||
val MAX_VALUE: Int /* = 112 */
|
||||
}
|
||||
|
||||
external object KeyMod {
|
||||
val CtrlCmd: Int
|
||||
val Shift: Int
|
||||
val Alt: Int
|
||||
val WinCtrl: Int
|
||||
|
||||
fun chord(firstPart: Int, secondPart: Int): Int
|
||||
}
|
||||
|
@ -535,7 +535,7 @@ external class CylinderBufferGeometry(
|
||||
open external class BufferAttribute {
|
||||
var needsUpdate: Boolean
|
||||
|
||||
fun copyAt(index1: Int, attribute: BufferAttribute, index2: Int): BufferAttribute
|
||||
fun copyAt(index1: Int, bufferAttribute: BufferAttribute, index2: Int): BufferAttribute
|
||||
}
|
||||
|
||||
external class Uint16BufferAttribute(
|
||||
|
@ -6,6 +6,7 @@ import world.phantasmal.web.core.PwToolType
|
||||
import world.phantasmal.web.core.loading.AssetLoader
|
||||
import world.phantasmal.web.core.rendering.DisposableThreeRenderer
|
||||
import world.phantasmal.web.core.stores.UiStore
|
||||
import world.phantasmal.web.core.undo.UndoManager
|
||||
import world.phantasmal.web.questEditor.controllers.*
|
||||
import world.phantasmal.web.questEditor.loading.AreaAssetLoader
|
||||
import world.phantasmal.web.questEditor.loading.EntityAssetLoader
|
||||
@ -14,7 +15,7 @@ import world.phantasmal.web.questEditor.persistence.QuestEditorUiPersister
|
||||
import world.phantasmal.web.questEditor.rendering.EntityImageRenderer
|
||||
import world.phantasmal.web.questEditor.rendering.QuestRenderer
|
||||
import world.phantasmal.web.questEditor.stores.AreaStore
|
||||
import world.phantasmal.web.questEditor.stores.AssemblyEditorStore
|
||||
import world.phantasmal.web.questEditor.stores.AsmStore
|
||||
import world.phantasmal.web.questEditor.stores.QuestEditorStore
|
||||
import world.phantasmal.web.questEditor.widgets.*
|
||||
import world.phantasmal.webui.DisposableContainer
|
||||
@ -36,14 +37,22 @@ class QuestEditor(
|
||||
// Persistence
|
||||
val questEditorUiPersister = QuestEditorUiPersister()
|
||||
|
||||
// Undo
|
||||
val undoManager = UndoManager()
|
||||
|
||||
// Stores
|
||||
val areaStore = addDisposable(AreaStore(areaAssetLoader))
|
||||
val questEditorStore = addDisposable(QuestEditorStore(uiStore, areaStore))
|
||||
val assemblyEditorStore = addDisposable(AssemblyEditorStore(questEditorStore))
|
||||
val questEditorStore = addDisposable(QuestEditorStore(
|
||||
uiStore,
|
||||
areaStore,
|
||||
undoManager,
|
||||
))
|
||||
val asmStore = addDisposable(AsmStore(questEditorStore, undoManager))
|
||||
|
||||
// Controllers
|
||||
val questEditorController = addDisposable(QuestEditorController(questEditorUiPersister))
|
||||
val toolbarController = addDisposable(QuestEditorToolbarController(
|
||||
uiStore,
|
||||
questLoader,
|
||||
areaStore,
|
||||
questEditorStore,
|
||||
@ -51,7 +60,7 @@ class QuestEditor(
|
||||
val questInfoController = addDisposable(QuestInfoController(questEditorStore))
|
||||
val npcCountsController = addDisposable(NpcCountsController(questEditorStore))
|
||||
val entityInfoController = addDisposable(EntityInfoController(questEditorStore))
|
||||
val assemblyEditorController = addDisposable(AssemblyEditorController(assemblyEditorStore))
|
||||
val asmController = addDisposable(AsmController(asmStore))
|
||||
val npcListController = addDisposable(EntityListController(questEditorStore, npcs = true))
|
||||
val objectListController =
|
||||
addDisposable(EntityListController(questEditorStore, npcs = false))
|
||||
@ -73,7 +82,7 @@ class QuestEditor(
|
||||
{ NpcCountsWidget(npcCountsController) },
|
||||
{ EntityInfoWidget(entityInfoController) },
|
||||
{ QuestEditorRendererWidget(renderer) },
|
||||
{ AssemblyEditorWidget(assemblyEditorController) },
|
||||
{ AsmWidget(asmController) },
|
||||
{ EntityListWidget(npcListController, entityImageRenderer) },
|
||||
{ EntityListWidget(objectListController, entityImageRenderer) },
|
||||
)
|
||||
|
@ -0,0 +1,22 @@
|
||||
package world.phantasmal.web.questEditor.actions
|
||||
|
||||
import world.phantasmal.web.core.actions.Action
|
||||
import world.phantasmal.web.questEditor.models.QuestEntityModel
|
||||
import world.phantasmal.web.questEditor.models.QuestModel
|
||||
|
||||
class DeleteEntityAction(
|
||||
private val setSelectedEntity: (QuestEntityModel<*, *>) -> Unit,
|
||||
private val quest: QuestModel,
|
||||
private val entity: QuestEntityModel<*, *>,
|
||||
) :Action{
|
||||
override val description: String = "Delete ${entity.type.name}"
|
||||
|
||||
override fun execute() {
|
||||
quest.removeEntity(entity)
|
||||
}
|
||||
|
||||
override fun undo() {
|
||||
quest.addEntity(entity)
|
||||
setSelectedEntity(entity)
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
package world.phantasmal.web.questEditor.assembly
|
||||
package world.phantasmal.web.questEditor.asm
|
||||
|
||||
import world.phantasmal.core.disposable.TrackedDisposable
|
||||
|
||||
class AssemblyAnalyser : TrackedDisposable() {
|
||||
class AsmAnalyser : TrackedDisposable() {
|
||||
fun setAssembly(assembly: List<String>) {
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
package world.phantasmal.web.questEditor.controllers
|
||||
|
||||
import world.phantasmal.observable.Observable
|
||||
import world.phantasmal.observable.value.*
|
||||
import world.phantasmal.web.externals.monacoEditor.ITextModel
|
||||
import world.phantasmal.web.externals.monacoEditor.createModel
|
||||
import world.phantasmal.web.questEditor.stores.AsmStore
|
||||
import world.phantasmal.webui.controllers.Controller
|
||||
|
||||
class AsmController(private val store: AsmStore) : Controller() {
|
||||
val enabled: Val<Boolean> = store.editingEnabled
|
||||
val readOnly: Val<Boolean> = !enabled or store.textModel.isNull()
|
||||
|
||||
val textModel: Val<ITextModel> = store.textModel.orElse { EMPTY_MODEL }
|
||||
|
||||
val didUndo: Observable<Unit> = store.didUndo
|
||||
val didRedo: Observable<Unit> = store.didRedo
|
||||
|
||||
val inlineStackArgs: Val<Boolean> = store.inlineStackArgs
|
||||
val inlineStackArgsEnabled: Val<Boolean> = falseVal() // TODO
|
||||
|
||||
// TODO: Notify user when disabled because of issues with the ASM.
|
||||
val inlineStackArgsTooltip: Val<String> = value(
|
||||
"Transform arg_push* opcodes to be inline with the opcode the arguments are given to."
|
||||
)
|
||||
|
||||
fun makeUndoCurrent() {
|
||||
store.makeUndoCurrent()
|
||||
}
|
||||
|
||||
fun setInlineStackArgs(value: Boolean) {
|
||||
TODO()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val EMPTY_MODEL = createModel("", AsmStore.ASM_LANG_ID)
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
package world.phantasmal.web.questEditor.controllers
|
||||
|
||||
import world.phantasmal.observable.value.*
|
||||
import world.phantasmal.web.externals.monacoEditor.ITextModel
|
||||
import world.phantasmal.web.externals.monacoEditor.createModel
|
||||
import world.phantasmal.web.questEditor.stores.AssemblyEditorStore
|
||||
import world.phantasmal.webui.controllers.Controller
|
||||
|
||||
class AssemblyEditorController(assemblyEditorStore: AssemblyEditorStore) : Controller() {
|
||||
val textModel: Val<ITextModel> = assemblyEditorStore.textModel.orElse { EMPTY_MODEL }
|
||||
val enabled: Val<Boolean> = assemblyEditorStore.editingEnabled
|
||||
val readOnly: Val<Boolean> = enabled.not() or assemblyEditorStore.textModel.isNull()
|
||||
|
||||
companion object {
|
||||
private val EMPTY_MODEL = createModel("", AssemblyEditorStore.ASM_LANG_ID)
|
||||
}
|
||||
}
|
@ -7,13 +7,17 @@ import world.phantasmal.web.questEditor.models.QuestNpcModel
|
||||
import world.phantasmal.web.questEditor.stores.QuestEditorStore
|
||||
import world.phantasmal.webui.controllers.Controller
|
||||
|
||||
class NpcCountsController(store: QuestEditorStore) : Controller() {
|
||||
class NpcCountsController(private val store: QuestEditorStore) : Controller() {
|
||||
val unavailable: Val<Boolean> = store.currentQuest.isNull()
|
||||
|
||||
val npcCounts: Val<List<NameWithCount>> = store.currentQuest
|
||||
.flatMap { it?.npcs ?: emptyListVal() }
|
||||
.map(::countNpcs)
|
||||
|
||||
fun focused() {
|
||||
store.makeMainUndoCurrent()
|
||||
}
|
||||
|
||||
private fun countNpcs(npcs: List<QuestNpcModel>): List<NameWithCount> {
|
||||
val npcCounts = mutableMapOf<NpcType, Int>()
|
||||
var extraCanadines = 0
|
||||
|
@ -15,24 +15,24 @@ class QuestEditorController(
|
||||
|
||||
companion object {
|
||||
// These IDs are persisted, don't change them.
|
||||
const val QUEST_INFO_WIDGET_ID = "info"
|
||||
const val NPC_COUNTS_WIDGET_ID = "npc_counts"
|
||||
const val ENTITY_INFO_WIDGET_ID = "entity_info"
|
||||
const val QUEST_RENDERER_WIDGET_ID = "quest_renderer"
|
||||
const val ASSEMBLY_EDITOR_WIDGET_ID = "asm_editor"
|
||||
const val NPC_LIST_WIDGET_ID = "npc_list_view"
|
||||
const val OBJECT_LIST_WIDGET_ID = "object_list_view"
|
||||
const val EVENTS_WIDGET_ID = "events_view"
|
||||
const val QUEST_INFO_WIDGET_ID = "quest-info"
|
||||
const val NPC_COUNTS_WIDGET_ID = "npc-counts"
|
||||
const val ENTITY_INFO_WIDGET_ID = "entity-info"
|
||||
const val QUEST_RENDERER_WIDGET_ID = "quest-renderer"
|
||||
const val ASM_WIDGET_ID = "asm"
|
||||
const val NPC_LIST_WIDGET_ID = "npc-list"
|
||||
const val OBJECT_LIST_WIDGET_ID = "object-list"
|
||||
const val EVENTS_WIDGET_ID = "events"
|
||||
|
||||
private val ALL_WIDGET_IDS: Set<String> = setOf(
|
||||
"info",
|
||||
"npc_counts",
|
||||
"entity_info",
|
||||
"quest_renderer",
|
||||
"asm_editor",
|
||||
"npc_list_view",
|
||||
"object_list_view",
|
||||
"events_view",
|
||||
QUEST_INFO_WIDGET_ID,
|
||||
NPC_COUNTS_WIDGET_ID,
|
||||
ENTITY_INFO_WIDGET_ID,
|
||||
QUEST_RENDERER_WIDGET_ID,
|
||||
ASM_WIDGET_ID,
|
||||
NPC_LIST_WIDGET_ID,
|
||||
OBJECT_LIST_WIDGET_ID,
|
||||
EVENTS_WIDGET_ID,
|
||||
)
|
||||
|
||||
private val DEFAULT_CONFIG = DockedRow(
|
||||
@ -67,7 +67,7 @@ class QuestEditorController(
|
||||
),
|
||||
DockedWidget(
|
||||
title = "Script",
|
||||
id = ASSEMBLY_EDITOR_WIDGET_ID,
|
||||
id = ASM_WIDGET_ID,
|
||||
),
|
||||
)
|
||||
),
|
||||
|
@ -13,6 +13,8 @@ import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.map
|
||||
import world.phantasmal.observable.value.mutableVal
|
||||
import world.phantasmal.observable.value.value
|
||||
import world.phantasmal.web.core.PwToolType
|
||||
import world.phantasmal.web.core.stores.UiStore
|
||||
import world.phantasmal.web.questEditor.loading.QuestLoader
|
||||
import world.phantasmal.web.questEditor.models.AreaModel
|
||||
import world.phantasmal.web.questEditor.stores.AreaStore
|
||||
@ -20,12 +22,14 @@ import world.phantasmal.web.questEditor.stores.QuestEditorStore
|
||||
import world.phantasmal.web.questEditor.stores.convertQuestToModel
|
||||
import world.phantasmal.webui.controllers.Controller
|
||||
import world.phantasmal.webui.readFile
|
||||
import world.phantasmal.webui.selectFiles
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
class AreaAndLabel(val area: AreaModel, val label: String)
|
||||
|
||||
class QuestEditorToolbarController(
|
||||
uiStore: UiStore,
|
||||
private val questLoader: QuestLoader,
|
||||
private val areaStore: AreaStore,
|
||||
private val questEditorStore: QuestEditorStore,
|
||||
@ -38,6 +42,8 @@ class QuestEditorToolbarController(
|
||||
val resultDialogVisible: Val<Boolean> = _resultDialogVisible
|
||||
val result: Val<PwResult<*>?> = _result
|
||||
|
||||
val openFileAccept = ".bin, .dat, .qst"
|
||||
|
||||
// Undo
|
||||
|
||||
val undoTooltip: Val<String> = questEditorStore.firstUndo.map { action ->
|
||||
@ -75,6 +81,26 @@ class QuestEditorToolbarController(
|
||||
|
||||
val areaSelectEnabled: Val<Boolean> = questEditorStore.currentQuest.isNotNull()
|
||||
|
||||
init {
|
||||
addDisposables(
|
||||
uiStore.onGlobalKeyDown(PwToolType.QuestEditor, "Ctrl-O") {
|
||||
openFiles(selectFiles(accept = openFileAccept, multiple = true))
|
||||
},
|
||||
|
||||
uiStore.onGlobalKeyDown(PwToolType.QuestEditor, "Ctrl-Z") {
|
||||
undo()
|
||||
},
|
||||
|
||||
uiStore.onGlobalKeyDown(PwToolType.QuestEditor, "Ctrl-Shift-Z") {
|
||||
redo()
|
||||
},
|
||||
|
||||
uiStore.onGlobalKeyDown(PwToolType.QuestEditor, "Ctrl-Y") {
|
||||
redo()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun createNewQuest(episode: Episode) {
|
||||
// TODO: Set filename and version.
|
||||
questEditorStore.setCurrentQuest(
|
||||
|
@ -14,10 +14,9 @@ import world.phantasmal.lib.fileFormats.quest.ObjectType
|
||||
import world.phantasmal.web.core.loading.AssetLoader
|
||||
import world.phantasmal.web.core.rendering.conversion.ninjaObjectToInstancedMesh
|
||||
import world.phantasmal.web.core.rendering.disposeObject3DResources
|
||||
import world.phantasmal.web.externals.three.CylinderBufferGeometry
|
||||
import world.phantasmal.web.externals.three.InstancedMesh
|
||||
import world.phantasmal.web.externals.three.MeshLambertMaterial
|
||||
import world.phantasmal.web.externals.three.*
|
||||
import world.phantasmal.webui.DisposableContainer
|
||||
import world.phantasmal.webui.obj
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
@ -26,10 +25,11 @@ class EntityAssetLoader(private val assetLoader: AssetLoader) : DisposableContai
|
||||
LoadingCache<Pair<EntityType, Int?>, InstancedMesh>(
|
||||
{ (type, model) ->
|
||||
try {
|
||||
loadMesh(type, model) ?: DEFAULT_MESH
|
||||
loadMesh(type, model)
|
||||
?: if (type is NpcType) DEFAULT_NPC_MESH else DEFAULT_OBJECT_MESH
|
||||
} catch (e: Exception) {
|
||||
logger.error(e) { "Couldn't load mesh for $type (model: $model)." }
|
||||
DEFAULT_MESH
|
||||
if (type is NpcType) DEFAULT_NPC_MESH else DEFAULT_OBJECT_MESH
|
||||
}
|
||||
},
|
||||
::disposeObject3DResources
|
||||
@ -60,8 +60,12 @@ class EntityAssetLoader(private val assetLoader: AssetLoader) : DisposableContai
|
||||
ninjaObject,
|
||||
textures,
|
||||
maxInstances = 300,
|
||||
defaultMaterial = MeshLambertMaterial(obj {
|
||||
color = if (type is NpcType) DEFAULT_NPC_COLOR else DEFAULT_OBJECT_COLOR
|
||||
side = DoubleSide
|
||||
}),
|
||||
boundingVolumes = true,
|
||||
)
|
||||
).apply { name = type.uniqueName }
|
||||
}
|
||||
|
||||
private suspend fun loadTextures(type: EntityType, model: Int?): List<XvrTexture> {
|
||||
@ -121,23 +125,30 @@ class EntityAssetLoader(private val assetLoader: AssetLoader) : DisposableContai
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DEFAULT_MESH = InstancedMesh(
|
||||
CylinderBufferGeometry(
|
||||
radiusTop = 2.5,
|
||||
radiusBottom = 2.5,
|
||||
height = 18.0,
|
||||
radialSegments = 16,
|
||||
private val DEFAULT_NPC_COLOR = Color(0xFF0000)
|
||||
private val DEFAULT_OBJECT_COLOR = Color(0xFFFF00)
|
||||
|
||||
private val DEFAULT_NPC_MESH = createCylinder(DEFAULT_NPC_COLOR)
|
||||
private val DEFAULT_OBJECT_MESH = createCylinder(DEFAULT_OBJECT_COLOR)
|
||||
|
||||
private fun createCylinder(color: Color) =
|
||||
InstancedMesh(
|
||||
CylinderBufferGeometry(
|
||||
radiusTop = 2.5,
|
||||
radiusBottom = 2.5,
|
||||
height = 18.0,
|
||||
radialSegments = 20,
|
||||
).apply {
|
||||
translate(0.0, 9.0, 0.0)
|
||||
computeBoundingBox()
|
||||
computeBoundingSphere()
|
||||
},
|
||||
MeshLambertMaterial(obj { this.color = color }),
|
||||
count = 1000,
|
||||
).apply {
|
||||
translate(0.0, 9.0, 0.0)
|
||||
computeBoundingBox()
|
||||
computeBoundingSphere()
|
||||
},
|
||||
MeshLambertMaterial(),
|
||||
count = 1000,
|
||||
).apply {
|
||||
// Start with 0 instances.
|
||||
count = 0
|
||||
}
|
||||
// Start with 0 instances.
|
||||
count = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,7 +8,7 @@ class LoadingCache<K, V>(
|
||||
private val loadValue: suspend (K) -> V,
|
||||
private val disposeValue: (V) -> Unit,
|
||||
) : TrackedDisposable() {
|
||||
private val scope: CoroutineScope = CoroutineScope(Dispatchers.Default)
|
||||
private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
private val map = mutableMapOf<K, Deferred<V>>()
|
||||
|
||||
val values: Collection<Deferred<V>> = map.values
|
||||
|
@ -1,5 +1,6 @@
|
||||
package world.phantasmal.web.questEditor.rendering
|
||||
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import mu.KotlinLogging
|
||||
import world.phantasmal.lib.fileFormats.quest.Episode
|
||||
import world.phantasmal.web.questEditor.loading.AreaAssetLoader
|
||||
@ -21,6 +22,8 @@ class AreaMeshManager(
|
||||
try {
|
||||
renderContext.collisionGeometry =
|
||||
areaAssetLoader.loadCollisionGeometry(episode, areaVariant)
|
||||
} catch (e: CancellationException) {
|
||||
// Do nothing.
|
||||
} catch (e: Exception) {
|
||||
logger.error(e) {
|
||||
"Couldn't load models for area ${areaVariant.area.id}, variant ${areaVariant.id}."
|
||||
|
@ -59,13 +59,14 @@ class EntityInstancedMesh(
|
||||
|
||||
private fun removeAt(index: Int) {
|
||||
val instance = instances.removeAt(index)
|
||||
instance.mesh.count--
|
||||
mesh.count--
|
||||
|
||||
for (i in index until instance.mesh.count) {
|
||||
instance.mesh.instanceMatrix.copyAt(i, instance.mesh.instanceMatrix, i + 1)
|
||||
for (i in index..instances.lastIndex) {
|
||||
mesh.instanceMatrix.copyAt(i, mesh.instanceMatrix, i + 1)
|
||||
instances[i].instanceIndex = i
|
||||
}
|
||||
|
||||
mesh.instanceMatrix.needsUpdate = true
|
||||
instance.dispose()
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,7 @@ package world.phantasmal.web.questEditor.rendering
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import mu.KotlinLogging
|
||||
import world.phantasmal.core.disposable.DisposableSupervisedScope
|
||||
import world.phantasmal.lib.fileFormats.quest.EntityType
|
||||
import world.phantasmal.web.externals.three.BoxHelper
|
||||
import world.phantasmal.web.externals.three.Color
|
||||
@ -19,7 +20,7 @@ class EntityMeshManager(
|
||||
private val renderContext: QuestRenderContext,
|
||||
private val entityAssetLoader: EntityAssetLoader,
|
||||
) : DisposableContainer() {
|
||||
private val scope: CoroutineScope = CoroutineScope(Dispatchers.Main)
|
||||
private val scope = addDisposable(DisposableSupervisedScope(this::class, Dispatchers.Main))
|
||||
|
||||
/**
|
||||
* Contains one [EntityInstancedMesh] per [EntityType] and model.
|
||||
@ -50,7 +51,7 @@ class EntityMeshManager(
|
||||
/**
|
||||
* Bounding box around the highlighted entity.
|
||||
*/
|
||||
private val highlightedBox = BoxHelper(color = Color(0.7, 0.7, 0.7)).apply {
|
||||
private val highlightedBox = BoxHelper(color = Color(.7, .7, .7)).apply {
|
||||
visible = false
|
||||
renderContext.scene.add(this)
|
||||
}
|
||||
@ -58,7 +59,7 @@ class EntityMeshManager(
|
||||
/**
|
||||
* Bounding box around the selected entity.
|
||||
*/
|
||||
private val selectedBox = BoxHelper(color = Color(0.9, 0.9, 0.9)).apply {
|
||||
private val selectedBox = BoxHelper(color = Color(.9, .9, .9)).apply {
|
||||
visible = false
|
||||
renderContext.scene.add(this)
|
||||
}
|
||||
@ -113,7 +114,7 @@ class EntityMeshManager(
|
||||
}
|
||||
|
||||
fun remove(entity: QuestEntityModel<*, *>) {
|
||||
loadingEntities.remove(entity)?.cancel()
|
||||
loadingEntities.remove(entity)?.cancel("Removed.")
|
||||
|
||||
entityMeshCache.getIfPresentNow(
|
||||
TypeAndModel(
|
||||
@ -125,7 +126,7 @@ class EntityMeshManager(
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun removeAll() {
|
||||
loadingEntities.values.forEach { it.cancel() }
|
||||
loadingEntities.values.forEach { it.cancel("Removed.") }
|
||||
loadingEntities.clear()
|
||||
|
||||
for (meshContainerDeferred in entityMeshCache.values) {
|
||||
@ -144,7 +145,7 @@ class EntityMeshManager(
|
||||
attachBoxHelper(
|
||||
highlightedBox,
|
||||
highlightedEntityInstance,
|
||||
instance
|
||||
instance,
|
||||
)
|
||||
highlightedEntityInstance = instance
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
package world.phantasmal.web.questEditor.rendering
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import world.phantasmal.core.disposable.DisposableSupervisedScope
|
||||
import world.phantasmal.core.disposable.Disposer
|
||||
import world.phantasmal.lib.fileFormats.quest.Episode
|
||||
import world.phantasmal.observable.value.list.ListVal
|
||||
@ -25,7 +25,7 @@ abstract class QuestMeshManager protected constructor(
|
||||
questEditorStore: QuestEditorStore,
|
||||
renderContext: QuestRenderContext,
|
||||
) : DisposableContainer() {
|
||||
private val scope: CoroutineScope = CoroutineScope(Dispatchers.Default)
|
||||
private val scope = addDisposable(DisposableSupervisedScope(this::class, Dispatchers.Default))
|
||||
private val areaDisposer = addDisposable(Disposer())
|
||||
private val areaMeshManager = AreaMeshManager(renderContext, areaAssetLoader)
|
||||
private val entityMeshManager = addDisposable(
|
||||
|
@ -6,6 +6,8 @@ import world.phantasmal.web.questEditor.widgets.EntityDragEvent
|
||||
|
||||
sealed class Evt
|
||||
|
||||
class KeyboardEvt(val key: String) : Evt()
|
||||
|
||||
sealed class PointerEvt : Evt() {
|
||||
abstract val buttons: Int
|
||||
abstract val shiftKeyDown: Boolean
|
||||
@ -38,6 +40,13 @@ class PointerMoveEvt(
|
||||
override val movedSinceLastPointerDown: Boolean,
|
||||
) : PointerEvt()
|
||||
|
||||
class PointerOutEvt(
|
||||
override val buttons: Int,
|
||||
override val shiftKeyDown: Boolean,
|
||||
override val pointerDevicePosition: Vector2,
|
||||
override val movedSinceLastPointerDown: Boolean,
|
||||
) : PointerEvt()
|
||||
|
||||
sealed class EntityDragEvt(
|
||||
private val event: EntityDragEvent,
|
||||
/**
|
||||
|
@ -1,6 +1,8 @@
|
||||
package world.phantasmal.web.questEditor.rendering.input
|
||||
|
||||
import kotlinx.browser.window
|
||||
import org.w3c.dom.events.FocusEvent
|
||||
import org.w3c.dom.events.KeyboardEvent
|
||||
import org.w3c.dom.pointerevents.PointerEvent
|
||||
import world.phantasmal.core.disposable.Disposable
|
||||
import world.phantasmal.web.core.rendering.InputManager
|
||||
@ -17,7 +19,7 @@ import world.phantasmal.webui.DisposableContainer
|
||||
import world.phantasmal.webui.dom.disposableListener
|
||||
|
||||
class QuestInputManager(
|
||||
questEditorStore: QuestEditorStore,
|
||||
private val questEditorStore: QuestEditorStore,
|
||||
private val renderContext: QuestRenderContext,
|
||||
) : DisposableContainer(), InputManager {
|
||||
private val stateContext: StateContext
|
||||
@ -42,14 +44,18 @@ class QuestInputManager(
|
||||
}
|
||||
|
||||
init {
|
||||
addDisposables(
|
||||
renderContext.canvas.disposableListener("pointerdown", ::onPointerDown)
|
||||
)
|
||||
|
||||
onPointerMoveListener =
|
||||
renderContext.canvas.disposableListener("pointermove", ::onPointerMove)
|
||||
|
||||
addDisposables(
|
||||
renderContext.canvas.disposableListener<FocusEvent>(
|
||||
"focus",
|
||||
{ onFocus() },
|
||||
useCapture = true,
|
||||
),
|
||||
renderContext.canvas.disposableListener("pointerdown", ::onPointerDown),
|
||||
renderContext.canvas.disposableListener("pointerout", ::onPointerOut),
|
||||
renderContext.canvas.disposableListener("keydown", ::onKeyDown),
|
||||
renderContext.canvas.observeEntityDragEnter(::onEntityDragEnter),
|
||||
renderContext.canvas.observeEntityDragOver(::onEntityDragOver),
|
||||
renderContext.canvas.observeEntityDragLeave(::onEntityDragLeave),
|
||||
@ -91,6 +97,10 @@ class QuestInputManager(
|
||||
cameraInputManager.beforeRender()
|
||||
}
|
||||
|
||||
private fun onFocus() {
|
||||
questEditorStore.makeMainUndoCurrent()
|
||||
}
|
||||
|
||||
private fun onPointerDown(e: PointerEvent) {
|
||||
processPointerEvent(e)
|
||||
|
||||
@ -146,6 +156,23 @@ class QuestInputManager(
|
||||
)
|
||||
}
|
||||
|
||||
private fun onPointerOut(e: PointerEvent) {
|
||||
processPointerEvent(type = null, e.clientX, e.clientY)
|
||||
|
||||
state = state.processEvent(
|
||||
PointerOutEvt(
|
||||
e.buttons.toInt(),
|
||||
shiftKeyDown = e.shiftKey,
|
||||
pointerDevicePosition,
|
||||
movedSinceLastPointerDown,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun onKeyDown(e: KeyboardEvent) {
|
||||
state = state.processEvent(KeyboardEvt(e.key))
|
||||
}
|
||||
|
||||
private fun onEntityDragEnter(e: EntityDragEvent) {
|
||||
processPointerEvent(type = null, e.clientX, e.clientY)
|
||||
|
||||
|
@ -36,6 +36,7 @@ class CreationState(
|
||||
else -> error("Unsupported entity type ${event.entityType::class}.")
|
||||
}
|
||||
|
||||
private val dragAdjust = Vector3(.0, .0, .0)
|
||||
private val pointerDevicePosition = Vector2()
|
||||
private var shouldTranslate = false
|
||||
private var shouldTranslateVertically = false
|
||||
@ -83,14 +84,14 @@ class CreationState(
|
||||
if (shouldTranslateVertically) {
|
||||
ctx.translateEntityVertically(
|
||||
entity,
|
||||
ZERO_VECTOR,
|
||||
dragAdjust,
|
||||
ZERO_VECTOR,
|
||||
pointerDevicePosition,
|
||||
)
|
||||
} else {
|
||||
ctx.translateEntityHorizontally(
|
||||
entity,
|
||||
ZERO_VECTOR,
|
||||
dragAdjust,
|
||||
ZERO_VECTOR,
|
||||
pointerDevicePosition,
|
||||
)
|
||||
|
@ -19,6 +19,17 @@ class IdleState(
|
||||
|
||||
override fun processEvent(event: Evt): State {
|
||||
when (event) {
|
||||
is KeyboardEvt -> {
|
||||
if (entityManipulationEnabled) {
|
||||
val quest = ctx.quest.value
|
||||
val entity = ctx.selectedEntity.value
|
||||
|
||||
if (quest != null && entity != null && event.key == "Delete") {
|
||||
ctx.deleteEntity(quest, entity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is PointerDownEvt -> {
|
||||
val pick = pickEntity(event.pointerDevicePosition)
|
||||
|
||||
@ -85,6 +96,11 @@ class IdleState(
|
||||
}
|
||||
}
|
||||
|
||||
is PointerOutEvt -> {
|
||||
ctx.setHighlightedEntity(null)
|
||||
shouldCheckHighlight = false
|
||||
}
|
||||
|
||||
is EntityDragEnterEvt -> {
|
||||
val quest = ctx.quest.value
|
||||
val area = ctx.area.value
|
||||
|
@ -6,6 +6,7 @@ import world.phantasmal.web.core.plusAssign
|
||||
import world.phantasmal.web.core.rendering.OrbitalCameraInputManager
|
||||
import world.phantasmal.web.externals.three.*
|
||||
import world.phantasmal.web.questEditor.actions.CreateEntityAction
|
||||
import world.phantasmal.web.questEditor.actions.DeleteEntityAction
|
||||
import world.phantasmal.web.questEditor.actions.RotateEntityAction
|
||||
import world.phantasmal.web.questEditor.actions.TranslateEntityAction
|
||||
import world.phantasmal.web.questEditor.models.*
|
||||
@ -22,6 +23,7 @@ class StateContext(
|
||||
val quest: Val<QuestModel?> = questEditorStore.currentQuest
|
||||
val area: Val<AreaModel?> = questEditorStore.currentArea
|
||||
val wave: Val<WaveModel?> = questEditorStore.selectedWave
|
||||
val selectedEntity: Val<QuestEntityModel<*, *>?> = questEditorStore.selectedEntity
|
||||
|
||||
fun setHighlightedEntity(entity: QuestEntityModel<*, *>?) {
|
||||
questEditorStore.setHighlightedEntity(entity)
|
||||
@ -180,6 +182,14 @@ class StateContext(
|
||||
))
|
||||
}
|
||||
|
||||
fun deleteEntity(quest: QuestModel, entity: QuestEntityModel<*, *>) {
|
||||
questEditorStore.executeAction(DeleteEntityAction(
|
||||
::setSelectedEntity,
|
||||
quest,
|
||||
entity,
|
||||
))
|
||||
}
|
||||
|
||||
/**
|
||||
* @param origin position in normalized device space.
|
||||
*/
|
||||
|
@ -1,35 +1,103 @@
|
||||
package world.phantasmal.web.questEditor.stores
|
||||
|
||||
import world.phantasmal.lib.assembly.disassemble
|
||||
import world.phantasmal.observable.ChangeEvent
|
||||
import world.phantasmal.observable.Observable
|
||||
import world.phantasmal.observable.emitter
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.map
|
||||
import world.phantasmal.observable.value.mutableVal
|
||||
import world.phantasmal.observable.value.trueVal
|
||||
import world.phantasmal.web.core.undo.SimpleUndo
|
||||
import world.phantasmal.web.core.undo.UndoManager
|
||||
import world.phantasmal.web.externals.monacoEditor.*
|
||||
import world.phantasmal.web.questEditor.models.QuestModel
|
||||
import world.phantasmal.webui.obj
|
||||
import world.phantasmal.webui.stores.Store
|
||||
import kotlin.js.RegExp
|
||||
|
||||
class AssemblyEditorStore(questEditorStore: QuestEditorStore) : Store() {
|
||||
private var _textModel: ITextModel? = null
|
||||
class AsmStore(
|
||||
questEditorStore: QuestEditorStore,
|
||||
private val undoManager: UndoManager,
|
||||
) : Store() {
|
||||
private var _textModel = mutableVal<ITextModel?>(null)
|
||||
|
||||
private val _didUndo = emitter<Unit>()
|
||||
private val _didRedo = emitter<Unit>()
|
||||
private val undo = SimpleUndo(
|
||||
undoManager,
|
||||
"Script edits",
|
||||
{ _didUndo.emit(ChangeEvent(Unit)) },
|
||||
{ _didRedo.emit(ChangeEvent(Unit)) },
|
||||
)
|
||||
|
||||
val inlineStackArgs: Val<Boolean> = trueVal()
|
||||
|
||||
val textModel: Val<ITextModel?> =
|
||||
map(questEditorStore.currentQuest, inlineStackArgs) { quest, inlineArgs ->
|
||||
_textModel?.dispose()
|
||||
|
||||
_textModel =
|
||||
if (quest == null) null
|
||||
else {
|
||||
val assembly = disassemble(quest.bytecodeIr, inlineArgs)
|
||||
createModel(assembly.joinToString("\n"), ASM_LANG_ID)
|
||||
}
|
||||
|
||||
_textModel
|
||||
}
|
||||
val textModel: Val<ITextModel?> = _textModel
|
||||
|
||||
val editingEnabled: Val<Boolean> = questEditorStore.questEditingEnabled
|
||||
|
||||
val didUndo: Observable<Unit> = _didUndo
|
||||
val didRedo: Observable<Unit> = _didRedo
|
||||
|
||||
init {
|
||||
observe(questEditorStore.currentQuest, inlineStackArgs) { quest, inlineArgs ->
|
||||
_textModel.value?.dispose()
|
||||
_textModel.value = quest?.let { createModel(quest, inlineArgs) }
|
||||
}
|
||||
}
|
||||
|
||||
fun makeUndoCurrent() {
|
||||
undoManager.setCurrent(undo)
|
||||
}
|
||||
|
||||
private fun createModel(quest: QuestModel, inlineArgs: Boolean): ITextModel {
|
||||
val assembly = disassemble(quest.bytecodeIr, inlineArgs)
|
||||
val model = createModel(assembly.joinToString("\n"), ASM_LANG_ID)
|
||||
addModelChangeListener(model)
|
||||
return model
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up undo/redo, code analysis and breakpoint updates on model change.
|
||||
*/
|
||||
private fun addModelChangeListener(model: ITextModel) {
|
||||
val initialVersion = model.getAlternativeVersionId()
|
||||
var currentVersion = initialVersion
|
||||
var lastVersion = initialVersion
|
||||
|
||||
model.onDidChangeContent {
|
||||
val version = model.getAlternativeVersionId()
|
||||
|
||||
if (version < currentVersion) {
|
||||
// Undoing.
|
||||
undo.canRedo.value = true
|
||||
|
||||
if (version == initialVersion) {
|
||||
undo.canUndo.value = false
|
||||
}
|
||||
} else {
|
||||
// Redoing.
|
||||
if (version <= lastVersion) {
|
||||
if (version == lastVersion) {
|
||||
undo.canRedo.value = false
|
||||
}
|
||||
} else {
|
||||
undo.canRedo.value = false
|
||||
|
||||
if (currentVersion > lastVersion) {
|
||||
lastVersion = currentVersion
|
||||
}
|
||||
}
|
||||
|
||||
undo.canUndo.value = true
|
||||
}
|
||||
|
||||
currentVersion = version
|
||||
|
||||
// TODO: Code analysis and breakpoint update.
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ASM_LANG_ID = "psoasm"
|
||||
|
@ -18,16 +18,15 @@ import world.phantasmal.webui.stores.Store
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
class QuestEditorStore(
|
||||
private val uiStore: UiStore,
|
||||
uiStore: UiStore,
|
||||
private val areaStore: AreaStore,
|
||||
private val undoManager: UndoManager,
|
||||
) : Store() {
|
||||
private val _currentQuest = mutableVal<QuestModel?>(null)
|
||||
private val _currentArea = mutableVal<AreaModel?>(null)
|
||||
private val _selectedWave = mutableVal<WaveModel?>(null)
|
||||
private val _highlightedEntity = mutableVal<QuestEntityModel<*, *>?>(null)
|
||||
private val _selectedEntity = mutableVal<QuestEntityModel<*, *>?>(null)
|
||||
|
||||
private val undoManager = UndoManager()
|
||||
private val mainUndo = UndoStack(undoManager)
|
||||
|
||||
val runner = QuestRunner()
|
||||
@ -76,21 +75,19 @@ class QuestEditorStore(
|
||||
}
|
||||
|
||||
fun makeMainUndoCurrent() {
|
||||
mainUndo.makeCurrent()
|
||||
undoManager.setCurrent(mainUndo)
|
||||
}
|
||||
|
||||
fun undo() {
|
||||
require(canUndo.value) { "Can't undo at the moment." }
|
||||
undoManager.undo()
|
||||
}
|
||||
|
||||
fun redo() {
|
||||
require(canRedo.value) { "Can't redo at the moment." }
|
||||
undoManager.redo()
|
||||
}
|
||||
|
||||
suspend fun setCurrentQuest(quest: QuestModel?) {
|
||||
mainUndo.reset()
|
||||
undoManager.reset()
|
||||
|
||||
// TODO: Stop runner.
|
||||
|
||||
|
@ -2,20 +2,19 @@ package world.phantasmal.web.questEditor.widgets
|
||||
|
||||
import org.w3c.dom.Node
|
||||
import world.phantasmal.core.disposable.disposable
|
||||
import world.phantasmal.web.externals.monacoEditor.IStandaloneCodeEditor
|
||||
import world.phantasmal.web.externals.monacoEditor.create
|
||||
import world.phantasmal.web.externals.monacoEditor.defineTheme
|
||||
import world.phantasmal.web.externals.monacoEditor.set
|
||||
import world.phantasmal.web.questEditor.controllers.AssemblyEditorController
|
||||
import world.phantasmal.web.externals.monacoEditor.*
|
||||
import world.phantasmal.web.questEditor.controllers.AsmController
|
||||
import world.phantasmal.webui.dom.div
|
||||
import world.phantasmal.webui.obj
|
||||
import world.phantasmal.webui.widgets.Widget
|
||||
|
||||
class AssemblyEditorWidget(private val ctrl: AssemblyEditorController) : Widget() {
|
||||
class AsmEditorWidget(private val ctrl: AsmController) : Widget() {
|
||||
private lateinit var editor: IStandaloneCodeEditor
|
||||
|
||||
override fun Node.createElement() =
|
||||
div {
|
||||
className = "pw-quest-editor-asm-editor"
|
||||
|
||||
editor = create(this, obj {
|
||||
theme = "phantasmal-world"
|
||||
scrollBeyondLastLine = false
|
||||
@ -34,13 +33,53 @@ class AssemblyEditorWidget(private val ctrl: AssemblyEditorController) : Widget(
|
||||
observe(ctrl.readOnly) { editor.updateOptions(obj { readOnly = it }) }
|
||||
|
||||
addDisposable(size.observe { (size) ->
|
||||
editor.layout(obj {
|
||||
width = size.width
|
||||
height = size.height
|
||||
})
|
||||
if (size.width > .0 && size.height > .0) {
|
||||
editor.layout(obj {
|
||||
width = size.width
|
||||
height = size.height
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Add VSCode keybinding for command palette.
|
||||
val quickCommand = editor.getAction("editor.action.quickCommand")
|
||||
|
||||
editor.addAction(object : IActionDescriptor {
|
||||
override var id = "editor.action.quickCommand"
|
||||
override var label = "Command Palette"
|
||||
override var keybindings =
|
||||
arrayOf(KeyMod.CtrlCmd or KeyMod.Shift or KeyCode.KEY_P)
|
||||
|
||||
override fun run(editor: ICodeEditor, vararg args: dynamic) {
|
||||
quickCommand.run()
|
||||
}
|
||||
})
|
||||
|
||||
// Undo/redo.
|
||||
observe(ctrl.didUndo) {
|
||||
editor.focus()
|
||||
editor.trigger(
|
||||
source = AsmEditorWidget::class.simpleName,
|
||||
handlerId = "undo",
|
||||
payload = undefined
|
||||
)
|
||||
}
|
||||
|
||||
observe(ctrl.didRedo) {
|
||||
editor.trigger(
|
||||
source = AsmEditorWidget::class.simpleName,
|
||||
handlerId = "redo",
|
||||
payload = undefined
|
||||
)
|
||||
}
|
||||
|
||||
editor.onDidFocusEditorWidget(ctrl::makeUndoCurrent)
|
||||
}
|
||||
|
||||
override fun focus() {
|
||||
editor.focus()
|
||||
}
|
||||
|
||||
companion object {
|
||||
init {
|
||||
defineTheme("phantasmal-world", obj {
|
||||
@ -61,6 +100,14 @@ class AssemblyEditorWidget(private val ctrl: AssemblyEditorController) : Widget(
|
||||
this["editor.lineHighlightBackground"] = "#202020"
|
||||
}
|
||||
})
|
||||
|
||||
@Suppress("CssUnusedSymbol")
|
||||
// language=css
|
||||
style("""
|
||||
.pw-quest-editor-asm-editor {
|
||||
flex-grow: 1;
|
||||
}
|
||||
""".trimIndent())
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package world.phantasmal.web.questEditor.widgets
|
||||
|
||||
import org.w3c.dom.Node
|
||||
import world.phantasmal.web.questEditor.controllers.AsmController
|
||||
import world.phantasmal.webui.dom.div
|
||||
import world.phantasmal.webui.widgets.Checkbox
|
||||
import world.phantasmal.webui.widgets.Toolbar
|
||||
import world.phantasmal.webui.widgets.Widget
|
||||
|
||||
class AsmToolbarWidget(private val ctrl: AsmController) : Widget() {
|
||||
override fun Node.createElement() =
|
||||
div {
|
||||
className = "pw-quest-editor-asm-toolbar"
|
||||
|
||||
addChild(Toolbar(
|
||||
enabled = ctrl.enabled,
|
||||
children = listOf(
|
||||
Checkbox(
|
||||
enabled = ctrl.inlineStackArgsEnabled,
|
||||
tooltip = ctrl.inlineStackArgsTooltip,
|
||||
label = "Inline args",
|
||||
checked = ctrl.inlineStackArgs,
|
||||
onChange = ctrl::setInlineStackArgs,
|
||||
)
|
||||
)
|
||||
))
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package world.phantasmal.web.questEditor.widgets
|
||||
|
||||
import org.w3c.dom.Node
|
||||
import world.phantasmal.web.questEditor.controllers.AsmController
|
||||
import world.phantasmal.webui.dom.div
|
||||
import world.phantasmal.webui.widgets.Widget
|
||||
|
||||
class AsmWidget(private val ctrl: AsmController) : Widget() {
|
||||
private lateinit var editorWidget: AsmEditorWidget
|
||||
|
||||
override fun Node.createElement() =
|
||||
div {
|
||||
className = "pw-quest-editor-asm"
|
||||
|
||||
addChild(AsmToolbarWidget(ctrl))
|
||||
editorWidget = addChild(AsmEditorWidget(ctrl))
|
||||
}
|
||||
|
||||
override fun focus() {
|
||||
editorWidget.focus()
|
||||
}
|
||||
|
||||
companion object {
|
||||
init {
|
||||
@Suppress("CssUnusedSymbol")
|
||||
// language=css
|
||||
style("""
|
||||
.pw-quest-editor-asm {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
""".trimIndent())
|
||||
}
|
||||
}
|
||||
}
|
@ -13,6 +13,8 @@ class EntityInfoWidget(private val ctrl: EntityInfoController) : Widget(enabled
|
||||
className = "pw-quest-editor-entity-info"
|
||||
tabIndex = -1
|
||||
|
||||
addEventListener("focus", { ctrl.focused() }, true)
|
||||
|
||||
table {
|
||||
hidden(ctrl.unavailable)
|
||||
|
||||
@ -113,11 +115,6 @@ class EntityInfoWidget(private val ctrl: EntityInfoController) : Widget(enabled
|
||||
))
|
||||
}
|
||||
|
||||
override fun focus() {
|
||||
super.focus()
|
||||
ctrl.focused()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val COORD_CLASS = "pw-quest-editor-entity-info-coord"
|
||||
|
||||
|
@ -17,7 +17,6 @@ class EntityListWidget(
|
||||
override fun Node.createElement() =
|
||||
div {
|
||||
className = "pw-quest-editor-entity-list"
|
||||
tabIndex = -1
|
||||
|
||||
div {
|
||||
className = "pw-quest-editor-entity-list-inner"
|
||||
|
@ -12,6 +12,9 @@ class NpcCountsWidget(
|
||||
override fun Node.createElement() =
|
||||
div {
|
||||
className = "pw-quest-editor-npc-counts"
|
||||
tabIndex = -1
|
||||
|
||||
addEventListener("focus", { ctrl.focused() }, true)
|
||||
|
||||
table {
|
||||
hidden(ctrl.unavailable)
|
||||
|
@ -25,7 +25,7 @@ class QuestEditorToolbarWidget(private val ctrl: QuestEditorToolbarController) :
|
||||
text = "Open file...",
|
||||
tooltip = value("Open a quest file (Ctrl-O)"),
|
||||
iconLeft = Icon.File,
|
||||
accept = ".bin, .dat, .qst",
|
||||
accept = ctrl.openFileAccept,
|
||||
multiple = true,
|
||||
filesSelected = { files -> scope.launch { ctrl.openFiles(files) } },
|
||||
),
|
||||
@ -45,11 +45,11 @@ class QuestEditorToolbarWidget(private val ctrl: QuestEditorToolbarController) :
|
||||
),
|
||||
Select(
|
||||
enabled = ctrl.areaSelectEnabled,
|
||||
itemsVal = ctrl.areas,
|
||||
items = ctrl.areas,
|
||||
itemToString = { it.label },
|
||||
selectedVal = ctrl.currentArea,
|
||||
selected = ctrl.currentArea,
|
||||
onSelect = ctrl::setCurrentArea,
|
||||
)
|
||||
),
|
||||
)
|
||||
))
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ package world.phantasmal.web.questEditor.widgets
|
||||
import org.w3c.dom.Node
|
||||
import world.phantasmal.web.core.widgets.DockWidget
|
||||
import world.phantasmal.web.questEditor.controllers.QuestEditorController
|
||||
import world.phantasmal.web.questEditor.controllers.QuestEditorController.Companion.ASSEMBLY_EDITOR_WIDGET_ID
|
||||
import world.phantasmal.web.questEditor.controllers.QuestEditorController.Companion.ASM_WIDGET_ID
|
||||
import world.phantasmal.web.questEditor.controllers.QuestEditorController.Companion.ENTITY_INFO_WIDGET_ID
|
||||
import world.phantasmal.web.questEditor.controllers.QuestEditorController.Companion.EVENTS_WIDGET_ID
|
||||
import world.phantasmal.web.questEditor.controllers.QuestEditorController.Companion.NPC_COUNTS_WIDGET_ID
|
||||
@ -24,7 +24,7 @@ class QuestEditorWidget(
|
||||
private val createNpcCountsWidget: () -> NpcCountsWidget,
|
||||
private val createEntityInfoWidget: () -> EntityInfoWidget,
|
||||
private val createQuestRendererWidget: () -> QuestRendererWidget,
|
||||
private val createAssemblyEditorWidget: () -> AssemblyEditorWidget,
|
||||
private val createAsmWidget: () -> AsmWidget,
|
||||
private val createNpcListWidget: () -> EntityListWidget,
|
||||
private val createObjectListWidget: () -> EntityListWidget,
|
||||
) : Widget() {
|
||||
@ -41,7 +41,7 @@ class QuestEditorWidget(
|
||||
NPC_COUNTS_WIDGET_ID -> createNpcCountsWidget()
|
||||
ENTITY_INFO_WIDGET_ID -> createEntityInfoWidget()
|
||||
QUEST_RENDERER_WIDGET_ID -> createQuestRendererWidget()
|
||||
ASSEMBLY_EDITOR_WIDGET_ID -> createAssemblyEditorWidget()
|
||||
ASM_WIDGET_ID -> createAsmWidget()
|
||||
NPC_LIST_WIDGET_ID -> createNpcListWidget()
|
||||
OBJECT_LIST_WIDGET_ID -> createObjectListWidget()
|
||||
EVENTS_WIDGET_ID -> null // TODO: EventsWidget.
|
||||
|
@ -15,6 +15,8 @@ class QuestInfoWidget(private val ctrl: QuestInfoController) : Widget(enabled =
|
||||
className = "pw-quest-editor-quest-info"
|
||||
tabIndex = -1
|
||||
|
||||
addEventListener("focus", { ctrl.focused() }, true)
|
||||
|
||||
table {
|
||||
hidden(ctrl.unavailable)
|
||||
|
||||
@ -92,11 +94,6 @@ class QuestInfoWidget(private val ctrl: QuestInfoController) : Widget(enabled =
|
||||
))
|
||||
}
|
||||
|
||||
override fun focus() {
|
||||
super.focus()
|
||||
ctrl.focused()
|
||||
}
|
||||
|
||||
companion object {
|
||||
init {
|
||||
@Suppress("CssUnusedSymbol")
|
||||
|
@ -8,12 +8,9 @@ kotlin {
|
||||
}
|
||||
}
|
||||
|
||||
val coroutinesVersion: String by project.ext
|
||||
|
||||
dependencies {
|
||||
api(project(":core"))
|
||||
api(project(":observable"))
|
||||
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
|
||||
implementation(npm("@fortawesome/fontawesome-free", "^5.13.1"))
|
||||
|
||||
testImplementation(kotlin("test-js"))
|
||||
|
@ -9,19 +9,21 @@ import org.w3c.dom.asList
|
||||
import org.w3c.files.File
|
||||
import org.w3c.files.FileReader
|
||||
|
||||
fun openFiles(accept: String = "", multiple: Boolean = false, callback: (List<File>) -> Unit) {
|
||||
val el = document.createElement("input") as HTMLInputElement
|
||||
el.type = "file"
|
||||
el.accept = accept
|
||||
el.multiple = multiple
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
suspend fun selectFiles(accept: String = "", multiple: Boolean = false): List<File> =
|
||||
suspendCancellableCoroutine { cont ->
|
||||
val el = document.createElement("input") as HTMLInputElement
|
||||
el.type = "file"
|
||||
el.accept = accept
|
||||
el.multiple = multiple
|
||||
|
||||
el.onchange = {
|
||||
callback(el.files?.asList() ?: emptyList())
|
||||
el.onchange = {
|
||||
cont.resume(el.files?.asList() ?: emptyList()) {}
|
||||
}
|
||||
|
||||
el.click()
|
||||
}
|
||||
|
||||
el.click()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
suspend fun readFile(file: File): ArrayBuffer = suspendCancellableCoroutine { cont ->
|
||||
val reader = FileReader()
|
||||
|
@ -23,6 +23,19 @@ fun <E : Event> EventTarget.disposableListener(
|
||||
}
|
||||
}
|
||||
|
||||
fun <E : Event> EventTarget.disposableListener(
|
||||
type: String,
|
||||
listener: (E) -> Unit,
|
||||
useCapture: Boolean,
|
||||
): Disposable {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
addEventListener(type, listener as (Event) -> Unit, useCapture)
|
||||
|
||||
return disposable {
|
||||
removeEventListener(type, listener)
|
||||
}
|
||||
}
|
||||
|
||||
fun Element.disposablePointerDrag(
|
||||
onPointerDown: (e: PointerEvent) -> Boolean,
|
||||
onPointerMove: (movedX: Int, movedY: Int, e: PointerEvent) -> Boolean,
|
||||
|
@ -1,15 +1,9 @@
|
||||
package world.phantasmal.webui.stores
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
import world.phantasmal.core.disposable.DisposableSupervisedScope
|
||||
import world.phantasmal.webui.DisposableContainer
|
||||
|
||||
abstract class Store : DisposableContainer() {
|
||||
protected val scope: CoroutineScope = CoroutineScope(Dispatchers.Default)
|
||||
|
||||
override fun internalDispose() {
|
||||
scope.cancel("Store disposed.")
|
||||
super.internalDispose()
|
||||
}
|
||||
protected val scope = addDisposable(DisposableSupervisedScope(this::class, Dispatchers.Default))
|
||||
}
|
||||
|
@ -0,0 +1,34 @@
|
||||
package world.phantasmal.webui.widgets
|
||||
|
||||
import org.w3c.dom.Node
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.nullVal
|
||||
import world.phantasmal.observable.value.trueVal
|
||||
import world.phantasmal.webui.dom.input
|
||||
|
||||
class Checkbox(
|
||||
visible: Val<Boolean> = trueVal(),
|
||||
enabled: Val<Boolean> = trueVal(),
|
||||
tooltip: Val<String?> = nullVal(),
|
||||
label: String? = null,
|
||||
labelVal: Val<String>? = null,
|
||||
preferredLabelPosition: LabelPosition = LabelPosition.After,
|
||||
private val checked: Val<Boolean>? = null,
|
||||
private val onChange: ((Boolean) -> Unit)? = null,
|
||||
) : LabelledControl(visible, enabled, tooltip, label, labelVal, preferredLabelPosition) {
|
||||
override fun Node.createElement() =
|
||||
input {
|
||||
className = "pw-checkbox"
|
||||
type = "checkbox"
|
||||
|
||||
if (this@Checkbox.checked != null) {
|
||||
observe(this@Checkbox.checked) {
|
||||
checked = it
|
||||
}
|
||||
}
|
||||
|
||||
if (onChange != null) {
|
||||
onchange = { onChange.invoke(checked) }
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +1,13 @@
|
||||
package world.phantasmal.webui.widgets
|
||||
|
||||
import kotlinx.coroutines.launch
|
||||
import org.w3c.dom.HTMLElement
|
||||
import org.w3c.files.File
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.nullVal
|
||||
import world.phantasmal.observable.value.trueVal
|
||||
import world.phantasmal.webui.dom.Icon
|
||||
import world.phantasmal.webui.openFiles
|
||||
import world.phantasmal.webui.selectFiles
|
||||
|
||||
class FileButton(
|
||||
visible: Val<Boolean> = trueVal(),
|
||||
@ -25,7 +26,9 @@ class FileButton(
|
||||
|
||||
if (filesSelected != null) {
|
||||
element.onclick = {
|
||||
openFiles(accept, multiple, filesSelected)
|
||||
scope.launch {
|
||||
filesSelected.invoke(selectFiles(accept, multiple))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import org.w3c.dom.Node
|
||||
import org.w3c.dom.events.KeyboardEvent
|
||||
import org.w3c.dom.events.MouseEvent
|
||||
import world.phantasmal.observable.value.*
|
||||
import world.phantasmal.observable.value.list.emptyListVal
|
||||
import world.phantasmal.webui.dom.Icon
|
||||
import world.phantasmal.webui.dom.div
|
||||
|
||||
@ -14,11 +15,9 @@ class Select<T : Any>(
|
||||
label: String? = null,
|
||||
labelVal: Val<String>? = null,
|
||||
preferredLabelPosition: LabelPosition = LabelPosition.Before,
|
||||
items: List<T>? = null,
|
||||
itemsVal: Val<List<T>>? = null,
|
||||
items: Val<List<T>>? = null,
|
||||
private val itemToString: (T) -> String = Any::toString,
|
||||
selected: T? = null,
|
||||
selectedVal: Val<T?>? = null,
|
||||
selected: Val<T?>? = null,
|
||||
private val onSelect: (T) -> Unit = {},
|
||||
) : LabelledControl(
|
||||
visible,
|
||||
@ -28,8 +27,8 @@ class Select<T : Any>(
|
||||
labelVal,
|
||||
preferredLabelPosition,
|
||||
) {
|
||||
private val items: Val<List<T>> = itemsVal ?: value(items ?: emptyList())
|
||||
private val selected: Val<T?> = selectedVal ?: value(selected)
|
||||
private val items: Val<List<T>> = items ?: emptyListVal()
|
||||
private val selected: Val<T?> = selected ?: nullVal()
|
||||
|
||||
private val buttonText = mutableVal(" ")
|
||||
private val menuVisible = mutableVal(false)
|
||||
|
@ -5,6 +5,9 @@ import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.trueVal
|
||||
import world.phantasmal.webui.dom.div
|
||||
|
||||
/**
|
||||
* Takes ownership of the given [children].
|
||||
*/
|
||||
class Toolbar(
|
||||
visible: Val<Boolean> = trueVal(),
|
||||
enabled: Val<Boolean> = trueVal(),
|
||||
|
@ -1,11 +1,11 @@
|
||||
package world.phantasmal.webui.widgets
|
||||
|
||||
import kotlinx.browser.document
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
import org.w3c.dom.*
|
||||
import org.w3c.dom.pointerevents.PointerEvent
|
||||
import world.phantasmal.core.disposable.DisposableSupervisedScope
|
||||
import world.phantasmal.core.disposable.Disposer
|
||||
import world.phantasmal.observable.Observable
|
||||
import world.phantasmal.observable.value.*
|
||||
@ -65,7 +65,7 @@ abstract class Widget(
|
||||
el
|
||||
}
|
||||
|
||||
protected val scope: CoroutineScope = MainScope()
|
||||
protected val scope = addDisposable(DisposableSupervisedScope(this::class, Dispatchers.Main))
|
||||
|
||||
/**
|
||||
* This widget's outermost DOM element.
|
||||
|
Loading…
Reference in New Issue
Block a user