diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 739de1f0..5dfbba5e 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -39,6 +39,9 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4 + with: + submodules: true + fetch-depth: 0 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.gitmodules b/.gitmodules index 37e5cd06..a8ec31b5 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "ferricia"] path = ferricia url = https://github.com/bitsusei/TerraModulus-Ferricia-Engine +[submodule "vector-math"] + path = vector-math + url = https://github.com/bitsusei/kotlin-vector-math diff --git a/.idea/compiler.xml b/.idea/compiler.xml index e254cd93..e3cab1e4 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -1,16 +1,16 @@ - - - - - - - - - - + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 00000000..e40fc1cc --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index 131e44d7..2e4da65a 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,7 @@ - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index ba62ca49..00000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml index ba9deec3..6e08a227 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -5,5 +5,6 @@ + \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 8541b9db..5a90eaf0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,21 +5,64 @@ plugins { kotlin("jvm") version "2.3.21" kotlin("plugin.serialization") version "2.1.20" id("org.jetbrains.kotlinx.atomicfu") version "0.27.0" + id("net.terramodulus.plugins.cargo") apply false +// id("fr.stardustenterprises.rust.wrapper") version "3.2.4" apply false application + kotlin("kapt") version "2.3.21" } -allprojects { - apply(plugin = "org.jetbrains.kotlin.jvm") +version = "0.0.1" - version = "0.0.1" +repositories { + mavenCentral() +} + +project(":ferricia") { + // Candidates: fr.stardustenterprises.rust.wrapper + apply(plugin = "net.terramodulus.plugins.cargo") - repositories { - mavenCentral() + if (providers.gradleProperty("release").isPresent) configure { + release = true // use `-Prelease=true` + } + configure { + outputFile = release.map { + projectDir.resolve("target/${if (it) "release" else "debug"}/${System.mapLibraryName("ferricia")}") + } + } + // somehow, .cargo extension is unusable + tasks.register("buildClient") { + args = listOf("-F", "client") + println(outputFile.get()) + } + tasks.register("buildServer") { + args = listOf("-F", "server") + } + configurations { + create("client") { + isCanBeConsumed = true + isCanBeResolved = false + } + create("server") { + isCanBeConsumed = true + isCanBeResolved = false + } + } + artifacts { + add("client", tasks.named("buildClient")) + add("server", tasks.named("buildServer")) } } configure(listOf(project(":kernel"), project(":internal"))) { configure(listOf(project("common"), project("client"), project("server"))) { + apply(plugin = "org.jetbrains.kotlin.jvm") + + version = rootProject.version + + repositories { + mavenCentral() + } + sourceSets.main { kotlin.srcDir("kotlin") resources.srcDir("resources") @@ -61,7 +104,26 @@ project(":kernel") { } } +project(":kernel:client") { + dependencies { + implementation(project(":ferricia", "client")) + } +} +project(":kernel:server") { + dependencies { + implementation(project(":ferricia", "server")) + } +} + +configure(listOf(project(":internal:common"), project(":kernel:common"))) { + dependencies { + api("com.cout970:kotlin-vector-math:0.1.0") + } +} + project(":kernel:common") { + apply(plugin = "org.jetbrains.kotlin.kapt") + dependencies { api("org.jetbrains:annotations:26.1.0") api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1") @@ -77,7 +139,7 @@ project(":kernel:common") { implementation("org.apache.logging.log4j:log4j-api:2.24.3") implementation("org.apache.logging.log4j:log4j-slf4j2-impl:2.24.3") implementation(platform("org.apache.logging.log4j:log4j-bom:2.24.3")) - annotationProcessor("org.apache.logging.log4j:log4j-core:2.24.3") + kapt("org.apache.logging.log4j:log4j-core:2.24.3") runtimeOnly("com.lmax:disruptor:4.0.0") api("io.github.oshai:kotlin-logging-jvm:7.0.3") implementation("net.sf.jopt-simple:jopt-simple:5.0.4") @@ -86,10 +148,12 @@ project(":kernel:common") { project(":kernel:client").dependencies { implementation("net.sf.jopt-simple:jopt-simple:5.0.4") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0") } project(":kernel:server").dependencies { implementation("net.sf.jopt-simple:jopt-simple:5.0.4") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0") } project(":internal:common").dependencies { @@ -112,28 +176,6 @@ configure(listOf(project(":kernel:server"), project(":kernel:client"))) { } } -/** Build Ferricia Engine with Cargo */ -tasks.register("cargoBuildClient") { - onlyIf { - !gradle.taskGraph.hasTask(":kernel:server:jar") - } - workingDir = rootProject.file("ferricia") - commandLine("cargo", "build") - if (project.hasProperty("release")) args("--release") // use `-Prelease=true` - args("-F", "client") -} -tasks.register("cargoBuildServer") { - onlyIf { - !gradle.taskGraph.hasTask(":kernel:client:jar") - } - workingDir = rootProject.file("ferricia") - commandLine("cargo", "build") - if (project.hasProperty("release")) args("--release") // use `-Prelease=true` - args("-F", "server") -} -project(":kernel:client").tasks.named("jar") { dependsOn(tasks.named("cargoBuildClient")) } -project(":kernel:server").tasks.named("jar") { dependsOn(tasks.named("cargoBuildServer")) } - tasks.register("buildClient") { group = "build" description = "Build client" @@ -151,17 +193,13 @@ tasks.named("run") { tasks.register("runClient") { group = "application" description = "Run client" - dependsOn("cargoBuildClient") dependsOn(":kernel:client:run") } -project(":kernel:client").tasks.named("run").get().mustRunAfter(tasks.named("cargoBuildClient")) tasks.register("runServer") { group = "application" description = "Run server" - dependsOn("cargoBuildServer") dependsOn(":kernel:server:run") } -project(":kernel:server").tasks.named("run").get().mustRunAfter(tasks.named("cargoBuildServer")) configure(listOf(project(":kernel:server"), project(":kernel:client"))) { distributions { @@ -170,15 +208,12 @@ configure(listOf(project(":kernel:server"), project(":kernel:client"))) { duplicatesStrategy = DuplicatesStrategy.EXCLUDE into("lib") { val dir = if (project.hasProperty("release")) "release" else "debug" + from("$rootDir/ferricia/target/$dir/${System.mapLibraryName("ferricia")}") if (OperatingSystem.current().isWindows) from( - "$rootDir/ferricia/target/$dir/ferricia.dll", "$rootDir/ferricia/target/$dir/oded.dll", "$rootDir/ferricia/target/$dir/OpenAL32.dll", "$rootDir/ferricia/target/$dir/SDL3.dll", - ) else { // suppose UNIX - // other libs should be installed on user's end directly - from("$rootDir/ferricia/target/$dir/libferricia.so") - } + ) // for UNIX, other libs should have been installed on user's end directly } } } @@ -197,9 +232,18 @@ configure(listOf(project(":kernel:server"), project(":kernel:client"))) { } tasks.named("run") { - jvmArgs("-Djava.library.path=${rootProject.file("ferricia/target/${ - if (project.hasProperty("release")) "release" else "debug" - }").path}") + jvmArgs( + "-Djava.library.path=${ + rootProject.file( + "ferricia/target/${ + if (project.hasProperty("release")) "release" else "debug" + }" + ).path + }" + ) args("--screen-size", "800x500") } } +dependencies { + testImplementation(kotlin("test")) +} diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 00000000..fb5da883 --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +plugins { + `kotlin-dsl` +} + +repositories { + mavenCentral() + gradlePluginPortal() +} + +gradlePlugin { + plugins { + register("cargoPlugin") { + id = "net.terramodulus.plugins.cargo" + implementationClass = "CargoPlugin" + } + } +} diff --git a/buildSrc/src/main/kotlin/CargoExtension.kt b/buildSrc/src/main/kotlin/CargoExtension.kt new file mode 100644 index 00000000..520ea2ae --- /dev/null +++ b/buildSrc/src/main/kotlin/CargoExtension.kt @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property + +interface CargoExtension { + val release: Property + val outputFile: RegularFileProperty +} diff --git a/buildSrc/src/main/kotlin/CargoPlugin.kt b/buildSrc/src/main/kotlin/CargoPlugin.kt new file mode 100644 index 00000000..843b7d61 --- /dev/null +++ b/buildSrc/src/main/kotlin/CargoPlugin.kt @@ -0,0 +1,21 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +import org.gradle.api.Plugin +import org.gradle.api.Project + +class CargoPlugin : Plugin { + override fun apply(target: Project) { + val extension = target.extensions.create("cargoConfig", CargoExtension::class.java) + + extension.release.convention(false) + + target.tasks.withType(CargoTask::class.java).configureEach { + release.convention(extension.release) + outputFile.convention(extension.outputFile) + inputs.dir(project.projectDir) + } + } +} diff --git a/buildSrc/src/main/kotlin/CargoTask.kt b/buildSrc/src/main/kotlin/CargoTask.kt new file mode 100644 index 00000000..1ee19e2a --- /dev/null +++ b/buildSrc/src/main/kotlin/CargoTask.kt @@ -0,0 +1,47 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +import org.gradle.api.DefaultTask +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import org.gradle.process.ExecOperations +import javax.inject.Inject + +abstract class CargoTask @Inject constructor(private val execOperations: ExecOperations, objectFactory: ObjectFactory) : DefaultTask() { +// @get:Input +// abstract val environment: Property> + + @get:Input + @get:Optional + abstract val release: Property + + @get:Input + @get:Optional + val args: ListProperty = objectFactory.listProperty(String::class.java) + + @get:OutputFile + abstract val outputFile: RegularFileProperty + + init { + release.convention(false) + args.convention(args.empty()) + } + + @TaskAction + fun build() { + execOperations.exec { + executable = "cargo" + args = listOf("build") + this@CargoTask.args.get() + if (release.get()) args = args + "--release" + workingDir = project.projectDir + } + } +} diff --git a/ferricia b/ferricia index 66bbf397..000d2e89 160000 --- a/ferricia +++ b/ferricia @@ -1 +1 @@ -Subproject commit 66bbf397fc38663b797a63f31493149602223434 +Subproject commit 000d2e89b38be73acd50ed7d3a20b679cff62afa diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index a4b76b95..1b33c55b 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index cea7a793..aaaabb3c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.4-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index f3b75f3b..23d15a93 100644 --- a/gradlew +++ b/gradlew @@ -114,7 +114,7 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar +CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. @@ -205,7 +205,7 @@ fi DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. @@ -213,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 9d21a218..db3a6ac2 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -70,11 +70,11 @@ goto fail :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar +set CLASSPATH= @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell diff --git a/settings.gradle.kts b/settings.gradle.kts index a487543e..e2221f71 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -16,3 +16,11 @@ rootProject.children.forEach { it.projectDir = File(settingsDir, "src/${it.name}") include("${it.name}:common", "${it.name}:client", "${it.name}:server") } + +include("ferricia") // this is Rust + +includeBuild("vector-math") { + dependencySubstitution { + substitute(module("com.cout970:kotlin-vector-math")).using(project(":")) + } +} diff --git a/src/internal/client/kotlin/net/terramodulus/engine/AsdIntData.kt b/src/internal/client/kotlin/net/terramodulus/engine/AsdIntData.kt new file mode 100644 index 00000000..401d7344 --- /dev/null +++ b/src/internal/client/kotlin/net/terramodulus/engine/AsdIntData.kt @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.engine + +/** + * ASD Intermediate Data + */ +class AsdIntData { +} diff --git a/src/internal/client/kotlin/net/terramodulus/engine/BaseAsdProcessor.kt b/src/internal/client/kotlin/net/terramodulus/engine/BaseAsdProcessor.kt new file mode 100644 index 00000000..a63aabda --- /dev/null +++ b/src/internal/client/kotlin/net/terramodulus/engine/BaseAsdProcessor.kt @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.engine + +import java.io.InputStream + +/** + * Since for most parts, there is no definite metadata of lengths for all data, + * it is necessary to provide at least processing fragmentation from implementation. + */ +interface BaseAsdProcessor { + /** + * @param data ByteArrayInputStream backed by direct byte buffer + */ + fun process(data: InputStream) +} diff --git a/src/internal/client/kotlin/net/terramodulus/engine/Camera3D.kt b/src/internal/client/kotlin/net/terramodulus/engine/Camera3D.kt index 343458f3..f5e500b4 100644 --- a/src/internal/client/kotlin/net/terramodulus/engine/Camera3D.kt +++ b/src/internal/client/kotlin/net/terramodulus/engine/Camera3D.kt @@ -5,8 +5,8 @@ package net.terramodulus.engine -import net.terramodulus.engine.ferricia.Gwr.drawGwrObj -import net.terramodulus.engine.ferricia.Gwr.geoShaders +import com.cout970.math.vec2.ImmVec2d +import net.terramodulus.engine.ferricia.Gwr.getCameraSpace import net.terramodulus.engine.ferricia.Gwr.newCamera import net.terramodulus.engine.ferricia.Gwr.refreshCameraPos import net.terramodulus.engine.ferricia.Gwr.setCameraZoomLevel @@ -16,7 +16,9 @@ import kotlin.properties.Delegates class Camera3D internal constructor(private val canvas: Canvas, pos: FloatArray) : Closeable { internal val handle = newCamera(canvas.handle, pos) - fun loadGeoShaders(vsh: String, fsh: String) = geoShaders(vsh, fsh) + fun loadGeoShaders(vsh: String, fsh: String) = canvas.load3DGeoShaders(vsh, fsh) + + fun getSpace() = getCameraSpace(handle).let { ImmVec2d(it[0], it[1]) } fun refreshPos(pos: FloatArray) = refreshCameraPos(handle, pos) @@ -25,7 +27,7 @@ class Camera3D internal constructor(private val canvas: Canvas, pos: FloatArray) } fun renderGwrGeo(drawable: WorldObjDrawable, programHandle: ULong) = - drawGwrObj(canvas.handle, handle, drawable.handle, programHandle) + canvas.drawGwrObj(this, drawable, programHandle) override fun close() { canvas.camera3D = null diff --git a/src/internal/client/kotlin/net/terramodulus/engine/Canvas.kt b/src/internal/client/kotlin/net/terramodulus/engine/Canvas.kt index a838815b..03562054 100644 --- a/src/internal/client/kotlin/net/terramodulus/engine/Canvas.kt +++ b/src/internal/client/kotlin/net/terramodulus/engine/Canvas.kt @@ -5,15 +5,25 @@ package net.terramodulus.engine +import net.terramodulus.engine.ferricia.Gwr +import net.terramodulus.engine.ferricia.Gwr.drawGwrObj +import net.terramodulus.engine.ferricia.Gwr.newMeshGeomCube +import net.terramodulus.engine.ferricia.Gwr.newMeshGeomSphere import net.terramodulus.engine.ferricia.Mui import net.terramodulus.engine.ferricia.Mui.clearCanvas +import net.terramodulus.engine.ferricia.Mui.disableScissor import net.terramodulus.engine.ferricia.Mui.drawGuiGeo import net.terramodulus.engine.ferricia.Mui.drawGuiTex import net.terramodulus.engine.ferricia.Mui.dropCanvasHandle +import net.terramodulus.engine.ferricia.Mui.enableScissor import net.terramodulus.engine.ferricia.Mui.geoShaders import net.terramodulus.engine.ferricia.Mui.getGLVersion import net.terramodulus.engine.ferricia.Mui.initCanvasHandle import net.terramodulus.engine.ferricia.Mui.loadImageToCanvas +import net.terramodulus.engine.ferricia.Mui.newSimpleLineGeom +import net.terramodulus.engine.ferricia.Mui.newSimpleRectGeom +import net.terramodulus.engine.ferricia.Mui.newSpriteMesh +import net.terramodulus.engine.ferricia.Mui.newTxtProgram import net.terramodulus.engine.ferricia.Mui.setCanvasClearColor import net.terramodulus.engine.ferricia.Mui.texShaders import java.io.Closeable @@ -23,16 +33,17 @@ import java.io.Closeable * * This manages GL viewport in the SDL window and rendering in the viewport. */ +@OptIn(ExperimentalUnsignedTypes::class) class Canvas internal constructor(private val windowHandle: ULong) : Closeable { internal val handle = initCanvasHandle(windowHandle) val glVersion = getGLVersion(windowHandle) internal var camera3D: Camera3D? = null; - fun clear() = clearCanvas() + fun clear() = clearCanvas(windowHandle) - fun setClearColor(r: Float, g: Float, b: Float, a: Float) = setCanvasClearColor(r, g, b, a) + fun setClearColor(r: Float, g: Float, b: Float, a: Float) = setCanvasClearColor(windowHandle, r, g, b, a) - fun resizeGLViewport() = if (camera3D == null) { + internal fun resizeGLViewport() = if (camera3D == null) { Mui.resizeGLViewport(windowHandle, handle) } else { Mui.resizeGLViewportCamera(windowHandle, handle, camera3D!!.handle) @@ -43,11 +54,33 @@ class Canvas internal constructor(private val windowHandle: ULong) : Closeable { return camera3D!! } + fun newGlyphManager(manager: FontManager) = manager.newGlyphManager(windowHandle) + fun loadImage(data: ByteArray) = loadImageToCanvas(handle, data) - fun loadGeoShaders(vsh: String, fsh: String) = geoShaders(vsh, fsh) + fun loadGeoShaders(vsh: String, fsh: String) = geoShaders(windowHandle, vsh, fsh) + + fun load3DGeoShaders(vsh: String, fsh: String) = Gwr.geoShaders(windowHandle, vsh, fsh) + + fun loadTexShaders(vsh: String, fsh: String) = texShaders(windowHandle, vsh, fsh) + + fun loadTxtShaders(vsh: String, fsh: String) = newTxtProgram(windowHandle, vsh, fsh) + + fun newTextRenderer(geoProgramHandle: ULong, txtProgramHandle: ULong) = + TextRenderer(windowHandle, geoProgramHandle, txtProgramHandle) + + internal fun newSimpleLineGeom(x0: Int, y0: Int, x1: Int, y1: Int, r: Int, g: Int, b: Int, a: Int) = + newSimpleLineGeom(windowHandle, intArrayOf(x0, y0, x1, y1, r, g, b, a)) - fun loadTexShaders(vsh: String, fsh: String) = texShaders(vsh, fsh) + internal fun newSimpleRectGeom(x0: Int, y0: Int, x1: Int, y1: Int, r: Int, g: Int, b: Int, a: Int) = + newSimpleRectGeom(windowHandle, intArrayOf(x0, y0, x1, y1, r, g, b, a)) + + internal fun newSpriteMesh(x0: Int, y0: Int, x1: Int, y1: Int) = + newSpriteMesh(windowHandle, intArrayOf(x0, y0, x1, y1)) + + internal fun newMeshGeomCube(width: Float) = newMeshGeomCube(windowHandle, width) + + internal fun newMeshGeomSphere(radius: Float) = newMeshGeomSphere(windowHandle, radius) fun renderGuiGeo(drawable: GeomDrawable, programHandle: ULong) = drawGuiGeo(handle, drawable.handle, programHandle) @@ -55,6 +88,13 @@ class Canvas internal constructor(private val windowHandle: ULong) : Closeable { fun renderGuiTex(drawable: MeshDrawable, programHandle: ULong, textureHandle: UInt) = drawGuiTex(handle, drawable.handle, programHandle, textureHandle) + internal fun drawGwrObj(camera3D: Camera3D, drawable: WorldObjDrawable, programHandle: ULong) = + drawGwrObj(windowHandle, handle, camera3D.handle, drawable.handle, programHandle) + + fun enableScissor(x: Int, y: Int, w: UInt, h: UInt) = enableScissor(handle, intArrayOf(x, y, w.toInt(), h.toInt())) + + fun disableScissor() = disableScissor(handle) + override fun close() { dropCanvasHandle(handle) } diff --git a/src/internal/client/kotlin/net/terramodulus/engine/Containers.kt b/src/internal/client/kotlin/net/terramodulus/engine/Containers.kt index 69913a49..c08aeab7 100644 --- a/src/internal/client/kotlin/net/terramodulus/engine/Containers.kt +++ b/src/internal/client/kotlin/net/terramodulus/engine/Containers.kt @@ -5,10 +5,9 @@ package net.terramodulus.engine -data class Rgba(val r: Int, val g: Int, val b: Int, val a: Int) { - fun toArray() = intArrayOf(r, g, b, a) -} +import com.cout970.math.vec2.Vec2f +import com.cout970.math.vec4.Vec4i -data class Vec3F(val x: Float, val y: Float, val z: Float) { - fun toArray() = floatArrayOf(x, y, z) -} +fun Vec4i.toArray() = intArrayOf(x, y, z, w) + +fun Vec2f.toArray() = floatArrayOf(x, y) diff --git a/src/internal/client/kotlin/net/terramodulus/engine/FontManager.kt b/src/internal/client/kotlin/net/terramodulus/engine/FontManager.kt new file mode 100644 index 00000000..a51a5d3d --- /dev/null +++ b/src/internal/client/kotlin/net/terramodulus/engine/FontManager.kt @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.engine + +import com.cout970.math.vec4.Vec4i +import net.terramodulus.engine.ferricia.Mui.newFontManager + +class FontManager { + internal val handle = newFontManager() + + internal fun newGlyphManager(windowHandle: ULong) = GlyphManager(handle, windowHandle) + + fun newTextRenderingManager(fontSize: Float, lineHeight: Float, color: Vec4i) = + TextRenderingContext(handle, fontSize, lineHeight, color) +} diff --git a/src/internal/client/kotlin/net/terramodulus/engine/GeomDrawable.kt b/src/internal/client/kotlin/net/terramodulus/engine/GeomDrawable.kt index 3d8ccd53..68e663bd 100644 --- a/src/internal/client/kotlin/net/terramodulus/engine/GeomDrawable.kt +++ b/src/internal/client/kotlin/net/terramodulus/engine/GeomDrawable.kt @@ -11,8 +11,8 @@ import net.terramodulus.engine.ferricia.Mui.newSimpleRectGeom sealed class GeomDrawable(handle: ULong) : Drawable(handle) { } -class SimpleLineGeom(x0: Int, y0: Int, x1: Int, y1: Int, r: Int, g: Int, b: Int, a: Int) : - GeomDrawable(newSimpleLineGeom(intArrayOf(x0, y0, x1, y1, r, g, b, a))) +class SimpleLineGeom(canvas: Canvas, x0: Int, y0: Int, x1: Int, y1: Int, r: Int, g: Int, b: Int, a: Int) : + GeomDrawable(canvas.newSimpleLineGeom(x0, y0, x1, y1, r, g, b, a)) -class SimpleRectGeom(x0: Int, y0: Int, x1: Int, y1: Int, r: Int, g: Int, b: Int, a: Int) : - GeomDrawable(newSimpleRectGeom(intArrayOf(x0, y0, x1, y1, r, g, b, a))) +class SimpleRectGeom(canvas: Canvas, x0: Int, y0: Int, x1: Int, y1: Int, r: Int, g: Int, b: Int, a: Int) : + GeomDrawable(canvas.newSimpleRectGeom(x0, y0, x1, y1, r, g, b, a)) diff --git a/src/internal/client/kotlin/net/terramodulus/engine/GlyphManager.kt b/src/internal/client/kotlin/net/terramodulus/engine/GlyphManager.kt new file mode 100644 index 00000000..76383f24 --- /dev/null +++ b/src/internal/client/kotlin/net/terramodulus/engine/GlyphManager.kt @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.engine + +import net.terramodulus.engine.ferricia.Mui.newGlyphManager + +class GlyphManager internal constructor(managerHandle: ULong, windowHandle: ULong) { + internal val handle = newGlyphManager(managerHandle, windowHandle) +} diff --git a/src/internal/client/kotlin/net/terramodulus/engine/MeshDrawable.kt b/src/internal/client/kotlin/net/terramodulus/engine/MeshDrawable.kt index b9357795..836ba323 100644 --- a/src/internal/client/kotlin/net/terramodulus/engine/MeshDrawable.kt +++ b/src/internal/client/kotlin/net/terramodulus/engine/MeshDrawable.kt @@ -10,6 +10,7 @@ import net.terramodulus.engine.ferricia.Mui.newSpriteMesh sealed class MeshDrawable(handle: ULong) : Drawable(handle) { } -class SpriteMesh(x0: Int, y0: Int, x1: Int, y1: Int) : MeshDrawable(newSpriteMesh(intArrayOf(x0, y0, x1, y1))) { +class SpriteMesh(canvas: Canvas, x0: Int, y0: Int, x1: Int, y1: Int) : + MeshDrawable(canvas.newSpriteMesh(x0, y0, x1, y1)) { } diff --git a/src/internal/client/kotlin/net/terramodulus/engine/ModelTransform.kt b/src/internal/client/kotlin/net/terramodulus/engine/ModelTransform.kt index 556db221..3b9752c7 100644 --- a/src/internal/client/kotlin/net/terramodulus/engine/ModelTransform.kt +++ b/src/internal/client/kotlin/net/terramodulus/engine/ModelTransform.kt @@ -5,29 +5,48 @@ package net.terramodulus.engine -import net.terramodulus.engine.ferricia.Mui.modelFullScaling -import net.terramodulus.engine.ferricia.Mui.modelSmartScaling +import com.cout970.math.vec2.ImmVec2d +import com.cout970.math.vec2.MutVec2d +import com.cout970.math.vec2.Vec2d +import net.terramodulus.engine.ferricia.Mui.modelGeneralTransform +import net.terramodulus.engine.ferricia.Mui.updateGeneralTransform +import java.io.Closeable +import kotlin.properties.Delegates @OptIn(ExperimentalUnsignedTypes::class) -sealed class ModelTransform(handles: ULongArray) { +sealed class ModelTransform(handles: ULongArray) : Closeable { internal val handle: ULong = handles[0] internal val wideHandle: ULong = handles[1] + + override fun close() { + TODO("Not yet implemented") + } } @OptIn(ExperimentalUnsignedTypes::class) -class SmartScaling private constructor(vararg args: Int) : - ModelTransform(modelSmartScaling(args)) { - - companion object { - fun none(w: Int, h: Int) = SmartScaling(w, h, 0) - - fun x(w: Int, h: Int, ww: Int, hh: Int) = SmartScaling(w, h, 1, ww, hh) - - fun y(w: Int, h: Int, ww: Int, hh: Int) = SmartScaling(w, h, 2, ww, hh) +class GeneralTransform(sx: Double, sy: Double, angle: Double, px: Double, py: Double) : + ModelTransform(modelGeneralTransform(doubleArrayOf(sx, sy, angle, px, py))) { + constructor() : this(1.0, 1.0, 0.0, 0.0, 0.0) + + var scale: Vec2d = ImmVec2d(sx, sy) + private set + var angle: Double = angle + private set + var pos: Vec2d = ImmVec2d(px, py) + private set + + interface Op { + var scale: Vec2d + var angle: Double + var pos: Vec2d + } - fun both(w: Int, h: Int, ww: Int, hh: Int) = SmartScaling(w, h, 3, ww, hh) + fun update(operation: Op.() -> Unit) { + operation(object : Op { + override var scale: Vec2d by this@GeneralTransform::scale + override var angle: Double by this@GeneralTransform::angle + override var pos: Vec2d by this@GeneralTransform::pos + }) + updateGeneralTransform(handle, doubleArrayOf(scale.x, scale.y, angle, pos.x, pos.y)) } } - -@OptIn(ExperimentalUnsignedTypes::class) -class FullScaling(w: Int, h: Int) : ModelTransform(modelFullScaling(intArrayOf(w, h))) diff --git a/src/internal/client/kotlin/net/terramodulus/engine/TextRenderer.kt b/src/internal/client/kotlin/net/terramodulus/engine/TextRenderer.kt new file mode 100644 index 00000000..be3e7ccb --- /dev/null +++ b/src/internal/client/kotlin/net/terramodulus/engine/TextRenderer.kt @@ -0,0 +1,21 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.engine + +import com.cout970.math.vec2.Vec2f +import net.terramodulus.engine.ferricia.Mui.newTextRenderer + +class TextRenderer internal constructor(windowHandle: ULong, geoProgramHandle: ULong, txtProgramHandle: ULong) { + private val handle = newTextRenderer(windowHandle, geoProgramHandle, txtProgramHandle) + + fun renderText( + ctx: TextRenderingContext, + canvas: Canvas, + glyphManager: GlyphManager, + fontManager: FontManager, + pos: Vec2f, + ) = ctx.render(canvas.handle, glyphManager.handle, handle, fontManager.handle, pos) +} diff --git a/src/internal/client/kotlin/net/terramodulus/engine/TextRenderingContext.kt b/src/internal/client/kotlin/net/terramodulus/engine/TextRenderingContext.kt new file mode 100644 index 00000000..91f34652 --- /dev/null +++ b/src/internal/client/kotlin/net/terramodulus/engine/TextRenderingContext.kt @@ -0,0 +1,44 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.engine + +import com.cout970.math.vec2.Vec2f +import com.cout970.math.vec4.Vec4i +import net.terramodulus.engine.ferricia.Mui.fetchTextRenderingContextSize +import net.terramodulus.engine.ferricia.Mui.newTextRenderingContext +import net.terramodulus.engine.ferricia.Mui.renderText +import net.terramodulus.engine.ferricia.Mui.setTextRenderingContextColor +import net.terramodulus.engine.ferricia.Mui.setTextRenderingContextMetrics +import net.terramodulus.engine.ferricia.Mui.setTextRenderingContextSize +import net.terramodulus.engine.ferricia.Mui.setTextRenderingContextText + +class TextRenderingContext internal constructor( + private val managerHandle: ULong, + fontSize: Float, + lineHeight: Float, + color: Vec4i, +) { + private val handle = newTextRenderingContext(managerHandle, floatArrayOf(fontSize, lineHeight), color.toArray()) + + fun setColor(color: Vec4i) = setTextRenderingContextColor(handle, color.toArray()) + + fun setMetrics(fontSize: Float, lineHeight: Float,) = + setTextRenderingContextMetrics(handle, floatArrayOf(fontSize, lineHeight)) + + fun setSize(width: Float, height: Float) = setTextRenderingContextSize(handle, floatArrayOf(width, height)) + + fun fetchSize() = fetchTextRenderingContextSize(handle, managerHandle) + + fun setText(text: String) = setTextRenderingContextText(handle, text) + + internal fun render( + canvasHandle: ULong, + glyphMgrHandle: ULong, + rendererHandle: ULong, + fontMgrHandle: ULong, + pos: Vec2f, + ) = renderText(canvasHandle, glyphMgrHandle, rendererHandle, fontMgrHandle, handle, pos.toArray()) +} diff --git a/src/internal/client/kotlin/net/terramodulus/engine/Window.kt b/src/internal/client/kotlin/net/terramodulus/engine/Window.kt index ef3c5e54..cfffb58f 100644 --- a/src/internal/client/kotlin/net/terramodulus/engine/Window.kt +++ b/src/internal/client/kotlin/net/terramodulus/engine/Window.kt @@ -5,8 +5,10 @@ package net.terramodulus.engine +import com.cout970.math.vec2.ImmVec2f import net.terramodulus.engine.ferricia.Mui.dropSdlHandle import net.terramodulus.engine.ferricia.Mui.dropWindowHandle +import net.terramodulus.engine.ferricia.Mui.getMousePos import net.terramodulus.engine.ferricia.Mui.initSdlHandle import net.terramodulus.engine.ferricia.Mui.initWindowHandle import net.terramodulus.engine.ferricia.Mui.resizeGLViewport @@ -18,17 +20,38 @@ import java.io.Closeable /** * Manages the SDL window instance and the underlying GL context. */ -class Window : Closeable { +class Window( + width: UInt, + height: UInt, +) : Closeable { + var width = width + private set + var height = height + private set private val sdlHandle = initSdlHandle() - private val windowHandle = initWindowHandle(sdlHandle) + private val windowHandle = initWindowHandle(sdlHandle) // TODO pass dimensions val canvas = Canvas(windowHandle) + private val listeners = HashSet<(UInt, UInt) -> Unit>() + + fun addListener(listener: (UInt, UInt) -> Unit) = listeners.add(listener) + fun removeListener(listener: (UInt, UInt) -> Unit) = listeners.remove(listener) + + fun sizeChanged(width: UInt, height: UInt) { + this.width = width + this.height = height + canvas.resizeGLViewport() + listeners.forEach { it(width, height) } + } + fun show() = showWindow(windowHandle) fun swap() = swapWindow(windowHandle) fun pollEvents() = sdlPoll(sdlHandle) + fun getMousePos() = getMousePos(sdlHandle).let { ImmVec2f(it[0], height.toFloat() - it[1]) } + override fun close() { dropWindowHandle(windowHandle) dropSdlHandle(sdlHandle) diff --git a/src/internal/client/kotlin/net/terramodulus/engine/WorldObjDrawable.kt b/src/internal/client/kotlin/net/terramodulus/engine/WorldObjDrawable.kt index 55fe2423..0e29ea6f 100644 --- a/src/internal/client/kotlin/net/terramodulus/engine/WorldObjDrawable.kt +++ b/src/internal/client/kotlin/net/terramodulus/engine/WorldObjDrawable.kt @@ -5,38 +5,47 @@ package net.terramodulus.engine -import net.terramodulus.engine.ferricia.Gwr.newMeshGeomCube -import net.terramodulus.engine.ferricia.Gwr.newMeshGeomSphere +import com.cout970.math.quaternion.Quatd +import com.cout970.math.vec3.Vec3d +import com.cout970.math.vec4.Vec4i +import net.terramodulus.engine.ferricia.Gwr.newDrawableWorldObj import net.terramodulus.engine.ferricia.Gwr.updateWorldObjModel -sealed class WorldObjDrawable(internal val handle: ULong, private var pos: Vec3D, private var scale: Vec3D, private var rot: Quat) { +class WorldObjDrawable(geom: WorldObjGeom, rgba: Vec4i, private var pos: Vec3d, private var scale: Vec3d, private var rot: Quatd) { + internal val handle = newDrawableWorldObj(geom.wideHandle, rgba.toArray()) fun updateModel(px: Double, py: Double, pz: Double, sx: Double, sy: Double, sz: Double, w: Double, i: Double, j: Double, k: Double) = updateWorldObjModel(handle, doubleArrayOf(px, py, pz, w, i, j, k, sx, sy, sz)) - fun updateModel(pos: Vec3D, scale: Vec3D, rot: Quat) = - updateModel(pos.x, pos.y, pos.z, scale.x, scale.y, scale.z, rot.w, rot.i, rot.j, rot.k) + fun updateModel(pos: Vec3d, scale: Vec3d, rot: Quatd) = + updateModel(pos.x, pos.y, pos.z, scale.x, scale.y, scale.z, rot.w, rot.x, rot.y, rot.z) init { updateModel(pos, scale, rot) } - fun setPos(value: Vec3D) { + fun setPos(value: Vec3d) { pos = value updateModel(pos, scale, rot) } - fun setScale(value: Vec3D) { + fun setScale(value: Vec3d) { scale = value updateModel(pos, scale, rot) } - fun setRot(value: Quat) { + fun setRot(value: Quatd) { rot = value updateModel(pos, scale, rot) } } -class SimpleMesh3dGeomCube(width: Float, rgba: Rgba, pos: Vec3D, scale: Vec3D, rot: Quat) : - WorldObjDrawable(newMeshGeomCube(width, rgba.toArray()), pos, scale, rot) +@OptIn(ExperimentalUnsignedTypes::class) +sealed class WorldObjGeom(handles: ULongArray) { + protected val handle = handles[0] + internal val wideHandle = handles[1] +} + +@OptIn(ExperimentalUnsignedTypes::class) +class SimpleMesh3dGeomCube(canvas: Canvas, width: Float) : WorldObjGeom(canvas.newMeshGeomCube(width)) -class SimpleMesh3dGeomSphere(radius: Float, rgba: Rgba, pos: Vec3D, scale: Vec3D, rot: Quat) : - WorldObjDrawable(newMeshGeomSphere(radius, rgba.toArray()), pos, scale, rot) +@OptIn(ExperimentalUnsignedTypes::class) +class SimpleMesh3dGeomSphere(canvas: Canvas, radius: Float) : WorldObjGeom(canvas.newMeshGeomSphere(radius)) diff --git a/src/internal/client/kotlin/net/terramodulus/engine/ferricia/Gwr.kt b/src/internal/client/kotlin/net/terramodulus/engine/ferricia/Gwr.kt index dc7c8d36..c3f55316 100644 --- a/src/internal/client/kotlin/net/terramodulus/engine/ferricia/Gwr.kt +++ b/src/internal/client/kotlin/net/terramodulus/engine/ferricia/Gwr.kt @@ -5,14 +5,16 @@ package net.terramodulus.engine.ferricia +@OptIn(ExperimentalUnsignedTypes::class) internal object Gwr { /** + * @param windowHandle window handle pointer * @param vsh source code of vector shader * @param fsh source code of fragment shader * @return GWR Geo Shader Program handle pointer */ @JvmName("geoShaders") - external fun geoShaders(vsh: String, fsh: String): ULong + external fun geoShaders(windowHandle: ULong, vsh: String, fsh: String): ULong /** * @param canvasHandle Canvas handle pointer @@ -22,6 +24,13 @@ internal object Gwr { @JvmName("newCamera") external fun newCamera(canvasHandle: ULong, data: FloatArray): ULong + /** + * @param cameraHandle Camera3D handle pointer + * @return `[x, z]` space dimensions + */ + @JvmName("getCameraSpace") + external fun getCameraSpace(cameraHandle: ULong): DoubleArray + /** * @param cameraHandle Camera3D handle pointer * @param data `[x, y, z]` @@ -37,20 +46,28 @@ internal object Gwr { external fun setCameraZoomLevel(cameraHandle: ULong, data: Float) /** + * @param windowHandle window handle pointer * @param width cube's width, in `(0,2]` - * @param data `[r, g, b, a]` - * @return DrawableWorldObj handle pointer + * @return SimpleMesh3dGeom handle pointer and Render3dPrimitive (wide) handle pointer */ @JvmName("newMeshGeomCube") - external fun newMeshGeomCube(width: Float, data: IntArray): ULong + external fun newMeshGeomCube(windowHandle: ULong, width: Float): ULongArray /** + * @param windowHandle window handle pointer * @param width cube's radius, in `(0,1]` + * @return SimpleMesh3dGeom handle pointer and Render3dPrimitive (wide) handle pointer + */ + @JvmName("newMeshGeomSphere") + external fun newMeshGeomSphere(windowHandle: ULong, width: Float): ULongArray + + /** + * @param handle Render3dPrimitive (wide) handle pointer * @param data `[r, g, b, a]` * @return DrawableWorldObj handle pointer */ - @JvmName("newMeshGeomSphere") - external fun newMeshGeomSphere(width: Float, data: IntArray): ULong + @JvmName("newDrawableWorldObj") + external fun newDrawableWorldObj(handle: ULong, data: IntArray): ULong /** * @param objHandle DrawableWorldObj handle pointer @@ -60,11 +77,12 @@ internal object Gwr { external fun updateWorldObjModel(objHandle: ULong, data: DoubleArray) /** + * @param windowHandle window handle pointer * @param canvasHandle Canvas handle pointer * @param cameraHandle Camera3D handle pointer * @param objHandle DrawableWorldObj handle pointer * @param programHandle GWR Shader Program handle pointer */ @JvmName("drawGwrObj") - external fun drawGwrObj(canvasHandle: ULong, cameraHandle: ULong, objHandle: ULong, programHandle: ULong) + external fun drawGwrObj(windowHandle: ULong, canvasHandle: ULong, cameraHandle: ULong, objHandle: ULong, programHandle: ULong) } diff --git a/src/internal/client/kotlin/net/terramodulus/engine/ferricia/Mui.kt b/src/internal/client/kotlin/net/terramodulus/engine/ferricia/Mui.kt index 6ffa4fa0..ab5ce5ad 100644 --- a/src/internal/client/kotlin/net/terramodulus/engine/ferricia/Mui.kt +++ b/src/internal/client/kotlin/net/terramodulus/engine/ferricia/Mui.kt @@ -7,6 +7,7 @@ package net.terramodulus.engine.ferricia import net.terramodulus.engine.MuiEvent +@OptIn(ExperimentalUnsignedTypes::class) internal object Mui { /** * @return SDL handle pointer @@ -39,6 +40,13 @@ internal object Mui { @JvmName("getGLVersion") external fun getGLVersion(windowHandle: ULong): String + /** + * @param sdlHandle SDL handle pointer + * @return `[x, y]` in window coordinates + */ + @JvmName("getMousePos") + external fun getMousePos(sdlHandle: ULong): FloatArray + /** * @param sdlHandle SDL handle pointer * @return the list of all MUI events in this frame @@ -91,48 +99,59 @@ internal object Mui { @JvmName("loadImageToCanvas") external fun loadImageToCanvas(canvasHandle: ULong, data: ByteArray): UInt + /** + * @param windowHandle window handle pointer + */ @JvmName("clearCanvas") - external fun clearCanvas() + external fun clearCanvas(windowHandle: ULong) + /** + * @param windowHandle window handle pointer + */ @JvmName("setCanvasClearColor") - external fun setCanvasClearColor(r: Float, g: Float, b: Float, a: Float) + external fun setCanvasClearColor(windowHandle: ULong, r: Float, g: Float, b: Float, a: Float) /** + * @param windowHandle window handle pointer * @param vsh source code of vector shader * @param fsh source code of fragment shader * @return Geo Shader Program handle pointer */ @JvmName("geoShaders") - external fun geoShaders(vsh: String, fsh: String): ULong + external fun geoShaders(windowHandle: ULong, vsh: String, fsh: String): ULong /** + * @param windowHandle window handle pointer * @param vsh source code of vector shader * @param fsh source code of fragment shader * @return Tex Shader Program handle pointer */ @JvmName("texShaders") - external fun texShaders(vsh: String, fsh: String): ULong + external fun texShaders(windowHandle: ULong, vsh: String, fsh: String): ULong /** + * @param windowHandle window handle pointer * @param data `[x0, y0, x1, y1, r, g, b, a]` * @return SimpleLineGeom as DrawableSet handle pointer */ @JvmName("newSimpleLineGeom") - external fun newSimpleLineGeom(data: IntArray): ULong + external fun newSimpleLineGeom(windowHandle: ULong, data: IntArray): ULong /** + * @param windowHandle window handle pointer * @param data `[x0, y0, x1, y1, r, g, b, a]` * @return SimpleRectGeom as DrawableSet handle pointer */ @JvmName("newSimpleRectGeom") - external fun newSimpleRectGeom(data: IntArray): ULong + external fun newSimpleRectGeom(windowHandle: ULong, data: IntArray): ULong /** + * @param windowHandle window handle pointer * @param data `[x0, y0, x1, y1]` * @return SpriteMesh as DrawableSet handle pointer */ @JvmName("newSpriteMesh") - external fun newSpriteMesh(data: IntArray): ULong + external fun newSpriteMesh(windowHandle: ULong, data: IntArray): ULong /** * @param handle DrawableSet handle pointer @@ -142,29 +161,22 @@ internal object Mui { external fun setGeomPos(handle: ULong, data: FloatArray) /** - * @param data `[w, h, param, w, h]` - * @return SmartScaling handle pointers + * @param data `[sx, sy, angle, px, py]`; scaling, rotation, position + * @return GeneralTransform handle pointer and PrimModelTransform (wide) handle pointer */ - @JvmName("modelSmartScaling") - external fun modelSmartScaling(data: IntArray): ULongArray + @JvmName("modelGeneralTransform") + external fun modelGeneralTransform(data: DoubleArray): ULongArray /** - * @param data `[w, h]` - * @return FullScaling handle pointers + * @param handle GeneralTransform thin pointer + * @param data `[sx, sy, angle, px, py]`; scaling, rotation, position */ - @JvmName("modelFullScaling") - external fun modelFullScaling(data: IntArray): ULongArray - - /** - * @param data `[x, y]` - * @return SimpleTranslation handle pointers - */ - @JvmName("modelSimpleTranslation") - external fun modelSimpleTranslation(data: FloatArray): ULongArray + @JvmName("updateGeneralTransform") + external fun updateGeneralTransform(handle: ULong, data: DoubleArray) /** * @param data alpha - * @return AlphaFilter handle pointers + * @return AlphaFilter handle pointer and PrimColorFilter (wide) handle pointer */ @JvmName("filterAlphaFilter") external fun filterAlphaFilter(data: Float): ULongArray @@ -219,4 +231,112 @@ internal object Mui { */ @JvmName("drawGuiTex") external fun drawGuiTex(canvasHandle: ULong, drawableHandle: ULong, programHandle: ULong, textureHandle: UInt) + + /** + * @return FontManager handle pointer + */ + @JvmName("newFontManager") + external fun newFontManager(): ULong + + /** + * @param managerHandle FontManager handle pointer + * @param windowHandle Window handle pointer + * @return GlyphManager handle pointer + */ + @JvmName("newGlyphManager") + external fun newGlyphManager(managerHandle: ULong, windowHandle: ULong): ULong + + /** + * @param windowHandle Window handle pointer + * @param vsh Window handle pointer + * @param fsh Window handle pointer + * @return TxtProgram handle pointer + */ + @JvmName("newTxtProgram") + external fun newTxtProgram(windowHandle: ULong, vsh: String, fsh: String): ULong + + /** + * @param windowHandle Window handle pointer + * @param geoProgramHandle GeoProgram handle pointer + * @param txtProgramHandle TxtProgram handle pointer + * @return TextRenderer handle pointer + */ + @JvmName("newTextRenderer") + external fun newTextRenderer(windowHandle: ULong, geoProgramHandle: ULong, txtProgramHandle: ULong): ULong + + /** + * @param managerHandle FontManager handle pointer + * @param data1 Font size and line height in pixels + * @param data2 `[r, g, b, a]` in [0,255] + * @return TextRenderingContext handle pointer + */ + @JvmName("newTextRenderingContext") + external fun newTextRenderingContext(managerHandle: ULong, data1: FloatArray, data2: IntArray): ULong + + /** + * @param ctxHandle TextRenderingContext handle pointer + * @param data `[r, g, b, a]` in [0,255] + */ + @JvmName("setTextRenderingContextColor") + external fun setTextRenderingContextColor(ctxHandle: ULong, data: IntArray) + + /** + * @param ctxHandle TextRenderingContext handle pointer + * @param data Font size and line height in pixels + */ + @JvmName("setTextRenderingContextMetrics") + external fun setTextRenderingContextMetrics(ctxHandle: ULong, data: FloatArray) + + /** + * @param ctxHandle TextRenderingContext handle pointer + * @param data Width and height in pixels + */ + @JvmName("setTextRenderingContextSize") + external fun setTextRenderingContextSize(ctxHandle: ULong, data: FloatArray) + + /** + * @param ctxHandle TextRenderingContext handle pointer + * @param text Contents of entire text widget + */ + @JvmName("setTextRenderingContextText") + external fun setTextRenderingContextText(ctxHandle: ULong, text: String) + + /** + * @param ctxHandle TextRenderingContext handle pointer + * @param fmHandle FontManager handle pointer + * @return `[w, h]` text context size/dimensions + */ + @JvmName("fetchTextRenderingContextSize") + external fun fetchTextRenderingContextSize(ctxHandle: ULong, fmHandle: ULong): FloatArray + + /** + * @param canvasHandle Canvas handle pointer + * @param glyphMgrHandle GlyphManager handle pointer + * @param rendererHandle TextRenderer handle pointer + * @param fontMgrHandle FontManager handle pointer + * @param ctxHandle TextRenderingContext handle pointer + * @param data `[x, y]` Position + */ + @JvmName("renderText") + external fun renderText( + canvasHandle: ULong, + glyphMgrHandle: ULong, + rendererHandle: ULong, + fontMgrHandle: ULong, + ctxHandle: ULong, + data: FloatArray, + ) + + /** + * @param canvasHandle Canvas handle pointer + * @param data `[x, y, w, h]` scissor box in window coordinates + */ + @JvmName("enableScissor") + external fun enableScissor(canvasHandle: ULong, data: IntArray) + + /** + * @param canvasHandle Canvas handle pointer + */ + @JvmName("disableScissor") + external fun disableScissor(canvasHandle: ULong) } diff --git a/src/internal/common/kotlin/net/terramodulus/engine/Containers.kt b/src/internal/common/kotlin/net/terramodulus/engine/Containers.kt deleted file mode 100644 index d15aea07..00000000 --- a/src/internal/common/kotlin/net/terramodulus/engine/Containers.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.engine - -data class Quat(val w: Double, val i: Double, val j: Double, val k: Double) { - fun toArray() = doubleArrayOf(w, i, j, k) -} - -data class Vec3D(val x: Double, val y: Double, val z: Double) { - companion object { - val ZERO = Vec3D(0.0, 0.0, 0.0) - - /** - * @param array array containing 3 double values - */ - fun fromArray(array: DoubleArray) = Vec3D(array[0], array[1], array[2]) - } - - fun toArray() = doubleArrayOf(x, y, z) -} diff --git a/src/internal/common/kotlin/net/terramodulus/engine/PhyBody.kt b/src/internal/common/kotlin/net/terramodulus/engine/PhyBody.kt index b2cca8ff..1e626f3d 100644 --- a/src/internal/common/kotlin/net/terramodulus/engine/PhyBody.kt +++ b/src/internal/common/kotlin/net/terramodulus/engine/PhyBody.kt @@ -5,6 +5,9 @@ package net.terramodulus.engine +import com.cout970.math.vec3.Vec3d +import net.terramodulus.engine.common.ImmVec3dFromArray +import net.terramodulus.engine.common.toArray import net.terramodulus.engine.ferricia.Physics.addPhyBodyForce import net.terramodulus.engine.ferricia.Physics.addPhyBodyGeom import net.terramodulus.engine.ferricia.Physics.getPhyBodyLinearVel @@ -22,19 +25,25 @@ class PhyBody internal constructor(worldHandle: ULong, mass: Mass) { class SphereTotal(mass: Double, radius: Double) : Mass(newMassSphereTotal(mass, radius)) } - var pos - get() = Vec3D.fromArray(getPhyBodyPos(handle)) + var pos: Vec3d + get() = ImmVec3dFromArray(getPhyBodyPos(handle)) set(value) = setPhyBodyPos(handle, value.toArray()) - var linearVel - get() = Vec3D.fromArray(getPhyBodyLinearVel(handle)) + var linearVel: Vec3d + get() = ImmVec3dFromArray(getPhyBodyLinearVel(handle)) set(value) = setPhyBodyLinearVel(handle, value.toArray()) var gravityMode: Boolean by Delegates.observable(true) { _, _, newValue -> setPhyBodyGravityMode(handle, newValue) } - fun addGeom(geom: PhyGeom) = addPhyBodyGeom(handle, geom.handle) + private val _geoms = mutableSetOf() + val geoms: Set get() = _geoms - fun addForce(force: Vec3D) = addPhyBodyForce(handle, force.toArray()) + fun addGeom(geom: PhyGeom) { + addPhyBodyGeom(handle, geom.handle) + _geoms.add(geom) + } + + fun addForce(force: Vec3d) = addPhyBodyForce(handle, force.toArray()) } diff --git a/src/internal/common/kotlin/net/terramodulus/engine/PhyWorld.kt b/src/internal/common/kotlin/net/terramodulus/engine/PhyWorld.kt index b985ab0a..ef74ddbe 100644 --- a/src/internal/common/kotlin/net/terramodulus/engine/PhyWorld.kt +++ b/src/internal/common/kotlin/net/terramodulus/engine/PhyWorld.kt @@ -5,6 +5,9 @@ package net.terramodulus.engine +import com.cout970.math.vec3.Vec3d +import net.terramodulus.engine.common.ZeroImmVec3d +import net.terramodulus.engine.common.toArray import net.terramodulus.engine.ferricia.Physics.newPhyCollisionManager import net.terramodulus.engine.ferricia.Physics.newPhyWorld import net.terramodulus.engine.ferricia.Physics.omitPhyCollisionManagerSpace @@ -18,7 +21,7 @@ class PhyWorld internal constructor(envHandle: ULong) { private val handle = newPhyWorld(envHandle) private val cmHandle = newPhyCollisionManager() - var gravity: Vec3D by Delegates.observable(Vec3D.ZERO) { _, _, newValue -> + var gravity: Vec3d by Delegates.observable(ZeroImmVec3d) { _, _, newValue -> setPhyWorldGravity(handle, newValue.toArray()) } diff --git a/src/internal/common/kotlin/net/terramodulus/engine/common/Containers.kt b/src/internal/common/kotlin/net/terramodulus/engine/common/Containers.kt new file mode 100644 index 00000000..e1a9a08e --- /dev/null +++ b/src/internal/common/kotlin/net/terramodulus/engine/common/Containers.kt @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.engine.common + +import com.cout970.math.vec3.ImmVec3d +import com.cout970.math.vec3.ImmVec3f +import com.cout970.math.vec3.Vec3d + +/** + * @throws ArrayIndexOutOfBoundsException if [array]'s size < 3 + */ +fun ImmVec3dFromArray(array: DoubleArray) = ImmVec3d(array[0], array[1], array[2]) + +fun Vec3d.toArray() = doubleArrayOf(x, y, z) + +val ZeroImmVec3d = ImmVec3d(0.0) +val ZeroImmVec3f = ImmVec3f(0F) diff --git a/src/kernel/client/kotlin/net/terramodulus/core/Main.kt b/src/kernel/client/kotlin/net/terramodulus/core/Main.kt index f27c43ad..44747f35 100644 --- a/src/kernel/client/kotlin/net/terramodulus/core/Main.kt +++ b/src/kernel/client/kotlin/net/terramodulus/core/Main.kt @@ -15,7 +15,6 @@ import net.terramodulus.common.core.ApplicationArgumentParsingError import net.terramodulus.common.core.ApplicationInitializationFault import net.terramodulus.common.core.run import net.terramodulus.common.core.setupInit -import net.terramodulus.mui.GuiManager import net.terramodulus.util.exception.CodeLogicFault import net.terramodulus.util.exception.triggerGlobalCrash import java.awt.Dimension diff --git a/src/kernel/client/kotlin/net/terramodulus/core/TerraModulus.kt b/src/kernel/client/kotlin/net/terramodulus/core/TerraModulus.kt index 29846b28..53cfb892 100644 --- a/src/kernel/client/kotlin/net/terramodulus/core/TerraModulus.kt +++ b/src/kernel/client/kotlin/net/terramodulus/core/TerraModulus.kt @@ -6,23 +6,34 @@ package net.terramodulus.core import net.terramodulus.common.core.AbstractTerraModulus -import net.terramodulus.mui.GuiManager +import net.terramodulus.mui.MuiManager +import net.terramodulus.mui.gui.GuiManager import net.terramodulus.void.World +import kotlin.time.Duration.Companion.seconds +import kotlin.time.TimeSource class TerraModulus internal constructor() : AbstractTerraModulus() { - private val guiManager = GuiManager(this) + private val muiManager = MuiManager(this) internal var world: World? = null - override var tps: Int - get() = TODO("Not yet implemented") - set(value) {} + var tps = 0 + private set override fun run() { - guiManager.showWindow() + muiManager.showWindow() + val timeSource = TimeSource.Monotonic + var lastTick = timeSource.markNow() + var ticks = 0 while (true) { - guiManager.updateCanvas() -// guiManager.updateScreens() - Thread.sleep(1) + muiManager.update() + ticks++ + val now = timeSource.markNow() + if (now - lastTick >= 1.seconds) { + lastTick = now + tps = ticks + ticks = 0 + } + Thread.sleep(0) } } diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/GuiManager.kt b/src/kernel/client/kotlin/net/terramodulus/mui/MuiManager.kt similarity index 77% rename from src/kernel/client/kotlin/net/terramodulus/mui/GuiManager.kt rename to src/kernel/client/kotlin/net/terramodulus/mui/MuiManager.kt index 60ab391b..5c25276a 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/GuiManager.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/MuiManager.kt @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors * SPDX-License-Identifier: LGPL-3.0-only */ @@ -8,38 +8,37 @@ package net.terramodulus.mui import net.terramodulus.core.TerraModulus import net.terramodulus.engine.MuiEvent import net.terramodulus.engine.Window -import net.terramodulus.mui.gfx.RenderSystem -import net.terramodulus.mui.gms.ScreenManager -import net.terramodulus.mui.input.InputSystem +import net.terramodulus.mui.aui.AuiManager +import net.terramodulus.mui.gui.GuiManager +import net.terramodulus.mui.hui.HuiManager +import net.terramodulus.mui.kui.InputSystem +import net.terramodulus.mui.kui.KeyboardInputHandler +import net.terramodulus.mui.kui.KuiManager +import net.terramodulus.mui.kui.MouseInputHandler +import net.terramodulus.mui.uid.UidManager import net.terramodulus.util.logging.logger import java.io.Closeable private val logger = logger {} +private const val WIDTH = 800u +private const val HEIGHT = 480u -/** - * Graphical User Interface (GUI) Manager - */ -internal class GuiManager internal constructor(core: TerraModulus) : Closeable { - private val window = Window() - val renderSystem = RenderSystem(core, window.canvas) - val inputSystem = InputSystem() - val screenManager = ScreenManager(renderSystem.handle) +internal class MuiManager internal constructor(core: TerraModulus) : Closeable { + private val window = Window(WIDTH, HEIGHT) // SDL Window + internal val uidManager = UidManager() + internal val auiManager = AuiManager() + internal val huiManager = HuiManager() + internal val kuiManager = KuiManager(uidManager) + internal val guiManager = GuiManager(window, core) internal fun showWindow() = window.show() -// /** -// * Screen updating, targeting as the same as *maximum FPS*, -// * but the numbers of ticks are not supposed to be compensated when missed, -// * so it is up to the callers to compensate missed activities. -// */ -// internal fun updateScreens() {} - /** - * Canvas updating, per frame, maximally the *maximum FPS*. + * SDL events updating, per frame, maximally the *maximum FPS*. * This includes input ticking and canvas rendering. */ - internal fun updateCanvas() { - val keyEvents = ArrayList() + internal fun update() { + val inputEvents = ArrayList() window.pollEvents().forEach { event -> when (event) { is MuiEvent.DisplayAdded -> { @@ -125,11 +124,11 @@ internal class GuiManager internal constructor(core: TerraModulus) : Closeable { } is MuiEvent.KeyboardKeyDown -> { logger.debug { "Keyboard (id: ${event.keyboardId}) key `${event.key}` down." } - keyEvents.add(InputSystem.KeyEvent.Down(InputSystem.KeyId(event.key))) + inputEvents.add(InputSystem.InputEvent.Keyboard(KeyboardInputHandler.KeyEvent.Down(KeyboardInputHandler.KeyId(event.key)))) } is MuiEvent.KeyboardKeyUp -> { logger.debug { "Keyboard (id: ${event.keyboardId}) key `${event.key}` up." } - keyEvents.add(InputSystem.KeyEvent.Up(InputSystem.KeyId(event.key))) + inputEvents.add(InputSystem.InputEvent.Keyboard(KeyboardInputHandler.KeyEvent.Up(KeyboardInputHandler.KeyId(event.key)))) } MuiEvent.KeyboardRemoved -> { logger.debug { "Keyboard removed." } @@ -142,12 +141,19 @@ internal class GuiManager internal constructor(core: TerraModulus) : Closeable { } is MuiEvent.MouseButtonDown -> { logger.debug { "Mouse (id: ${event.mouseId}) key `${event.key}` down." } + inputEvents.add(InputSystem.InputEvent.Mouse(MouseInputHandler.Event.Button.Down(MouseInputHandler.ButtonId( + event.key.toUInt() + )))) } is MuiEvent.MouseButtonUp -> { logger.debug { "Mouse (id: ${event.mouseId}) key `${event.key}` up." } + inputEvents.add(InputSystem.InputEvent.Mouse(MouseInputHandler.Event.Button.Up(MouseInputHandler.ButtonId( + event.key.toUInt() + )))) } is MuiEvent.MouseMotion -> { - logger.debug { "Mouse (id: ${event.mouseId}) motion (${event.x}, ${event.y})." } + // y is inverted as coordinates in y are inversed from window coordinates to rendering coordinates + inputEvents.add(InputSystem.InputEvent.Mouse(MouseInputHandler.Event.Movement(event.x, -event.y))) } MuiEvent.MouseRemoved -> { logger.debug { "Mouse removed." } @@ -226,7 +232,7 @@ internal class GuiManager internal constructor(core: TerraModulus) : Closeable { } is MuiEvent.WindowPixelSizeChanged -> { logger.debug { "Window pixel size changed to ${event.width}x${event.height}." } - window.canvas.resizeGLViewport() + window.sizeChanged(event.width, event.height) logger.debug { "Window viewport resized." } } is MuiEvent.WindowResized -> { @@ -240,14 +246,13 @@ internal class GuiManager internal constructor(core: TerraModulus) : Closeable { } } } - inputSystem.update(keyEvents) - screenManager.update(renderSystem, inputSystem) - window.canvas.clear() - screenManager.render(renderSystem) - window.swap() + guiManager.inputStatesHandle.update(inputEvents.asSequence(), window.getMousePos()) + kuiManager.inputSystem.update(inputEvents.asSequence()) + guiManager.updateScreens(this) + guiManager.updateCanvas() } override fun close() { - window.close() + TODO("Not yet implemented") } } diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/audio/AudioSystem.kt b/src/kernel/client/kotlin/net/terramodulus/mui/aui/AudioSystem.kt similarity index 82% rename from src/kernel/client/kotlin/net/terramodulus/mui/audio/AudioSystem.kt rename to src/kernel/client/kotlin/net/terramodulus/mui/aui/AudioSystem.kt index 3f8bc02b..26f330fa 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/audio/AudioSystem.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/aui/AudioSystem.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: LGPL-3.0-only */ -package net.terramodulus.mui.audio +package net.terramodulus.mui.aui class AudioSystem internal constructor() { } diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/AuiManager.kt b/src/kernel/client/kotlin/net/terramodulus/mui/aui/AuiManager.kt similarity index 57% rename from src/kernel/client/kotlin/net/terramodulus/mui/AuiManager.kt rename to src/kernel/client/kotlin/net/terramodulus/mui/aui/AuiManager.kt index 7d58bc1f..32ab8450 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/AuiManager.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/aui/AuiManager.kt @@ -1,11 +1,9 @@ /* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors * SPDX-License-Identifier: LGPL-3.0-only */ -package net.terramodulus.mui - -import net.terramodulus.mui.audio.AudioSystem +package net.terramodulus.mui.aui /** * Audio User Interface (AUI) Manager diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/ColorFilter.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gfx/ColorFilter.kt deleted file mode 100644 index d3a3ed42..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/ColorFilter.kt +++ /dev/null @@ -1,10 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.mui.gfx - -typealias ColorFilter = net.terramodulus.engine.ColorFilter - -typealias AlphaFilter = net.terramodulus.engine.AlphaFilter diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/GuiSprite.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gfx/GuiSprite.kt deleted file mode 100644 index d7d7a944..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/GuiSprite.kt +++ /dev/null @@ -1,18 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.mui.gfx - -import net.terramodulus.engine.SpriteMesh - -class GuiSprite(private val rect: RectangleI, private val texture: UInt) { - private val mesh = SpriteMesh(rect.x, rect.y, rect.x + rect.width, rect.y + rect.height) - - fun add(model: ModelTransform) = mesh.add(model) - - fun add(filter: ColorFilter) = mesh.add(filter) - - fun render(renderSystem: RenderSystem) = renderSystem.renderGuiTex(mesh, texture) -} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/ManagedRect.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gfx/ManagedRect.kt deleted file mode 100644 index 2362b7ae..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/ManagedRect.kt +++ /dev/null @@ -1,22 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.mui.gfx - -import kotlin.properties.Delegates.observable - -class ManagedRect(rect: RectangleF) { - var rect: RectangleF by observable(rect) { _, _, newValue -> observers.forEach { it(newValue) } } - - private val observers = LinkedHashSet<(RectangleF) -> Unit>() - - fun observe(observer: (RectangleF) -> Unit) { - observers.add(observer) - } - - fun unobserve(observer: (RectangleF) -> Unit) { - observers.remove(observer) - } -} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/ModelTransform.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gfx/ModelTransform.kt deleted file mode 100644 index 8dcdb20c..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/ModelTransform.kt +++ /dev/null @@ -1,14 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.mui.gfx - -typealias ModelTransform = net.terramodulus.engine.ModelTransform - -typealias SmartScaling = net.terramodulus.engine.SmartScaling - -typealias FullScaling = net.terramodulus.engine.FullScaling - -fun FullScaling(rect: Dimension2I) = FullScaling(rect.width, rect.height) diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/Rectangle.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gfx/Rectangle.kt deleted file mode 100644 index ecfeca3f..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/Rectangle.kt +++ /dev/null @@ -1,130 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.mui.gfx - -/** - * Rectangle in a coordinate system with (0, 0) on the bottom left. - * The anchor of the rectangle is the bottom-left corner. - */ -data class RectangleI( - val x: Int, - val y: Int, - val width: Int, - val height: Int -) { - companion object { - fun withPoints(x0: Int, y0: Int, x1: Int, y1: Int): RectangleI { - val minX: Int; - val maxX: Int; - if (x0 < x1) { - minX = x0; - maxX = x1; - } else { - maxX = x0; - minX = x1; - } - val minY: Int; - val maxY: Int; - if (y0 < y1) { - minY = y0; - maxY = y1; - } else { - maxY = y0; - minY = y1; - } - return RectangleI(minX, minY, maxX - minX, maxY - minY) - } - } - - val size get() = Dimension2I(width, height) - - fun anchor(pos: Anchor5) = when (pos) { - Anchor5.TopLeft -> Vector2I(x, y + width) - Anchor5.TopRight -> Vector2I(x + width, y + height) - Anchor5.BottomLeft -> Vector2I(x, y) - Anchor5.BottomRight -> Vector2I(x + width, y) - Anchor5.Center -> Vector2I(x + width / 2, y + height / 2) - } - - fun translateBy(pos: Vector2I) = RectangleI(x + pos.x, y + pos.y, width, height) - - fun translateBy(x: Int, y: Int) = RectangleI(this.x + x, this.y + y, width, height) - - fun translateByY(y: Int) = RectangleI(x, this.y + y, width, height) - - fun translateByX(x: Int) = RectangleI(this.x + x, y, width, height) - - fun translateToY(y: Int) = RectangleI(x, y, width, height) - - fun translateToX(x: Int) = RectangleI(x, y, width, height) - - fun translateTo(pos: Vector2I) = RectangleI(pos.x, pos.y, width, height) - - fun translateTo(x: Int, y: Int) = RectangleI(x, y, width, height) - - fun toFloat() = RectangleF(x.toFloat(), y.toFloat(), width.toFloat(), height.toFloat()) -} - -/** - * Rectangle in a coordinate system with (0, 0) on the bottom left. - * The anchor of the rectangle is the bottom-left corner. - */ -data class RectangleF( - val x: Float, - val y: Float, - val width: Float, - val height: Float -) { - companion object { - fun withPoints(x0: Float, y0: Float, x1: Float, y1: Float): RectangleF { - val minX: Float; - val maxX: Float; - if (x0 < x1) { - minX = x0; - maxX = x1; - } else { - maxX = x0; - minX = x1; - } - val minY: Float; - val maxY: Float; - if (y0 < y1) { - minY = y0; - maxY = y1; - } else { - maxY = y0; - minY = y1; - } - return RectangleF(minX, maxX, minY, maxY) - } - } - - val size get() = Dimension2F(width, height) - - fun anchor(pos: Anchor5) = when (pos) { - Anchor5.TopLeft -> Vector2F(x, y + width) - Anchor5.TopRight -> Vector2F(x + width, y + height) - Anchor5.BottomLeft -> Vector2F(x, y) - Anchor5.BottomRight -> Vector2F(x + width, y) - Anchor5.Center -> Vector2F(x + width / 2, y + height / 2) - } - - fun translateBy(pos: Vector2F) = RectangleF(x + pos.x, y + pos.y, width, height) - - fun translateBy(x: Float, y: Float) = RectangleF(this.x + x, this.y + y, width, height) - - fun translateByY(y: Float) = RectangleF(x, this.y + y, width, height) - - fun translateByX(x: Float) = RectangleF(this.x + x, y, width, height) - - fun translateToY(y: Float) = RectangleF(x, y, width, height) - - fun translateToX(x: Float) = RectangleF(x, y, width, height) - - fun translateTo(pos: Vector2F) = RectangleF(pos.x, pos.y, width, height) - - fun translateTo(x: Float, y: Float) = RectangleF(x, y, width, height) -} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/RenderSystem.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gfx/RenderSystem.kt deleted file mode 100644 index 0e86fb93..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/RenderSystem.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.mui.gfx - -import net.terramodulus.core.TerraModulus -import net.terramodulus.core.getResourceAsBytes -import net.terramodulus.core.getResourceAsString -import net.terramodulus.engine.Canvas -import net.terramodulus.engine.GeomDrawable -import net.terramodulus.engine.MeshDrawable -import net.terramodulus.mui.gms.impl.GameplayScreen - -class RenderSystem internal constructor(private val core: TerraModulus, private val canvas: Canvas) { - val handle: Handle = HandleImpl() - private val texShaders = canvas.loadTexShaders( - getResourceAsString("/gms_tex.vsh"), - getResourceAsString("/gms_tex.fsh") - ) - private val geoShaders = canvas.loadGeoShaders( - getResourceAsString("/gms_geo.vsh"), - getResourceAsString("/gms_geo.fsh") - ) - val targetFps = 1000; - - sealed interface Handle { - fun loadTexture(path: String): UInt - - fun setBackgroundColor(red: Float, green: Float, blue: Float, alpha: Float) - } - - private inner class HandleImpl : Handle { - override fun loadTexture(path: String) = canvas.loadImage(getResourceAsBytes(path)) - - override fun setBackgroundColor(red: Float, green: Float, blue: Float, alpha: Float) { - canvas.setClearColor(red, green, blue, alpha) - } - } - - internal fun newGameplayScreen(pos: Vector3F) = - { it: Handle -> GameplayScreen(core, canvas.createCamera(floatArrayOf(pos.x, pos.y, pos.z)), it) } - - internal fun renderGuiTex(drawable: MeshDrawable, texture: UInt) = canvas.renderGuiTex(drawable, texShaders, texture) - - internal fun renderGuiGeo(drawable: GeomDrawable) = canvas.renderGuiGeo(drawable, geoShaders) - - internal fun render() { - - } -} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/Vector.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gfx/Vector.kt deleted file mode 100644 index a92d68db..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/Vector.kt +++ /dev/null @@ -1,58 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.mui.gfx - -data class Vector2I(val x: Int, val y: Int) { - companion object { - val ZERO = Vector2I(0, 0) - } - - operator fun plus(other: Vector2I) = Vector2I(x + other.x, y + other.y) -} - -data class Vector2D(val x: Double, val y: Double) { - companion object { - val ZERO = Vector2D(.0, .0) - } - - operator fun plus(other: Vector2D) = Vector2D(x + other.x, y + other.y) -} - -data class Vector2F(val x: Float, val y: Float) { - companion object { - val ZERO = Vector2F(0F, 0F) - } - - operator fun plus(other: Vector2F) = Vector2F(x + other.x, y + other.y) -} - -data class Vector3I(val x: Int, val y: Int, val z: Int) { - companion object { - val ZERO = Vector3I(0, 0, 0) - } - - operator fun plus(other: Vector3I) = Vector3I(x + other.x, y + other.y, z + other.z) -} - -data class Vector3D(val x: Double, val y: Double, val z: Double) { - companion object { - val ZERO = Vector3D(.0, .0, .0) - } - - operator fun plus(other: Vector3D) = Vector3D(x + other.x, y + other.y, z + other.z) - - operator fun times(factor: Int) = Vector3D(x * factor, y * factor, z * factor) - operator fun times(factor: Float) = Vector3D(x * factor, y * factor, z * factor) - operator fun times(factor: Double) = Vector3D(x * factor, y * factor, z * factor) -} - -data class Vector3F(val x: Float, val y: Float, val z: Float) { - companion object { - val ZERO = Vector3F(0F, 0F, 0F) - } - - operator fun plus(other: Vector3F) = Vector3F(x + other.x, y + other.y, z + other.z) -} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gms/AbstractPanel.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gms/AbstractPanel.kt deleted file mode 100644 index 810e7259..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gms/AbstractPanel.kt +++ /dev/null @@ -1,9 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.mui.gms - -abstract class AbstractPanel : Component(), Container { -} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gms/Container.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gms/Container.kt deleted file mode 100644 index de7f2fba..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gms/Container.kt +++ /dev/null @@ -1,12 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.mui.gms - -import net.terramodulus.mui.gfx.ManagedRect - -sealed interface Container { - val rect: ManagedRect -} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gms/Menu.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gms/Menu.kt deleted file mode 100644 index ada1df80..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gms/Menu.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.mui.gms - -import net.terramodulus.mui.gms.event.MenuEvent -import java.util.ArrayDeque - -abstract class Menu : Container { - private val listeners = HashMap, LinkedHashSet<(MenuEvent) -> Unit>>() - private val components = LinkedHashSet() - private val componentQueue = ArrayDeque() - val handle: Handle = HandleImpl() - - private sealed interface ComponentOperation { - class Add(val component: () -> Component) : ComponentOperation - - class Remove(val component: Component) : ComponentOperation - } - - /** - * It is strongly suggested only using this function during initialization. - */ - protected fun addComponent(component: Component) { - components.add(component) - } - - /** - * It is strongly suggested only using this function during initialization. - */ - protected fun removeComponent(component: Component) { - components.remove(component) - } - - fun addListener(e: Class, l: (T) -> Unit) { - @Suppress("UNCHECKED_CAST") - listeners.computeIfAbsent(e) { LinkedHashSet() }.add(l as (MenuEvent) -> Unit) - } - - fun removeListener(e: Class, l: (T) -> Unit) { - listeners[e]?.remove(l) - } - - internal fun dispatchEvent(event: MenuEvent) { - listeners[event.javaClass]?.forEach { it(event) } - } - - sealed interface Handle { - fun addComponent(component: () -> Component) - - fun removeComponent(component: Component) - } - - private inner class HandleImpl : Handle { - override fun addComponent(component: () -> Component) { - componentQueue.add(ComponentOperation.Add(component)) - } - - override fun removeComponent(component: Component) { - componentQueue.add(ComponentOperation.Remove(component)) - } - } - - abstract fun render() -} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gms/Screen.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gms/Screen.kt deleted file mode 100644 index 60715ce5..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gms/Screen.kt +++ /dev/null @@ -1,139 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.mui.gms - -import net.terramodulus.mui.gfx.ManagedRect -import net.terramodulus.mui.gfx.RenderSystem -import net.terramodulus.mui.gms.event.ScreenEvent -import net.terramodulus.mui.input.InputSystem -import java.util.ArrayDeque - -abstract class Screen : Container { - private val listeners = HashMap, LinkedHashSet<(ScreenEvent) -> Unit>>() - private val menus = LinkedHashSet() - private val components = ArrayList() - private val componentQueue = ArrayDeque() - private val menuQueue = ArrayDeque() - override val rect: ManagedRect - get() = TODO("Not yet implemented") - val handle: Handle = HandleImpl() - - private sealed interface ComponentOperation { - fun apply(components: ArrayList) - - class Add(val component: () -> Component) : ComponentOperation { - override fun apply(components: ArrayList) { - components.add(component()) - } - } - - class Remove(val component: Component) : ComponentOperation { - override fun apply(components: ArrayList) { - components.remove(component) - } - } - } - - private sealed interface MenuOperation { - fun apply(menus: LinkedHashSet) - - class Add(val menu: () -> Menu) : MenuOperation { - override fun apply(menus: LinkedHashSet) { - menus.add(menu()) - } - } - - class Remove(val menu: Menu) : MenuOperation { - override fun apply(menus: LinkedHashSet) { - menus.remove(menu) - } - } - } - - /** - * It is strongly suggested only using this function during initialization. - */ - protected fun addComponent(component: Component) { - components.add(component) - } - - /** - * It is strongly suggested only using this function during initialization. - */ - protected fun removeComponent(component: Component) { - components.remove(component) - } - - /** - * It is strongly suggested only using this function during initialization. - */ - protected fun addMenu(menu: Menu) { - menus.add(menu) - } - - /** - * It is strongly suggested only using this function during initialization. - */ - protected fun removeMenu(menu: Menu) { - menus.remove(menu) - } - - fun addListener(e: Class, l: (T) -> Unit) { - @Suppress("UNCHECKED_CAST") - listeners.computeIfAbsent(e) { LinkedHashSet() }.add(l as (ScreenEvent) -> Unit) - } - - fun removeListener(e: Class, l: (T) -> Unit) { - listeners[e]?.remove(l) - } - - internal fun dispatchEvent(event: ScreenEvent) { - listeners[event.javaClass]?.forEach { it(event) } - } - - sealed interface Handle { - fun addComponent(component: () -> Component) - - fun removeComponent(component: Component) - - fun addMenu(menu: () -> Menu) - - fun removeMenu(menu: Menu) - } - - private inner class HandleImpl : Handle { - override fun addComponent(component: () -> Component) { - componentQueue.add(ComponentOperation.Add(component)) - } - - override fun removeComponent(component: Component) { - componentQueue.add(ComponentOperation.Remove(component)) - } - - override fun addMenu(menu: () -> Menu) { - menuQueue.add(MenuOperation.Add(menu)) - } - - override fun removeMenu(menu: Menu) { - menuQueue.add(MenuOperation.Remove(menu)) - } - } - - internal open fun update(renderSystem: RenderSystem, screenManager: ScreenManager, inputSystem: InputSystem) {}; - - internal fun render(renderSystem: RenderSystem, screenManager: ScreenManager) { - componentQueue.forEach { it.apply(components) } - componentQueue.clear() - menuQueue.forEach { it.apply(menus) } - menuQueue.clear() - components.forEach { it.render(renderSystem) } - } - - /** - * Cleans up and closes any used resource here. - */ - abstract fun exit() -} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gms/ScreenManager.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gms/ScreenManager.kt deleted file mode 100644 index f618d3ee..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gms/ScreenManager.kt +++ /dev/null @@ -1,156 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.mui.gms - -import net.terramodulus.mui.gfx.RenderSystem -import net.terramodulus.mui.gms.impl.LaunchingScreen -import net.terramodulus.mui.input.InputSystem - -class ScreenManager internal constructor(private val renderSystemHandle: RenderSystem.Handle) { - /** - * FILO screen stack; the top-most screen instance is in the last. - */ - private val screens = ArrayDeque() - private val screenQueue = ArrayDeque() - val handle: Handle = HandleImpl() - - init { - screens.add(LaunchingScreen(renderSystemHandle)) - } - - private sealed interface ScreenOperation { - fun apply(handle: RenderSystem.Handle, screens: ArrayDeque) - - /** - * Exits `n` times - * - * @throws IllegalArgumentException when `n` < 1 - * @throws IllegalStateException when `n` >= [screens] size during operation - */ - class Exit(val n: Int) : ScreenOperation { - init { - require(n < 0) { "`n` < 1" } - } - - override fun apply(handle: RenderSystem.Handle, screens: ArrayDeque) { - if (n >= screens.size) { - throw IllegalStateException("`n` >= screens.size") - } - - for (i in 1..n) { - screens.removeLast().exit() - } - } - } - - /** - * Opens the `screen` - */ - class Open(val screen: (RenderSystem.Handle) -> Screen) : ScreenOperation { - override fun apply(handle: RenderSystem.Handle, screens: ArrayDeque) { - screens.addLast(screen(handle)) - } - } - - /** - * Opens the `screen` before the `target` screen - */ - class OpenBefore(val screen: (RenderSystem.Handle) -> Screen, val target: Screen) : ScreenOperation { - override fun apply(handle: RenderSystem.Handle, screens: ArrayDeque) { - screens.add(screens.lastIndexOf(target), screen(handle)) - } - } - - /** - * Exits until reaching the `screen` then remains on the `screen` - */ - class ExitTo(val screen: Screen) : ScreenOperation { - override fun apply(handle: RenderSystem.Handle, screens: ArrayDeque) { - val it = screens.asReversed().listIterator() - while (it.hasNext()) { - val e = it.next() - if (e == screen) { - break - } else { - it.remove() - e.exit() - } - } - } - } - - /** - * Clears [screens] then opens the `screen` - */ - class Reset(val screen: (RenderSystem.Handle) -> Screen) : ScreenOperation { - override fun apply(handle: RenderSystem.Handle, screens: ArrayDeque) { - screens.asReversed().forEach { it.exit() } - screens.clear() - screens.add(screen(handle)) - } - } - } - - sealed interface Handle { - /** - * @see ScreenOperation.Exit - */ - fun exit(n: Int) - - /** - * @see ScreenOperation.Open - */ - fun open(screen: (RenderSystem.Handle) -> Screen) - - /** - * It is not recommended to use this in general scenarios. - * @see ScreenOperation.OpenBefore - */ - fun openBefore(screen: (RenderSystem.Handle) -> Screen, target: Screen) - - /** - * @see ScreenOperation.ExitTo - */ - fun exitTo(screen: Screen) - - /** - * @see ScreenOperation.Reset - */ - fun reset(screen: (RenderSystem.Handle) -> Screen) - } - - private inner class HandleImpl : Handle { - override fun exit(n: Int) { - screenQueue.add(ScreenOperation.Exit(n)) - } - - override fun open(screen: (RenderSystem.Handle) -> Screen) { - screenQueue.add(ScreenOperation.Open(screen)) - } - - override fun openBefore(screen: (RenderSystem.Handle) -> Screen, target: Screen) { - screenQueue.add(ScreenOperation.OpenBefore(screen, target)) - } - - override fun exitTo(screen: Screen) { - screenQueue.add(ScreenOperation.ExitTo(screen)) - } - - override fun reset(screen: (RenderSystem.Handle) -> Screen) { - screenQueue.add(ScreenOperation.Reset(screen)) - } - } - - internal fun update(renderSystem: RenderSystem, inputSystem: InputSystem) { - screens.forEach { it.update(renderSystem, this, inputSystem) } - } - - internal fun render(renderSystem: RenderSystem) { - screenQueue.forEach { it.apply(renderSystemHandle, screens) } - screenQueue.clear() - screens.forEach { it.render(renderSystem, this) } - } -} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gms/event/ComponentEvent.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gms/event/ComponentEvent.kt deleted file mode 100644 index 7383b352..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gms/event/ComponentEvent.kt +++ /dev/null @@ -1,9 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.mui.gms.event - -sealed interface ComponentEvent { -} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gms/event/MenuEvent.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gms/event/MenuEvent.kt deleted file mode 100644 index 2cdbc0b5..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gms/event/MenuEvent.kt +++ /dev/null @@ -1,9 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.mui.gms.event - -sealed interface MenuEvent { -} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gms/event/ScreenEvent.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gms/event/ScreenEvent.kt deleted file mode 100644 index ae203cae..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gms/event/ScreenEvent.kt +++ /dev/null @@ -1,11 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.mui.gms.event - -sealed interface ScreenEvent { - data object Open : ScreenEvent - data object Close : ScreenEvent -} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/BlankComponent.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/BlankComponent.kt deleted file mode 100644 index 129f679b..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/BlankComponent.kt +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package terramodulus.mui.gms.impl - -import net.terramodulus.mui.gfx.RenderSystem -import net.terramodulus.mui.gms.Component - -/** - * This can act as a placeholder [Component] in a [Layout][terramodulus.mui.gms.Layout]. - */ -class BlankComponent : Component() { - override fun render(renderSystem: RenderSystem) {} -} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/FlexibleBoxLayout.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/FlexibleBoxLayout.kt deleted file mode 100644 index 421b08d2..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/FlexibleBoxLayout.kt +++ /dev/null @@ -1,20 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.mui.gms.impl - -import net.terramodulus.mui.gfx.RectangleF -import net.terramodulus.mui.gms.Component -import net.terramodulus.mui.gms.Container -import net.terramodulus.mui.gms.Layout - -class FlexibleBoxLayout(container: Container) : Layout(container) { - override val components: Iterable - get() = TODO("Not yet implemented") - - override fun layout(rect: RectangleF) { - TODO("Not yet implemented") - } -} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/GameplayScreen.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/GameplayScreen.kt deleted file mode 100644 index e704c474..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/GameplayScreen.kt +++ /dev/null @@ -1,339 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.mui.gms.impl - -import net.terramodulus.core.TerraModulus -import net.terramodulus.core.getResourceAsString -import net.terramodulus.engine.Camera3D -import net.terramodulus.engine.PhyBody -import net.terramodulus.engine.PhyGeom -import net.terramodulus.engine.Quat -import net.terramodulus.engine.Rgba -import net.terramodulus.engine.SimpleMesh3dGeomCube -import net.terramodulus.engine.SimpleMesh3dGeomSphere -import net.terramodulus.engine.Vec3D -import net.terramodulus.engine.Vec3F -import net.terramodulus.engine.WorldObjDrawable -import net.terramodulus.mui.gfx.Direction6C -import net.terramodulus.mui.gfx.RenderSystem -import net.terramodulus.mui.gfx.Vector3D -import net.terramodulus.mui.gms.Component -import net.terramodulus.mui.gms.Screen -import net.terramodulus.mui.gms.ScreenManager -import net.terramodulus.mui.input.InputSystem -import net.terramodulus.util.logging.logger -import net.terramodulus.void.World -import kotlin.math.PI -import kotlin.math.sqrt -import kotlin.random.Random - -private val WHITE = Rgba(255, 255, 255, 255) -private val RED = Rgba(255, 0, 0, 255) -private val GREEN = Rgba(0, 255, 0, 255) -private val BLUE = Rgba(0, 0, 255, 255) -private val STD_SCALE = Vec3D(.5, .5, .5) -private val IDENT_ROT = Quat(1.0, .0, .0, .0) -private const val MASS = 1.0 -private const val MAX_SPEED = PI * PI // reachable by autonomous movement -private const val MAX_ACC = PI * PI // without other forces, reaching MAX_SPEED in one second -private const val MOVE_EPSILON = .1 // smallest acc to apply -private const val MIN_GRAVITY = 1.0 -private const val MAX_GRAVITY = 20.0 -private const val MIN_FRICTION = 1.0 / 16.0 -private const val MAX_FRICTION = 64.0 -private const val MIN_ZOOM = 1.0 / 4.0 -private const val MAX_ZOOM = 4 - -private val logger = logger {} - -internal class GameplayScreen(private val core: TerraModulus, private val camera: Camera3D, renderSystemHandle: RenderSystem.Handle) : Screen() { - private val geoShaders = camera.loadGeoShaders( - getResourceAsString("/gwr_geo.vsh"), - getResourceAsString("/gwr_geo.fsh"), - ) - - private lateinit var player: PlayerVoidGeom - - init { - renderSystemHandle.setBackgroundColor(0F, 0F, 0F, 0F) - core.world = World(Ymir()) - addComponent(GameplayRenderer()) -// val progressBarEdge = GeomComponent(GuiLine(0, 100, 100, 100, 255, 255, 255, 255)) -// addComponent(progressBarEdge) -// val progressBarCtnVal = GuiLine(0, 101, 0, 101, 255, 255, 0, 255) -// val progressBarCtn = GeomComponent(progressBarCtnVal) -// addComponent(progressBarCtn) -// class Tracker : World.ProgressTracker { -// override val progress: AtomicInteger = AtomicInteger(0) -// override val max: AtomicInteger = AtomicInteger(0) -// override fun update() { -// progressBarCtnVal.setPos(0, 101, 100 * progress.get() / max.get(), 101) -// } -// } -// Thread { -// core.world = World(Tracker(), Ymir()) -// removeComponent(progressBarEdge) -// removeComponent(progressBarCtn) -// addComponent(GameplayRenderer()) -// }.start() - } - - private inner class Ymir : World.Ymir { - override fun wrapCube(phyGeom: PhyGeom, x: Double, y: Double, z: Double): VoidGeom = EnvVoidGeom(phyGeom, - SimpleMesh3dGeomCube( - 2F, - randomColor(), - Vec3D(x, y, z), - STD_SCALE, - IDENT_ROT, - ), - Vec3D(x, y, z) - ) - - private fun randomColor() = when (Random.nextInt(3)) { - 0 -> RED - 1 -> GREEN - 2 -> BLUE - else -> throw AssertionError("Invalid color") - } - - override fun wrapChar(phyBody: PhyBody): VoidGeom { - player = PlayerVoidGeom(phyBody, - SimpleMesh3dGeomSphere(1F, WHITE, Vec3D(0.0, 1.0, 0.0), STD_SCALE, IDENT_ROT) - ) - return player - } - } - - private abstract inner class VoidGeom(val drawable: WorldObjDrawable) : World.VoidGeom { - override fun render() { - renderGwrGeo(drawable) - } - } - - private inner class EnvVoidGeom(override val phyGeom: PhyGeom, drawable: WorldObjDrawable, override val pos: Vec3D) : - VoidGeom(drawable), World.EnvVoidGeom - - private inner class PlayerVoidGeom(override val phyBody: PhyBody, drawable: WorldObjDrawable) : - VoidGeom(drawable), World.PlayerVoidGeom { - fun move(dir: Vector3D) { - if (dir == Vector3D.ZERO) return // avoid math errors and computations - val dir = Vec3D(dir.x, dir.y, dir.z).normalize() - val curVel = phyBody.linearVel - // Let d be the unit vector of autonomous movement target direction, - // v_c be the current velocity of body, - // v_p be the scalar projection of v_c on d. - // v_p = v_c * d, may be negative - // Autonomous acceleration is made only if v_p < MAX_SPEED. - val projVel = curVel * dir - if (projVel < MAX_SPEED) { - // Let v_d be the delta velocity in direction of d, - // a_d be the delta acceleration to be made. - // v_t = MAX_SPEED - v_p, must be positive - // a_d = dir * clamp(v_t / 1 s, EPSILON, MAX) - val deltaVel = MAX_SPEED - projVel - val deltaAcc = dir * deltaVel.coerceIn(MOVE_EPSILON, MAX_ACC) - phyBody.addForce(deltaAcc * MASS) - } - } - - override fun render() { - drawable.setPos(phyBody.pos) - camera.refreshPos(phyBody.pos.toVec3F().toArray()) - super.render() - } - - override var pos: Vec3D by phyBody::pos - } - - private fun Vec3D.normalize(): Vec3D { - val mag = mag() - return Vec3D(x / mag, y / mag, z / mag) - } - - private operator fun Vec3D.times(d: Double) = Vec3D(x * d, y * d, z * d) - private operator fun Vec3D.div(d: Double) = Vec3D(x / d, y / d, z / d) - private operator fun Vec3D.minus(other: Vec3D) = Vec3D(x - other.x, y - other.y, z - other.z) - // dot product - private operator fun Vec3D.times(other: Vec3D) = x * other.x + y * other.y + z * other.z - - // dot product with itself - private fun Vec3D.squared() = x * x + y * y + z * z - // magnitude or length - private fun Vec3D.mag() = sqrt(squared()) - - private fun Vec3D.toVec3F() = Vec3F(x.toFloat(), y.toFloat(), z.toFloat()) - - private fun Direction6C.toKey() = when (this) { - Direction6C.North -> InputSystem.Keys.W - Direction6C.South -> InputSystem.Keys.S - Direction6C.West -> InputSystem.Keys.A - Direction6C.East -> InputSystem.Keys.D - Direction6C.Up -> InputSystem.Keys.Space - Direction6C.Down -> InputSystem.Keys.LShift - } - - private fun Direction6C.toVector() = when (this) { - Direction6C.North -> Vector3D(.0, .0, -1.0) - Direction6C.South -> Vector3D(.0, .0, 1.0) - Direction6C.West -> Vector3D(-1.0, .0, .0) - Direction6C.East -> Vector3D(1.0, .0, .0) - Direction6C.Up -> Vector3D(.0, 1.0, .0) - Direction6C.Down -> Vector3D(.0, -1.0, .0) - } - - private fun Vec3D.display() = "[$x, $y, $z]" - - override fun update(renderSystem: RenderSystem, screenManager: ScreenManager, inputSystem: InputSystem) { - // Those keys are not related to GUI, so they are fine to be here. - if (inputSystem.condition { Q.justDown() }) { - // Query position of sphere - logger.info { "Position: ${player.pos.display()}" } - } - if (inputSystem.condition { R.justDown() }) { - // Query velocity of sphere - // Note: Acceleration is hard to be queried as force is zeroed after each world step - logger.info { "Velocity: ${player.phyBody.linearVel.display()}" } - } - if (inputSystem.condition { U.justDown() }) { - // Query gravity of world and gravity mode of (influence to) sphere - logger.info { "Gravity: ${core.world!!.gravity.display()}; influence: ${player.phyBody.gravityMode}" } - } - if (inputSystem.condition { I.justDown() }) { - // Toggle gravity mode of (influence to) sphere - player.phyBody.gravityMode = !player.phyBody.gravityMode - logger.info { "Gravity influence toggled: ${player.phyBody.gravityMode}" } - } - if (inputSystem.condition { O.justDown() }) { - // Increase world gravity - if (-core.world!!.gravity.y < MAX_GRAVITY) { - core.world!!.gravity *= 2.0 - logger.info { - "Gravity increased: ${core.world!!.gravity.display()}".let { - if (!player.phyBody.gravityMode) "$it (ineffective)" else it - } - } - } else { - logger.info { - "Gravity maximized: ${core.world!!.gravity.display()}".let { - if (!player.phyBody.gravityMode) "$it (ineffective)" else it - } - } - } - } - if (inputSystem.condition { P.justDown() }) { - // Decrease world gravity - if (-core.world!!.gravity.y > MIN_GRAVITY) { - core.world!!.gravity /= 2.0 - logger.info { - "Gravity decreased: ${core.world!!.gravity.display()}".let { - if (!player.phyBody.gravityMode) "$it (ineffective)" else it - } - } - } else { - logger.info { - "Gravity minimized: ${core.world!!.gravity.display()}".let { - if (!player.phyBody.gravityMode) "$it (ineffective)" else it - } - } - } - } - if (inputSystem.condition { J.justDown() }) { - // Query friction states - logger.info { "Friction: ${core.world!!.friction}; mode: ${core.world!!.frictionMode}" } - } - if (inputSystem.condition { K.justDown() }) { - // Toggle friction mode - core.world!!.frictionMode = World.FrictionMode.entries[ - (core.world!!.frictionMode.ordinal + 1) % World.FrictionMode.entries.size - ] - logger.info { - "Friction mode toggled: ${core.world!!.frictionMode}".let { - if (core.world!!.frictionMode == World.FrictionMode.Limited) "$it ; friction: ${core.world!!.friction}" else it - } - } - } - if (inputSystem.condition { L.justDown() }) { - // Increase friction (for Limited mode) - if (core.world!!.friction < MAX_FRICTION) { - core.world!!.friction *= 2 - logger.info { - "Friction increased: ${core.world!!.friction}".let { - if (core.world!!.frictionMode != World.FrictionMode.Limited) "$it (ineffective)" else it - } - } - } else { - logger.info { - "Friction maximized: ${core.world!!.friction}".let { - if (core.world!!.frictionMode != World.FrictionMode.Limited) "$it (ineffective)" else it - } - } - } - } - if (inputSystem.condition { M.justDown() }) { - // Decrease friction (for Limited mode) - if (core.world!!.friction > MIN_FRICTION) { - core.world!!.friction /= 2 - logger.info { - "Friction decreased: ${core.world!!.friction}".let { - if (core.world!!.frictionMode != World.FrictionMode.Limited) "$it (ineffective)" else it - } - } - } else { - logger.info { - "Friction minimized: ${core.world!!.friction}".let { - if (core.world!!.frictionMode != World.FrictionMode.Limited) "$it (ineffective)" else it - } - } - } - } - if (inputSystem.condition { N.justDown() }) { - // Reset velocity of sphere to zero - player.phyBody.linearVel = Vec3D.ZERO - logger.info { "Reset velocity to zero" } - } - // This is problematic and difficult to be resolved. -// if (inputSystem.condition { Z.justDown() }) { -// // Reset position of sphere to spawn point -// player.pos = Vec3D(0.0, 1.0, 0.0) -// logger.info { "Reset position to spawn point" } -// } - if (inputSystem.condition { Equals.justDown() }) { - // Zoom in camera - if (camera.zoomLevel < MAX_ZOOM) { - camera.zoomLevel *= 2 - logger.info { "Zoomed in: ${camera.zoomLevel}" } - } else { - logger.info { "Zoom maximized: ${camera.zoomLevel}" } - } - } - if (inputSystem.condition { Minus.justDown() }) { - // Zoom out camera - if (camera.zoomLevel > MIN_ZOOM) { - camera.zoomLevel /= 2 - logger.info { "Zoomed out: ${camera.zoomLevel}" } - } else { - logger.info { "Zoom minimized: ${camera.zoomLevel}" } - } - } - - val dirs = ArrayList() - Direction6C.entries.forEach { if (inputSystem.condition { it.toKey().down() }) dirs.add(it.toVector()) } - player.move(dirs.fold(Vector3D.ZERO, Vector3D::plus)) - } - - private inner class GameplayRenderer : Component() { - override fun render(renderSystem: RenderSystem) { - if (core.world != null) core.world!!.objects.values.sortedWith( - compareBy { it.pos.y }.thenBy { it.pos.z } - ).forEach { it.render() } - } - } - - internal fun renderGwrGeo(drawable: WorldObjDrawable) = camera.renderGwrGeo(drawable, geoShaders) - - override fun exit() {} -} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/GeomComponent.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/GeomComponent.kt deleted file mode 100644 index a1ed2188..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/GeomComponent.kt +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.mui.gms.impl - -import net.terramodulus.mui.gfx.GuiGeometry -import net.terramodulus.mui.gfx.RenderSystem -import net.terramodulus.mui.gms.Component - -class GeomComponent(val geom: GuiGeometry) : Component() { - override fun render(renderSystem: RenderSystem) { - geom.render(renderSystem) - } -} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/GraphicsComponent.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/GraphicsComponent.kt deleted file mode 100644 index 2d8aa37f..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/GraphicsComponent.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.mui.gms.impl - -import net.terramodulus.mui.gfx.GuiSprite -import net.terramodulus.mui.gfx.RenderSystem -import net.terramodulus.mui.gms.AbstractPanel -import net.terramodulus.mui.gms.Component - -sealed interface GraphicsComponent - -class SpriteComponent(val sprite: GuiSprite) : Component(), GraphicsComponent { - override fun render(renderSystem: RenderSystem) { - sprite.render(renderSystem) - } -} - -@Suppress("CanSealedSubClassBeObject") -class CanvasComponent : AbstractPanel(), GraphicsComponent { - override fun render(renderSystem: RenderSystem) { - TODO("Not yet implemented") - } -} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/LaunchingScreen.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/LaunchingScreen.kt deleted file mode 100644 index 3442eef2..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/LaunchingScreen.kt +++ /dev/null @@ -1,79 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.mui.gms.impl - -import net.terramodulus.mui.gfx.AlphaFilter -import net.terramodulus.mui.gfx.Dimension2I -import net.terramodulus.mui.gfx.FullScaling -import net.terramodulus.mui.gfx.GuiRect -import net.terramodulus.mui.gfx.GuiSprite -import net.terramodulus.mui.gfx.RectangleI -import net.terramodulus.mui.gfx.RenderSystem -import net.terramodulus.mui.gfx.SmartScaling -import net.terramodulus.mui.gms.Screen -import net.terramodulus.mui.gms.ScreenManager -import net.terramodulus.mui.input.InputSystem - -private val REF_SIZE = Dimension2I(800, 480) - -private val BG_COLOR = floatArrayOf(.145F, .776F, .768F) - -private const val ANI_DURATION = .75F // in second - -private const val PAUSE_DURATION = 2 // in second - -internal class LaunchingScreen(renderSystemHandle: RenderSystem.Handle) : Screen() { - private var stage = 0 - private var last = System.currentTimeMillis() // timestamp in milliseconds - private var alphaFilter = AlphaFilter(0F) - - init { - GeomComponent(GuiRect(0, 0, 800, 480, 37, 198, 196, 255)).apply { - geom.add(alphaFilter) - geom.add(FullScaling(REF_SIZE)) - addComponent(this) - } - SpriteComponent(GuiSprite( - RectangleI(0, 0, 512, 128), - renderSystemHandle.loadTexture("/studio_logo.png"), - )).apply { - sprite.add(alphaFilter) - sprite.add(SmartScaling.both(REF_SIZE.width, REF_SIZE.height, 512, 128)) - addComponent(this) - } - } - - override fun update(renderSystem: RenderSystem, screenManager: ScreenManager, inputSystem: InputSystem) { - val current = System.currentTimeMillis() - val elapsed = (current - last) / 1000F // elapsed time for this stage - when (stage) { - 0 -> if (elapsed >= ANI_DURATION) { - stage = 1 - last = current - alphaFilter.alpha = 1F - } else { - alphaFilter.alpha = elapsed / ANI_DURATION - } - - 1 -> if (elapsed >= PAUSE_DURATION) { - stage = 2 - last = current - } - - 2 -> if (elapsed >= ANI_DURATION) { - stage = 3 - last = current - alphaFilter.alpha = 0F - } else { - alphaFilter.alpha = 1 - elapsed / ANI_DURATION - } - - 3 -> screenManager.handle.reset(::ResourceLoadingScreen) - } - } - - override fun exit() {} -} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/PositioningComponent.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/PositioningComponent.kt deleted file mode 100644 index 729b19be..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/PositioningComponent.kt +++ /dev/null @@ -1,15 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package terramodulus.mui.gms.impl - -import net.terramodulus.mui.gfx.RenderSystem -import net.terramodulus.mui.gms.Component - -class PositioningComponent : Component() { - override fun render(renderSystem: RenderSystem) { - TODO("Not yet implemented") - } -} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/ResourceLoadingScreen.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/ResourceLoadingScreen.kt deleted file mode 100644 index e62d091b..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/ResourceLoadingScreen.kt +++ /dev/null @@ -1,114 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.mui.gms.impl - -import net.terramodulus.mui.gfx.AlphaFilter -import net.terramodulus.mui.gfx.Dimension2I -import net.terramodulus.mui.gfx.FullScaling -import net.terramodulus.mui.gfx.GuiRect -import net.terramodulus.mui.gfx.GuiSprite -import net.terramodulus.mui.gfx.RectangleI -import net.terramodulus.mui.gfx.RenderSystem -import net.terramodulus.mui.gfx.SmartScaling -import net.terramodulus.mui.gfx.Vector3F -import net.terramodulus.mui.gms.Screen -import net.terramodulus.mui.gms.ScreenManager -import net.terramodulus.mui.input.InputSystem -import kotlin.math.max -import kotlin.math.min -import kotlin.properties.Delegates - -private val REF_SIZE = Dimension2I(800, 480) - -private val CONTENT_SIZE = Dimension2I(400, 200) - -private val BG_COLOR = floatArrayOf(.145F, .776F, 0.768F) - -private const val ANI_DURATION = 1F // in second -private const val PAUSE_DURATION = 2F // in second - -class ResourceLoadingScreen(renderSystemHandle: RenderSystem.Handle) : Screen() { - private var stage = 0 - private var last = System.currentTimeMillis() // timestamp in milliseconds - private var alphaFilter = AlphaFilter(0F) - private val progressBar = ProgressBar() - - init { - GeomComponent(GuiRect(0, 0, 800, 480, 0, 255, 213, 255)).apply { - geom.add(alphaFilter) - geom.add(FullScaling(REF_SIZE)) - addComponent(this) - } - val smartScaling = SmartScaling.both(REF_SIZE.width, REF_SIZE.height, CONTENT_SIZE.width, CONTENT_SIZE.height) - SpriteComponent(GuiSprite(RectangleI(0, 100, 400, 100), renderSystemHandle.loadTexture("/game_logo.png"))).apply { - sprite.add(alphaFilter) - sprite.add(smartScaling) - addComponent(this) - } - GeomComponent(GuiRect(0, 0, 400, 40, 240, 240, 240, 255)).apply { - geom.add(alphaFilter) - geom.add(smartScaling) - addComponent(this) - } - GeomComponent(GuiRect(5, 5, 395, 35, 0, 255, 213, 255)).apply { - geom.add(alphaFilter) - geom.add(smartScaling) - addComponent(this) - } - GeomComponent(progressBar.rect).apply { - geom.add(alphaFilter) - geom.add(smartScaling) - addComponent(this) - } - } - - private class ProgressBar { - val rectDim = RectangleI.withPoints(7, 7, 393, 33) - val length = rectDim.width - var progress: Float by Delegates.observable(0f) { _, _, _ -> - rect.setPos(7, 7, rectDim.x + (progress * length).toInt(), 33) - } - val rect = GuiRect(7, 7, 7, 33, 240, 240, 240, 255) - } - - override fun update(renderSystem: RenderSystem, screenManager: ScreenManager, inputSystem: InputSystem) { - val current = System.currentTimeMillis() - val elapsed = (current - last) / 1000F // elapsed time in second at this stage - when (stage) { - 0 -> if (elapsed >= ANI_DURATION) { - stage = 1 - last = current - alphaFilter.alpha = 1F - } else { - alphaFilter.alpha = elapsed / ANI_DURATION - } - - 1 -> { - // TODO when there is something to load, stay at this stage until ready - progressBar.progress = min(elapsed / PAUSE_DURATION, 1F) - if (progressBar.progress >= 1F) { - stage = 2 - last = current - } - } - - 2 -> if (elapsed >= ANI_DURATION) { - stage = 3 - last = current - alphaFilter.alpha = 0F - } else { - alphaFilter.alpha = 1 - elapsed / ANI_DURATION - } - -// 3 -> screenManager.handle.openBefore(::TitleScreen, this) - 3 -> screenManager.handle.reset(renderSystem.newGameplayScreen(Vector3F.ZERO)) - } - } - - override fun exit() { - - } -} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/SequenceLayout.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/SequenceLayout.kt deleted file mode 100644 index 9d80c12c..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/SequenceLayout.kt +++ /dev/null @@ -1,93 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.mui.gms.impl - -import net.terramodulus.mui.gfx.RectangleF -import net.terramodulus.mui.gms.Component -import net.terramodulus.mui.gms.Container -import net.terramodulus.mui.gms.Layout - -/** - * Common implementation that is either [ColumnLayout] or [RowLayout]. - * - * This is an optimized special version of [FlexibleBoxLayout] without any expected - * multiple *sequences* of components in a single layout. - */ -sealed class SequenceLayout(container: Container, elements: ElementList) : - Layout.ElementGroup(container, elements) { - class Element { - companion object { - fun default() = Element() - } - } - - override fun add(component: Component) = elements.add(component, Element.default()) - - override fun addBefore(target: Component, component: Component) = - elements.addBefore(target, component, Element.default()) - - override fun addAfter(target: Component, component: Component) = - elements.addAfter(target, component, Element.default()) - - override fun replace(target: Component, component: Component) = - elements.replace(target, component, Element.default()) -} - -/** - * **Column** case of [SequenceLayout]. - */ -class ColumnLayout private constructor(container: Container, elements: ElementList) : - SequenceLayout(container, elements) { - companion object { - fun withComponents(vararg components: Component) = { it: Container -> - ColumnLayout(it, ElementList.withComponentsDefault(Element::default, *components)) - } - - fun withComponents(components: Collection) = { it: Container -> - ColumnLayout(it, ElementList.withComponentsDefault(Element::default, components)) - } - - fun withElements(vararg elements: Pair) = { it: Container -> - ColumnLayout(it, ElementList.withElements(*elements)) - } - - fun withElements(elements: Map) = { it: Container -> - ColumnLayout(it, ElementList.withElements(elements)) - } - } - - override fun layout(rect: RectangleF) { - elements.forEach { TODO() } - } -} - -/** - * **Row** case of [SequenceLayout]. - */ -class RowLayout private constructor(container: Container, elements: ElementList) : - SequenceLayout(container, elements) { - companion object { - fun withComponents(vararg components: Component) = { it: Container -> - RowLayout(it, ElementList.withComponentsDefault(Element::default, *components)) - } - - fun withComponents(components: Collection) = { it: Container -> - RowLayout(it, ElementList.withComponentsDefault(Element::default, components)) - } - - fun withElements(vararg elements: Pair) = { it: Container -> - RowLayout(it, ElementList.withElements(*elements)) - } - - fun withElements(elements: Map) = { it: Container -> - RowLayout(it, ElementList.withElements(elements)) - } - } - - override fun layout(rect: RectangleF) { - elements.forEach { TODO() } - } -} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/TitleScreen.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/TitleScreen.kt deleted file mode 100644 index a13d8e02..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/TitleScreen.kt +++ /dev/null @@ -1,13 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.mui.gms.impl - -import net.terramodulus.mui.gfx.RenderSystem -import net.terramodulus.mui.gms.Screen - -class TitleScreen(renderSystemHandle: RenderSystem.Handle) : Screen() { - override fun exit() {} -} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/GuiManager.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/GuiManager.kt new file mode 100644 index 00000000..7207701b --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/GuiManager.kt @@ -0,0 +1,59 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui + +import net.terramodulus.core.TerraModulus +import net.terramodulus.engine.Window +import net.terramodulus.mui.MuiManager +import net.terramodulus.mui.gui.agim.LayoutManager +import net.terramodulus.mui.gui.gfx.RenderSystem +import net.terramodulus.mui.gui.agim.ScreenManager +import net.terramodulus.mui.gui.asd.AsdManager +import net.terramodulus.util.logging.logger +import java.io.Closeable + +private val logger = logger {} + +/** + * Graphical User Interface (GUI) Manager + */ +internal class GuiManager internal constructor(private val window: Window, core: TerraModulus) : Closeable { + val renderSystem = RenderSystem(core, window.canvas) + val asdManager = AsdManager() + val inputStatesHandle = InputStatesHandle() + val screenManager = ScreenManager(window, renderSystem.handle, asdManager.AgimHandle(), inputStatesHandle) + val layoutManager = LayoutManager(screenManager) + + private var proceeded = false + + /** + * Screen updating, targeting as the same as *maximum FPS*, + * but the numbers of ticks are not supposed to be compensated when missed, + * so it is up to the callers to compensate missed activities. + */ + internal fun updateScreens(muiManager: MuiManager) { + screenManager.update(muiManager) + if (!proceeded) { + asdManager.process() + proceeded = true + } + layoutManager.tick() + } + + /** + * Canvas updating, per frame, maximally the *maximum FPS*. + * This includes input ticking and canvas rendering. + */ + internal fun updateCanvas() { + window.canvas.clear() + screenManager.render(renderSystem) + window.swap() + } + + override fun close() { + window.close() + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputCtxStates.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputCtxStates.kt new file mode 100644 index 00000000..722d1b77 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputCtxStates.kt @@ -0,0 +1,50 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui + +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.gfx.RectRange +import java.io.Closeable + +sealed class InputCtxStates( + protected val globalStates: InputGlobalStates, + protected val asdHandle: AsdHandle, +) : Closeable { + protected val listeners = mutableSetOf>() + val ctxRange: CtxRange by lazy { CtxRange() } + + inner class CtxRange : Closeable { + lateinit var rect: RectRange + private set + private val listener = { + rect = RectRange.range(asdHandle.rect) + }.also(asdHandle::observeRect) + + init { + try { + rect = RectRange.range(asdHandle.rect) + } catch (_: UninitializedPropertyAccessException) {} + } + + override fun close() { + asdHandle.unobserveRect(listener) + } + } + + fun addListener(listener: InputState.Listener) { + listeners.add(listener) + globalStates.addListener(listener) + } + + fun removeListener(listener: InputState.Listener) { + listeners.remove(listener) + globalStates.removeListener(listener) + } + + override fun close() { + listeners.forEach { globalStates.removeListener(it) } + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputGlobalStates.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputGlobalStates.kt new file mode 100644 index 00000000..88239010 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputGlobalStates.kt @@ -0,0 +1,29 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui + +sealed class InputGlobalStates { + protected val triggers = mutableMapOf>>() + protected val listeners = mutableMapOf, InputState.Listener>() + + internal fun addListener(listener: InputState.Listener) { + listener.triggers.forEach { + listeners[it] = listener + triggers.computeIfAbsent(it.key) { mutableSetOf() }.add(it) + } + } + + internal fun removeListener(listener: InputState.Listener) { + listener.triggers.forEach { + listeners.remove(it) + triggers[it.key]!!.remove(it) + } + } + + internal fun triggerListeners(key: K, state: S) { + triggers[key]?.forEach { if (it.check(state)) listeners[it]!!.act(state) } + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputState.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputState.kt new file mode 100644 index 00000000..a91c3615 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputState.kt @@ -0,0 +1,29 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui + +/** + * It is unlikely that local triggers being unaffected by other factors, + * so this should match any range of inputs while this may also filter for specific triggers. + * + * Likely be implemented as simple data classes. + */ +sealed class InputState { + sealed interface Listener { + val triggers: Set> + + fun act(state: S) + } + + sealed interface Trigger { + /** + * Shall be implemented as simple data classes supporting equality. + */ + val key: K + + fun check(state: S): Boolean + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputStatesHandle.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputStatesHandle.kt new file mode 100644 index 00000000..013fdf52 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputStatesHandle.kt @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui + +import com.cout970.math.vec2.Vec2f +import net.terramodulus.mui.kui.InputSystem.InputEvent + +class InputStatesHandle { + val mouseGlobalStates = MouseGlobalStates() + + internal fun update(events: Sequence, mousePos: Vec2f) { + mouseGlobalStates.update(events.filterIsInstance().map { it.inner }, mousePos) + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/MouseCtxStates.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/MouseCtxStates.kt new file mode 100644 index 00000000..c04cc7eb --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/MouseCtxStates.kt @@ -0,0 +1,33 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui + +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.uid.MouseDevice + +class MouseCtxStates(globalStates: MouseGlobalStates, asdHandle: AsdHandle) : + InputCtxStates(globalStates, asdHandle) { + inline fun listenRectFullClick(buttonId: MouseDevice.ButtonId, crossinline action: () -> Unit): MouseState.Listener { + var clicked = false + return MouseState.Listener(setOf( + MouseState.Trigger(MouseState.Key.ButtonJustDown(buttonId)) { true }, + MouseState.Trigger(MouseState.Key.ButtonJustUp(buttonId)) { true }, + )) { + when (it) { + is MouseState.ButtonJustDown -> { + assert(it.id == buttonId) + clicked = ctxRange.rect.contains(it.pos) + } + is MouseState.ButtonJustUp -> { + assert(it.id == buttonId) + if (clicked && ctxRange.rect.contains(it.pos)) action() + clicked = false + } + else -> throw UnsupportedOperationException() + } + } + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/MouseGlobalStates.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/MouseGlobalStates.kt new file mode 100644 index 00000000..0903d8a0 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/MouseGlobalStates.kt @@ -0,0 +1,38 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui + +import com.cout970.math.vec2.ImmVec2d +import com.cout970.math.vec2.Vec2f +import net.terramodulus.mui.kui.MouseInputHandler + +class MouseGlobalStates : InputGlobalStates() { + internal fun update(events: Sequence, mousePos: Vec2f) { + val mousePos = ImmVec2d(mousePos.xd, mousePos.yd) + events.forEach { + when (it) { + is MouseInputHandler.Event.Button -> { + when (it) { + is MouseInputHandler.Event.Button.Down -> triggerListeners( + MouseState.Key.ButtonJustDown(it.key), + MouseState.ButtonJustDown(it.key, mousePos), + ) + is MouseInputHandler.Event.Button.Up -> triggerListeners( + MouseState.Key.ButtonJustUp(it.key), + MouseState.ButtonJustUp(it.key, mousePos), + ) + } + } + is MouseInputHandler.Event.Movement -> { + triggerListeners( + MouseState.Key.Movement, + MouseState.Movement(it.delX.toDouble(), it.delY.toDouble(), mousePos), + ) + } + } + } + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/MouseState.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/MouseState.kt new file mode 100644 index 00000000..fd804e17 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/MouseState.kt @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui + +import com.cout970.math.vec2.Vec2d +import net.terramodulus.mui.uid.MouseDevice +import kotlin.time.Duration + +sealed class MouseState : InputState() { + sealed class Key private constructor() { + data object Movement : Key() + data class ButtonJustDown(val id: MouseDevice.ButtonId) : Key() + data class ButtonJustUp(val id: MouseDevice.ButtonId) : Key() + data object ButtonKeepDown : Key() + data object WheelYNeg : Key() + data object WheelYPos : Key() + data object WheelXNeg : Key() + data object WheelXPos : Key() + } + + data class Movement(val delX: Double, val delY: Double, val pos: Vec2d) : MouseState() + data class ButtonJustDown(val id: MouseDevice.ButtonId, val pos: Vec2d) : MouseState() + data class ButtonJustUp(val id: MouseDevice.ButtonId, val pos: Vec2d) : MouseState() + data class ButtonKeepDown(val dur: Duration, val pos: Vec2d) : MouseState() + data class WheelYMotion(val delta: Double, val pos: Vec2d) : MouseState() + data class WheelXMotion(val delta: Double, val pos: Vec2d) : MouseState() + + class Listener(override val triggers: Set, private val action: (MouseState) -> Unit) : + InputState.Listener { + override fun act(state: MouseState) = action(state) + } + + class Trigger(override val key: Key, private val condition: (MouseState) -> Boolean) : + InputState.Trigger { + override fun check(state: MouseState) = condition(state) + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/AbstractPane.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/AbstractPane.kt new file mode 100644 index 00000000..28a4b261 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/AbstractPane.kt @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim + +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.asd.AsdProcessor +import net.terramodulus.mui.gui.gfx.RectangleD +import net.terramodulus.mui.gui.gfx.RectangleF + +abstract class AbstractPane(asdHandle: AsdHandle) : Component(asdHandle), Container { + protected inner class ComponentAsdHandleImpl : AsdHandle.Container() { + override lateinit var rect: RectangleD + override fun registerAsdProcessor(processor: AsdProcessor<*>) = asdHandle.registerAsdProcessor(processor) + } + + final override fun update(muiIoI: ScreenManager.MuiIoI) { + super.update(muiIoI) + layout.update() + layout.components.forEach { it.update(muiIoI) } + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/AgimoProperty.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/AgimoProperty.kt new file mode 100644 index 00000000..4ec774df --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/AgimoProperty.kt @@ -0,0 +1,105 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim + +import net.terramodulus.util.TypedMap + +/** + * Instances of this class must be immutable, as simple values; + * may be data classes, data objects, enums. + */ +abstract class AgimoProperty + +fun T.getPropertyKey() = AgimoPropertyMap.Key(javaClass) + +class AgimoPropertyMap { + private val properties = TypedMap>() + private val observers = HashMap, + LinkedHashSet<(AgimoProperty?, AgimoProperty?) -> Unit>>() + + class Key(c: Class) : TypedMap.Key(c) + + @Suppress("UNCHECKED_CAST") + fun getProperty(key: Key) = properties[key] as T? + + fun putProperty(key: Key, value: T) { + @Suppress("UNCHECKED_CAST") + val old = properties.put(key, value) as T? + triggerPropertyObservers(key, old, value) + } + + inline fun putProperty(value: T) = putProperty(value.getPropertyKey(), value) + + @Suppress("UNCHECKED_CAST") + fun removeProperty(key: Key): T? { + val old = properties.remove(key) as T? + triggerPropertyObservers(key, old, null) + return old + } + + fun containsProperty(key: Key) = properties.containsKey(key) + + /** + * @see java.util.Map.compute + */ + inline fun computeProperty(key: Key, remappingFunction: (Key, T?) -> T?) { + val v = remappingFunction(key, getProperty(key)) + if (v === null) removeProperty(key) else putProperty(key, v) + } + + /** + * @see java.util.Map.computeIfAbsent + */ + inline fun computePropertyIfAbsent(key: Key, mappingFunction: (Key) -> T?) { + if (!containsProperty(key)) { + val v = mappingFunction(key) + if (v !== null) putProperty(key, v) + } + } + + /** + * @see java.util.Map.computeIfPresent + */ + inline fun computePropertyIfPresent(key: Key, remappingFunction: (Key, T) -> T?) { + val v = getProperty(key) + if (v !== null) { + val v = remappingFunction(key, v) + if (v !== null) putProperty(key, v) + } + } + + /** + * @see java.util.Map.merge + */ + inline fun mergeProperty(key: Key, value: T, remappingFunction: (T, T) -> T?) { + val v = getProperty(key) + if (v !== null) { + val v = remappingFunction(v, value) + if (v !== null) putProperty(key, v) + } else putProperty(key, value) + } + + fun observeProperty(key: Key, l: (T?, T?) -> Unit) { + @Suppress("UNCHECKED_CAST") + observers.computeIfAbsent(key) { LinkedHashSet() }.add(l as (AgimoProperty?, AgimoProperty?) -> Unit) + } + + fun unobserveProperty(key: Key, l: (T?, T?) -> Unit) { + observers[key]?.remove(l) + } + + private fun triggerPropertyObservers(key: Key, old: T?, new: T?) { + observers[key]?.forEach { it(old, new) } + } + + @Suppress("UNCHECKED_CAST") + fun asMap(): Map, AgimoProperty> = properties as Map, AgimoProperty> + +} + +@Suppress("UNCHECKED_CAST") +fun Map, AgimoProperty>.getProperty(key: AgimoPropertyMap.Key) = + this[key] as T? diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/AgimoTreeVisitor.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/AgimoTreeVisitor.kt new file mode 100644 index 00000000..c16ce7ac --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/AgimoTreeVisitor.kt @@ -0,0 +1,123 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim + +import java.util.LinkedList + +internal abstract class AgimoTreeVisitor { + internal fun interface ScreenTreeVisitor { + fun visit(): Sequence + } + + internal fun interface MenuTreeVisitor { + fun visit(): Sequence + } + + class RootNode(screens: Sequence, menus: Sequence) { + val screens = ScreenTree(screens) + val menus = MenuTree(menus) + } + + sealed class ContainerNode(val layout: Layout) { + val elements = LinkedList(layout.components.map { + if (it is AbstractPane) PaneNode(it) else SimpleComponentNode(it) + }.toList()) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ContainerNode + + if (layout != other.layout) return false + if (elements != other.elements) return false + + return true + } + + override fun hashCode(): Int { + var result = layout.hashCode() + result = 31 * result + elements.hashCode() + return result + } + } + + sealed interface ComponentNode { + val component: Component + } + + class SimpleComponentNode(override val component: Component) : ComponentNode { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SimpleComponentNode + + return component == other.component + } + + override fun hashCode(): Int { + return component.hashCode() + } + } + + class PaneNode(override val component: AbstractPane) : ContainerNode(component.layout), ComponentNode { + override fun equals(other: Any?): Boolean { + return super.equals(other) && component == (other as PaneNode).component + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + component.hashCode() + return result + } + } + + class ScreenTree(screens: Sequence) { + val list = LinkedList(screens.map { ScreenNode(it) }.toList()) + } + + class ScreenNode(val screen: Screen) : ContainerNode(screen.layout) { + val menus = MenuTree(screen.visit().visit()) + + override fun equals(other: Any?): Boolean { + return super.equals(other) && screen == (other as ScreenNode).screen && menus == other.menus + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + screen.hashCode() + result = 31 * result + menus.hashCode() + return result + } + } + + class MenuTree(menus: Sequence) { + val list = LinkedList(menus.map { MenuNode(it) }.toList()) + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as MenuTree + + return list == other.list + } + + override fun hashCode() = list.hashCode() + } + + class MenuNode(val menu: Menu) : ContainerNode(menu.layout) { + override fun equals(other: Any?): Boolean { + return super.equals(other) && menu == (other as MenuNode).menu + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + menu.hashCode() + return result + } + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Alignment.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Alignment.kt new file mode 100644 index 00000000..d3b97649 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Alignment.kt @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim + +import com.cout970.math.vec2.ImmVec2d +import com.cout970.math.vec2.Vec2d +import com.cout970.math.vec2.minus +import com.cout970.math.vec2.plus +import net.terramodulus.mui.gui.gfx.Anchor5 +import net.terramodulus.mui.gui.gfx.Dimension2D +import net.terramodulus.mui.gui.gfx.RectangleD + +const val ALIGN_START = 0.0 +const val ALIGN_CENTER = 0.5 +const val ALIGN_END = 1.0 + +/** + * `anchor` is relative position of positioning anchor from the anchor of the rectangle/dimension.; + * should be within the bounds of the rectangle/dimension. + */ +class AnchorAlignmentHelper { + companion object { + private operator fun RectangleD.times(other: Vec2d) = + ImmVec2d(width * other.x, height * other.y) + private operator fun Dimension2D.times(other: Vec2d) = + ImmVec2d(width * other.x, height * other.y) + + fun simple(subjectRect: RectangleD, targetDim: Dimension2D, alignment: Vec2d) = + Subject(subjectRect.toDouble(), subjectRect * alignment) + .alignTarget(Target(targetDim, targetDim * alignment)) + } + + data class Subject(val rect: RectangleD, val anchor: Vec2d) { + fun alignTarget(target: Target): RectangleD { + // This should be the anchor of the rectangle of target + val anchor = rect.anchor(Anchor5.BottomLeft) + anchor - target.anchor + return RectangleD(anchor.x, anchor.y, target.dim.width, target.dim.height) + } + } + + data class Target(val dim: Dimension2D, val anchor: Vec2d) +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gms/Component.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Component.kt similarity index 50% rename from src/kernel/client/kotlin/net/terramodulus/mui/gms/Component.kt rename to src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Component.kt index c67121d8..f4058b7c 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gms/Component.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Component.kt @@ -1,29 +1,28 @@ /* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors * SPDX-License-Identifier: LGPL-3.0-only */ -package net.terramodulus.mui.gms +package net.terramodulus.mui.gui.agim -import net.terramodulus.mui.gfx.ManagedRect -import net.terramodulus.mui.gfx.RenderSystem -import net.terramodulus.mui.gms.event.ComponentEvent -import net.terramodulus.mui.input.InputSystem +import net.terramodulus.mui.gui.agim.event.ComponentEvent +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.gfx.RenderSystem /** - * [Component] can only be contained by only one [Container]. + * [Component] can only be contained by only one [Container][net.terramodulus.mui.gui.agim.Container] at once. * * It is an undefined behavior when the `Component` is contained repeatedly * or in different containers simultaneously. */ -abstract class Component { +abstract class Component(open val asdHandle: AsdHandle) { private val listeners = HashMap, LinkedHashSet<(ComponentEvent) -> Unit>>() - /** - * This should only be modified by [Layout] managers. - */ - open lateinit var rect: ManagedRect - internal set +// /** +// * Caveat: This should only be modified by [Layout][net.terramodulus.mui.gui.agim.Layout] managers. +// */ +// open lateinit var layoutHandle: LayoutHandle +// internal set abstract fun render(renderSystem: RenderSystem) @@ -40,7 +39,7 @@ abstract class Component { listeners[event.javaClass]?.forEach { it(event) } } - fun update(inputSystem: InputSystem) { - TODO() + internal open fun update(muiIoI: ScreenManager.MuiIoI) { + dispatchEvent(ComponentEvent.Update(muiIoI)) } } diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Container.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Container.kt new file mode 100644 index 00000000..a80e16d8 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Container.kt @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim + +import net.terramodulus.mui.gui.asd.AsdHandle + +/** + * **AGIM Container**, direct subclasses are explicitly defined. + */ +sealed interface Container { + val asdHandle: AsdHandle//.Container + + val layout: Layout +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gms/Layout.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Layout.kt similarity index 70% rename from src/kernel/client/kotlin/net/terramodulus/mui/gms/Layout.kt rename to src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Layout.kt index 53e52ff9..e7992044 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gms/Layout.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Layout.kt @@ -1,13 +1,16 @@ /* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors * SPDX-License-Identifier: LGPL-3.0-only */ -package net.terramodulus.mui.gms +package net.terramodulus.mui.gui.agim -import net.terramodulus.mui.gfx.RectangleF -import net.terramodulus.mui.gms.impl.SequenceLayout.Element -import kotlin.reflect.KProperty +import com.cout970.math.vec2.ImmVec2i +import net.terramodulus.mui.gui.gfx.Dimension2I +import net.terramodulus.mui.gui.gfx.RenderSystem +import java.io.Closeable +import java.util.ArrayDeque +import kotlin.math.roundToInt /** * [Layout] is always mutable. @@ -15,40 +18,79 @@ import kotlin.reflect.KProperty * **Layout** is defined only when all its managed components all belong to the container * associated with this layout manager *exclusively*. */ -abstract class Layout(private val container: Container) { +abstract class Layout(protected val container: Container) : Closeable { companion object { - const val ALIGN_START = 0F; - const val ALIGN_CENTER = .5F; - const val ALIGN_END = 1F; + const val ALIGN_START = 0F + const val ALIGN_CENTER = .5F + const val ALIGN_END = 1F } - abstract val components: Iterable + abstract val components: Sequence - private val containerObserver = ::layout.apply(container.rect::observe) + private val layoutOperations = ArrayDeque() + + fun interface Operation { + fun Layout.operate() + } /** - * Updates the layout output using the current layout configurations - * by invoking [layout] internally. + * Query [Operation] on the [Layout] structures, potentially changing any components and configurations. * - * It is recommended to invoke this when this layout is being initialized - * or any layout configuration has been changed. + * It is required to use this when this layout is being initialized + * or any layout configuration is being changed. */ - fun update() = layout(container.rect.rect) + fun operate(operation: Operation) { + layoutOperations.add(operation) + } + + /** + * Updates the layout output using pending operations added via [operate], + * and the resultant layout configurations by invoking [layOut] internally if any operation exists. + */ + fun update() { + val nonEmpty = layoutOperations.isNotEmpty() + while (layoutOperations.isNotEmpty()) { + with(layoutOperations.removeFirst()) { this@Layout.operate() } + } + if (nonEmpty) updated = true + } + + /** + * Whether this [Layout] has been updated (as in [update]) at this moment. + * This should only be updated by [update] and [LayoutManager]. + */ + internal var updated = false /** * Lays out the managed [components] by this [Layout] manager. * - * Only the `rect`s of the managed `components` should be (re)assigned; - * no other state-changing operations should be done beside this. - * @param rect the rectangle of the container at this moment + * In most cases, only the `layoutHandle`s of the managed `components` should be (re)assigned. + * Otherwise, no other state-changing operations should be done beside this. */ - protected abstract fun layout(rect: RectangleF) + protected abstract fun layOut(handle: LayoutHandle): Sequence + + internal fun layOutInternal(handle: LayoutHandle) = layOut(handle) + + /** + * Renders this [Layout] with underlying managed [components]. + */ + internal fun render(renderSystem: RenderSystem) = components.forEach { + val rect = it.asdHandle.rect + renderSystem.handle.withScissor( + ImmVec2i(rect.x.roundToInt(), rect.y.roundToInt()), + Dimension2I(rect.width.toInt(), rect.height.toInt()), + ) { it.render(renderSystem) } + } /** * Must be invoked when this [Layout] is no longer in use. */ - fun clear() { - container.rect.unobserve(containerObserver) + fun clear() { // Not sure whether there is the necessity to separate this from [close]. +// container.asdHandle.unobserveRect(containerObserver) + } + + override fun close() { + clear() } /** @@ -93,9 +135,9 @@ abstract class Layout(private val container: Container) { abstract class ElementGroup protected constructor( container: Container, - protected val elements: ElementList, + protected open val elements: ElementList, ) : Group(container) { - final override val components = elements.componentsView + final override val components = elements.componentsView.asSequence() override fun contains(component: Component): Boolean = elements.contains(component) @@ -144,39 +186,18 @@ abstract class Layout(private val container: Container) { elements.replace(target, component, element) } - /** - * @param components must not be empty - */ - protected class ComponentIterable(private vararg val components: KProperty) : Iterable { - override fun iterator(): Iterator = object : Iterator { - private var index = 0 - - private fun untilNotNull(): Boolean { - do { - if (components[index].getter.call() != null) - return true - else - index++ - } while (index < components.size) - return false - } - - override fun hasNext(): Boolean = untilNotNull() + protected fun componentsNullableSequence(vararg components: () -> Component?) = + sequenceOf(*components).mapNotNull { it() } - override fun next(): Component = if (untilNotNull()) { - components[index].getter.call()!! - } else { - throw NoSuchElementException() - } - } - } + protected fun componentsSequence(vararg components: () -> Component) = + sequenceOf(*components).map { it() } /** * Internal list of the layout elements. */ protected class ElementList private constructor( - private val components: MutableList, // order is defined here - private val elementMap: MutableMap, // elements are mapped here + private val components: MutableList, // order is defined here + private val elementMap: MutableMap, // elements are mapped here ) : Iterable> { companion object { fun withComponentsDefault(default: () -> E, vararg components: Component): ElementList { diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/LayoutComputation.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/LayoutComputation.kt new file mode 100644 index 00000000..08c1a778 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/LayoutComputation.kt @@ -0,0 +1,47 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim + +import net.terramodulus.mui.gui.asd.AsdHandle + +class LayoutComputationUnit @PublishedApi internal constructor( + val computation: LayoutHandle.() -> Map, + val dependencies: Map>>, + val results: Map>>, +) { + companion object { + inline operator fun invoke( + dependencies: MutableMap>>.() -> Unit, + results: MutableMap>>.() -> Unit, + noinline computation: LayoutHandle.() -> Map, + ) = LayoutComputationUnit(computation, + mutableMapOf>>().apply(dependencies), + mutableMapOf>>().apply(results), + ) + } +} + +class LayoutComputationGroup @PublishedApi internal constructor( + val conditions: () -> Set, +// val conditions: LayoutHandle.() -> Set, + val dependencies: MutableMap>>, +) { + companion object { + inline operator fun invoke( + dependencies: MutableMap>>.() -> Unit, + noinline conditions: () -> Set, +// noinline conditions: LayoutHandle.() -> Set, + ) = LayoutComputationGroup(conditions, + mutableMapOf>>().apply(dependencies), + ) + } +} + +// class LayoutComputationDependency(val key: AgimoPropertyMap.Key<*>) { // Dependencies saved in a Set (immutable) +// } + +// class LayoutComputationResult() { // Results saved in a Map (immutable) +// } diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/LayoutHandle.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/LayoutHandle.kt new file mode 100644 index 00000000..334e8e21 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/LayoutHandle.kt @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim + +import net.terramodulus.mui.gui.asd.AsdHandle + +abstract class LayoutHandle internal constructor() { + abstract fun getUnit(handle: AsdHandle): Unit + + abstract class Unit internal constructor() { + abstract fun getProperty(key: AgimoPropertyMap.Key): T? + abstract fun containsProperty(key: AgimoPropertyMap.Key): Boolean + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/LayoutManager.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/LayoutManager.kt new file mode 100644 index 00000000..4e984063 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/LayoutManager.kt @@ -0,0 +1,389 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim + +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import net.terramodulus.mui.gui.agim.impl.BoundsProperty +import net.terramodulus.mui.gui.agim.impl.RectangleProperty +import net.terramodulus.mui.gui.asd.AsdHandle + +internal class LayoutManager(screenManager: ScreenManager) { + private val agimoTree = AgimoTree(screenManager) + + // TODO maybe later think of a way to dynamically update nodes partially + private class AgimoTree(screenManager: ScreenManager) : AgimoTreeVisitor() { + private val screenVisitor = screenManager.visitScreens() + private val menuVisitor = screenManager.visitMenus() + var root = RootNode(sequenceOf(), sequenceOf()) + + private fun update() { + root = RootNode(screenVisitor.visit(), menuVisitor.visit()) + } + + private fun listContainers(): Map { + val containers = mutableMapOf() + fun pushElements(node: ContainerNode) { + node.elements.forEach { + if (it is PaneNode) { + containers[it] = it.component.asdHandle + pushElements(it) + } + } + } + fun pushMenu(node: MenuNode) { + containers[node] = node.menu.asdHandle + pushElements(node) + } + fun pushScreen(node: ScreenNode) { + containers[node] = node.screen.asdHandle + node.menus.list.forEach(::pushMenu) + pushElements(node) + } + root.screens.list.forEach(::pushScreen) + root.menus.list.forEach(::pushMenu) + return containers + } + + fun discoverLayoutChanges(): LayoutChanges { + val old = listContainers() + update() + val new = listContainers() + return LayoutChanges( + old.filter { !new.containsKey(it.key) }.entries.associate { it.value to it.key.layout }, + new.filter { !old.containsKey(it.key) }.entries.associate { it.value to it.key.layout }, + new.filter { old.containsKey(it.key) && it.key.layout.updated } + .entries.associate { it.value to it.key.layout }, + ) + } + + class LayoutChanges( + val removed: Map, + val added: Map, + val updated: Map, + ) + } + + private data class LayoutNode( + val layout: Layout, + val containerHandle: AsdHandle, + val groups: Sequence, +// var units: Set, + ) + + private typealias InstancedPropertyMap = MutableMap, V>> + + private operator fun InstancedPropertyMap.get(handle: AsdHandle, key: AgimoPropertyMap.Key<*>) = + this[handle]?.get(key) + + private inline fun , E> InstancedPropertyMap + .push(handle: AsdHandle, key: AgimoPropertyMap.Key<*>, value: E, crossinline ifAbsent: () -> V) = + computeIfAbsent(handle) { mutableMapOf() }.computeIfAbsent(key) { ifAbsent() }.add(value) + + private fun InstancedPropertyMap.put(handle: AsdHandle, key: AgimoPropertyMap.Key<*>, value: V) = + computeIfAbsent(handle) { mutableMapOf() }.put(key, value) + +// private fun , E> InstancedPropertyMap +// .push(handle: AsdHandle, key: AgimoPropertyMap.Key<*>, value: E) = +// push(handle, key, value) { mutableSetOf() as MutableCollection } + + private val states = LayoutStates() + + private inner class LayoutStates { + private val layouts = mutableMapOf() + private val groupDeps: InstancedPropertyMap> = mutableMapOf() + private val unitDeps: InstancedPropertyMap> = mutableMapOf() +// private val unitResults: InstancedPropertyMap> = mutableMapOf() + private val units = mutableMapOf>() + private val unitGroups = mutableMapOf() + private val results = mutableMapOf>() + private val depResults = mutableMapOf, AgimoProperty>>() + private val depResultUnits: InstancedPropertyMap = mutableMapOf() + private val affectedProps = mutableMapOf>>() +// private val changedUnits = mutableSetOf() + + fun getLayout(asdHandle: AsdHandle) = layouts[asdHandle] + + fun addLayout(asdHandle: AsdHandle, layout: LayoutNode) { + if (layouts.containsKey(asdHandle)) throw IllegalStateException() + layouts[asdHandle] = layout + layout.groups.forEach { group -> + group.dependencies.forEach { (handle, keys) -> + keys.forEach { + groupDeps.push(handle, it, group) { mutableSetOf() } + } + } + } + } + + private fun addAffectedProp(handle: AsdHandle, key: AgimoPropertyMap.Key<*>) { + affectedProps.computeIfAbsent(handle) { mutableSetOf() }.add(key) + } + + fun compute() { + // TODO Currently groups have no dependencies, so not sure how this should be handled + // However, instancing Groups does not depend on states of Layouts, + // but computations of Units from Groups depend on states of Layouts. + + val unitsToCompute = mutableSetOf() + // Recompute for Units where needed + run { + val unitsToRecompute = mutableSetOf() + affectedProps.forEach { (handle, keys) -> + keys.forEach { + unitDeps[handle, it]?.apply { unitsToRecompute.addAll(this) } + } + } + affectedProps.clear() + val groupsToCompute = mutableSetOf() +// unitsToRecompute.forEach { +// val group = unitGroups[it]!! +// if (groupsToCompute.add(group)) removeGroupUnits(group) +// } + layouts.values.forEach { layout -> + layout.groups.forEach { + if (!units.containsKey(it)) groupsToCompute.add(it) + } + } + groupsToCompute.forEach { group -> + // Ignore dependencies and LayoutHandle used for Group.conditions at the moment + group.conditions().apply { + units[group] = this + }.forEach { unit -> + unitsToCompute.add(unit) + unitGroups[unit] = group + unit.dependencies.forEach { (handle, keys) -> + keys.forEach { + unitDeps.push(handle, it, unit) { mutableSetOf() } + } + } +// unit.results.forEach { (handle, keys) -> +// keys.forEach { +// unitResults.push(handle, it, unit) { mutableSetOf() } +// } +// } + } + } + } + + // Compute Units while respecting their dependencies + units.values.asSequence().flatten().forEach { if (!results.containsKey(it)) unitsToCompute.add(it) } + run { + var added = true + val unitsToRecompute = mutableSetOf() + unitsToRecompute.addAll(unitsToCompute) + while (added) { + unitsToRecompute.forEach { unit -> + unit.results.forEach { (handle, keys) -> + keys.forEach { addAffectedProp(handle, it) } + } + } + unitsToRecompute.clear() + affectedProps.forEach { (handle, keys) -> + keys.forEach { + unitDeps[handle, it]?.apply { unitsToRecompute.addAll(this) } + } + } + added = unitsToCompute.addAll(unitsToRecompute) + affectedProps.clear() + } + } +// val parents = mutableMapOf() +// val children = mutableMapOf>() +// val roots = mutableSetOf() + // one coroutine put Units to the forest and send ?? (do we really need this?) + // one coroutine checks for computability of Units (by dependencies) + // - compute result dependencies that are required by awaiting Units at the moment, + // only when those result dependencies are with uncomputed Units + // - Note: if any case some Units are dependencies but computed already, already fulfilled + // - send fulfilled computable Units to another coroutine + // one coroutine receives computable Units then compute Units sequentially in its scope + // with dependencies computed and combined as Handle, + // then notify another coroutine for dependencies by computed results + runBlocking { + val channelToCompute = Channel(UNLIMITED) + val channelComputed = Channel(UNLIMITED) + launch { + val unitResults: InstancedPropertyMap> = mutableMapOf() + unitsToCompute.forEach { unit -> + unit.results.forEach { (handle, keys) -> + keys.forEach { + unitResults.push(handle, it, unit) { mutableSetOf() } + } + } + } + val unitDeps = mutableMapOf>() + val unitChildren = mutableMapOf>() + unitsToCompute.forEach { unit -> + unit.dependencies.forEach { (handle, keys) -> + keys.forEach { key -> + val deps = unitResults[handle, key] + if (deps !== null) { + deps.forEach { + unitDeps.computeIfAbsent(unit) { mutableSetOf() }.add(it) + unitChildren.computeIfAbsent(it) { mutableSetOf() }.add(unit) + } + } + } + } + } + // First compute Units without any dependency needed to be computed + unitsToCompute.iterator().apply { + while (hasNext()) { + val unit = next() + if (unitDeps[unit] === null) { + channelToCompute.send(unit) + remove() + } + } + } + if (unitsToCompute.isNotEmpty()) for (unit in channelComputed) { + unitChildren.remove(unit)?.forEach { + val deps = requireNotNull(unitDeps[it]) + assert(deps.remove(unit)) + if (deps.isEmpty()) { + channelToCompute.send(it) + unitDeps.remove(it) + unitsToCompute.remove(it) + } + } + if (unitsToCompute.isEmpty()) break + } + channelToCompute.close() + assert(unitChildren.isEmpty()) + assert(unitDeps.isEmpty()) + } + launch { + val layoutHandle = object : LayoutHandle() { + override fun getUnit(handle: AsdHandle) = object : Unit() { + override fun getProperty(key: AgimoPropertyMap.Key) = + depResults[handle]?.getProperty(key) ?: handle.properties.getProperty(key) + + override fun containsProperty(key: AgimoPropertyMap.Key) = + depResults[handle]?.containsKey(key) == true || handle.properties.containsProperty(key) + } + } + for (unit in channelToCompute) { + val result = unit.computation(layoutHandle) + results[unit] = result + result.forEach { (handle, map) -> + map.asMap().forEach { (key, property) -> + // At the moment, if two Units compute to the same Property, a conflict occurs; + // maybe later if needed, implement precedence or priority or explicit overriding + // for each Property result from different Units + depResultUnits.put(handle, key, unit)?.let { + if (unit != it) throw IllegalStateException() + } + depResults.computeIfAbsent(handle) { mutableMapOf() }[key] = property + } + } + channelComputed.send(unit) + } + channelComputed.close() + } + } + + layouts.values.forEach { node -> + node.layout.components.forEach { + val dep = requireNotNull(depResults[it.asdHandle]) + val prev = try { + it.asdHandle::rect.get() + } catch (_: UninitializedPropertyAccessException) { + null + } + it.asdHandle.rect = dep.getProperty(RectangleProperty.KEY)?.value + ?: requireNotNull(dep.getProperty(BoundsProperty.KEY)).value + if (prev != it.asdHandle.rect) + it.asdHandle.triggerRectObservers() + } + } + } + + fun validateCyclicGraphs() { + run { // Groups + data class GroupNode( + val deps: Set>>, + val children: Set>>, + ) + data class PropNode( + val deps: Set, + val children: Set, + ) + val visited = mutableSetOf() + val visiting = mutableSetOf() + layouts.values.map { +// GroupNode( +// it.group.dependencies.flatMap { (handle, keys) -> keys.map { it -> handle to it } }.toSet(), +// it.units.flatMap { it -> +// it.results.flatMap { (handle, keys) -> keys.map { it -> handle to it } } +// }.toSet(), +// ) + } + + } + run { // Units + + } + } + + fun removeLayout(asdHandle: AsdHandle) { + val layout = layouts.remove(asdHandle) + if (layout === null) throw IllegalStateException() + layout.groups.forEach { group -> + group.dependencies.forEach { (handle, keys) -> + keys.forEach { groupDeps[handle, it]?.remove(group) } + } + removeGroupUnits(group) + } + } + + private fun removeGroupUnits(group: LayoutComputationGroup) { + units.remove(group)?.forEach { unit -> + results.remove(unit)!!.forEach { (handle, map) -> // Then this must have been computed, so non-null + map.asMap().keys.forEach { + if (depResultUnits[handle, it] === unit) { + assert(depResults[handle]!!.remove(it) !== null) + assert(depResultUnits[handle]!!.remove(it) !== null) + } + } + } + unitGroups.remove(unit) + unit.dependencies.forEach { (handle, keys) -> + keys.forEach { unitDeps[handle, it]?.remove(unit) } + } + unit.results.forEach { (handle, keys) -> + keys.forEach { addAffectedProp(handle, it) } + } + } + } + } + + internal fun tick() { + val changes = agimoTree.discoverLayoutChanges() + (changes.removed.keys.asSequence() + changes.updated.keys.asSequence()).forEach { + states.removeLayout(it) + } + val layoutHandle = object : LayoutHandle() { + override fun getUnit(handle: AsdHandle) = object : Unit() { + override fun getProperty(key: AgimoPropertyMap.Key) = + handle.properties.getProperty(key) + + override fun containsProperty(key: AgimoPropertyMap.Key) = + handle.properties.containsProperty(key) + } + } + (changes.updated.asSequence() + changes.added.asSequence()).forEach { (handle, layout) -> + states.addLayout(handle, LayoutManager.LayoutNode( + layout, + handle, + layout.layOutInternal(layoutHandle) + )) + } + states.compute() + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Menu.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Menu.kt new file mode 100644 index 00000000..70adafe8 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Menu.kt @@ -0,0 +1,69 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim + +import net.terramodulus.mui.gui.agim.event.MenuEvent +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.asd.AsdProcessor +import net.terramodulus.mui.gui.gfx.RectangleD +import net.terramodulus.mui.gui.gfx.RectangleF +import net.terramodulus.mui.gui.gfx.RenderSystem +import java.io.Closeable + +abstract class Menu( + managerHandle: MenuManager.Handle, + final override val asdHandle: AsdHandle.Container, +) : Container, Closeable { + private val listeners = HashMap, LinkedHashSet<(MenuEvent) -> Unit>>() + val handle: Handle = HandleImpl(managerHandle) + + fun addListener(e: Class, l: (T) -> Unit) { + @Suppress("UNCHECKED_CAST") + listeners.computeIfAbsent(e) { LinkedHashSet() }.add(l as (MenuEvent) -> Unit) + } + + fun removeListener(e: Class, l: (T) -> Unit) { + listeners[e]?.remove(l) + } + + internal fun dispatchEvent(event: MenuEvent) { + listeners[event.javaClass]?.forEach { it(event) } + } + + sealed interface Handle { + fun addMenu(menu: (MenuManager.Handle, AsdHandle) -> Menu) + + fun removeMenu(menu: Menu) + } + + private inner class HandleImpl(private val managerHandle: MenuManager.Handle) : Handle { + override fun addMenu(menu: (MenuManager.Handle, AsdHandle) -> Menu) = managerHandle.addMenu(menu) + + override fun removeMenu(menu: Menu) = managerHandle.removeMenu(menu) + } + + protected inner class ComponentAsdHandleImpl : AsdHandle.Container() { + override lateinit var rect: RectangleD + override fun registerAsdProcessor(processor: AsdProcessor<*>) = asdHandle.registerAsdProcessor(processor) + } + + internal fun render(renderSystem: RenderSystem) { + layout.render(renderSystem) + } + + internal fun update(muiIoI: ScreenManager.MuiIoI) { + dispatchEvent(MenuEvent.Update(muiIoI)) + layout.update() + layout.components.forEach { it.update(muiIoI) } + } + + /** + * Cleans up and closes any used resources in this session. + */ + final override fun close() { + dispatchEvent(MenuEvent.Close) + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/MenuManager.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/MenuManager.kt new file mode 100644 index 00000000..7bd08dee --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/MenuManager.kt @@ -0,0 +1,70 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim + +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.asd.AsdProcessor +import net.terramodulus.mui.gui.gfx.RectangleD +import net.terramodulus.mui.gui.gfx.RectangleF +import net.terramodulus.mui.gui.gfx.RenderSystem +import java.util.ArrayDeque + +class MenuManager internal constructor(private val asdHandle: (AsdProcessor<*>) -> Unit) { + private val menus = LinkedHashSet() + private val menuQueue = ArrayDeque() + val handle: Handle = HandleImpl() + + private sealed interface MenuOperation { + fun apply(menus: LinkedHashSet) + + class Add(val menu: () -> Menu) : MenuOperation { + override fun apply(menus: LinkedHashSet) { + menus.add(menu()) + } + } + + class Remove(val menu: Menu) : MenuOperation { + override fun apply(menus: LinkedHashSet) { + menus.remove(menu) + } + } + } + + sealed interface Handle { + fun addMenu(menu: (Handle, AsdHandle.Menu) -> Menu) + + fun removeMenu(menu: Menu) + } + + private inner class HandleImpl : Handle { + override fun addMenu(menu: (Handle, AsdHandle.Menu) -> Menu) { + menuQueue.add(MenuOperation.Add { menu(handle, MenuAsdHandleImpl()) }) + } + + override fun removeMenu(menu: Menu) { + menuQueue.add(MenuOperation.Remove(menu)) + } + } + + private inner class MenuAsdHandleImpl : AsdHandle.Menu() { + override lateinit var rect: RectangleD + override fun registerAsdProcessor(processor: AsdProcessor<*>) = asdHandle(processor) + } + + internal fun update(muiIoI: ScreenManager.MuiIoI) { + menuQueue.forEach { it.apply(menus) } + menuQueue.clear() + menus.forEach { it.update(muiIoI) } + } + + internal fun render(renderSystem: RenderSystem, screenManager: ScreenManager) { + menus.forEach { it.render(renderSystem) } + } + + internal fun visit() = AgimoTreeVisitor.MenuTreeVisitor { + menus.asSequence() + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Screen.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Screen.kt new file mode 100644 index 00000000..47e97f23 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Screen.kt @@ -0,0 +1,86 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim + +import net.terramodulus.mui.gui.agim.event.ScreenEvent +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.asd.AsdProcessor +import net.terramodulus.mui.gui.gfx.RectangleD +import net.terramodulus.mui.gui.gfx.RectangleF +import net.terramodulus.mui.gui.gfx.RenderSystem +import java.io.Closeable + +abstract class Screen( + managerHandle: ScreenManager.Handle, + final override val asdHandle: AsdHandle.Container +) : Container, Closeable { + private val listeners = HashMap, LinkedHashSet<(ScreenEvent) -> Unit>>() + private val menuManager = MenuManager(asdHandle::registerAsdProcessor) + val handle: Handle = HandleImpl(managerHandle) + +// init { +// // TODO there should be an entry for background, maybe it sets background for entire render background? +// asdHandle.registerAsdProcessor() +// } + + fun addListener(e: Class, l: (T) -> Unit) { + @Suppress("UNCHECKED_CAST") + listeners.computeIfAbsent(e) { LinkedHashSet() }.add(l as (ScreenEvent) -> Unit) + } + + fun removeListener(e: Class, l: (T) -> Unit) { + listeners[e]?.remove(l) + } + + internal fun dispatchEvent(event: ScreenEvent) { + listeners[event.javaClass]?.forEach { it(event) } + } + + sealed interface Handle { + fun addMenu(menu: (MenuManager.Handle, AsdHandle.Menu) -> Menu) + + fun removeMenu(menu: Menu) + + fun addTopMenu(menu: (MenuManager.Handle, AsdHandle.Menu) -> Menu) + + fun removeTopMenu(menu: Menu) + } + + private inner class HandleImpl(private val managerHandle: ScreenManager.Handle) : Handle { + override fun addMenu(menu: (MenuManager.Handle, AsdHandle.Menu) -> Menu) = menuManager.handle.addMenu(menu) + + override fun removeMenu(menu: Menu) = menuManager.handle.removeMenu(menu) + + override fun addTopMenu(menu: (MenuManager.Handle, AsdHandle.Menu) -> Menu) = managerHandle.addMenu(menu) + + override fun removeTopMenu(menu: Menu) = managerHandle.removeMenu(menu) + } + + protected inner class ComponentAsdHandleImpl : AsdHandle.Container() { + override lateinit var rect: RectangleD + override fun registerAsdProcessor(processor: AsdProcessor<*>) = asdHandle.registerAsdProcessor(processor) + } + + internal fun update(muiIoI: ScreenManager.MuiIoI) { + dispatchEvent(ScreenEvent.Update(muiIoI)) + layout.update() + layout.components.forEach { it.update(muiIoI) } + } + + internal fun render(renderSystem: RenderSystem, screenManager: ScreenManager) { + layout.render(renderSystem) + menuManager.render(renderSystem, screenManager) + } + + /** + * Cleans up and closes any used resources in this session. + */ + final override fun close() { + dispatchEvent(ScreenEvent.Close) + } + + internal fun visit() = menuManager.visit() +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/ScreenManager.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/ScreenManager.kt new file mode 100644 index 00000000..3e909f84 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/ScreenManager.kt @@ -0,0 +1,296 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim + +import net.terramodulus.engine.Window +import net.terramodulus.mui.MuiManager +import net.terramodulus.mui.gui.InputStatesHandle +import net.terramodulus.mui.gui.agim.impl.BoundsProperty +import net.terramodulus.mui.gui.gfx.RenderSystem +import net.terramodulus.mui.gui.agim.impl.LaunchingScreen +import net.terramodulus.mui.gui.agim.impl.RectangleProperty +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.asd.AsdManager +import net.terramodulus.mui.gui.asd.AsdProcessor +import net.terramodulus.mui.gui.gfx.RectangleD +import net.terramodulus.mui.kui.InputSystem + +class ScreenManager internal constructor( + window: Window, + private val renderSystemHandle: RenderSystem.Handle, + private val asdManagerHandle: AsdManager.AgimHandle, + private val inputStatesHandle: InputStatesHandle, +) { + /** + * FILO screen stack; the top-most screen instance is in the last. + */ + private val screens = ArrayDeque() + private val screenOpQueue = ArrayDeque() + private val menuManager = MenuManager(asdManagerHandle::registerProcessor) + + private val asdHandles = HashSet() + private var viewportRect = RectangleD(0.0, 0.0, window.width.toDouble(), window.height.toDouble()) + + init { + window.addListener { w, h -> + viewportRect = RectangleD(0.0, 0.0, w.toDouble(), h.toDouble()) + asdHandles.forEach { it.triggerRectObservers() } // already referring viewportRect + } + } + + // TODO Basically need to hide those rectangles in LayoutManager instead +// inner class LayoutHandle.Screen : ManagedRect(), Closeable { +// override var value: RectangleF by Delegates.observable(viewportRect.value) { _, _, newValue -> +// observers.forEach { it(newValue) } +// } +// private set +// +// } + + val handle: Handle = HandleImpl() + + init { + screens.add(LaunchingScreen(handle, ScreenAsdHandleImpl(), renderSystemHandle)) + } + + private sealed interface ScreenOperation { + fun apply(handle: RenderSystem.Handle, screens: ArrayDeque, asdHandles: HashSet) + + /** + * Exits `n` times + * + * @throws IllegalArgumentException when `n` < 1 + * @throws IllegalStateException when `n` >= [screens] size during operation + */ + class Exit(val n: Int) : ScreenOperation { + init { + require(n > 0) { "`n` < 1" } + } + + override fun apply( + handle: RenderSystem.Handle, + screens: ArrayDeque, + asdHandles: HashSet + ) { + if (n >= screens.size) { + throw IllegalStateException("`n` >= screens.size") + } + + for (i in 1..n) { + screens.removeLast().apply { + close() + asdHandles.remove(asdHandle) + } + } + } + } + + /** + * Opens the `screen` + */ + class Open(val screen: (RenderSystem.Handle) -> Screen) : ScreenOperation { + override fun apply( + handle: RenderSystem.Handle, + screens: ArrayDeque, + asdHandles: HashSet + ) { + screens.addLast(screen(handle)) + } + } + + /** + * Opens the `screen` before the `target` screen + */ + class OpenBefore(val target: Screen, val screen: (RenderSystem.Handle) -> Screen) : ScreenOperation { + override fun apply( + handle: RenderSystem.Handle, + screens: ArrayDeque, + asdHandles: HashSet + ) { + screens.add(screens.lastIndexOf(target), screen(handle)) + } + } + + /** + * Exits until reaching the `screen` then remains on the `screen` + */ + class ExitTo(val screen: Screen) : ScreenOperation { + override fun apply( + handle: RenderSystem.Handle, + screens: ArrayDeque, + asdHandles: HashSet + ) { + val it = screens.asReversed().listIterator() + while (it.hasNext()) { + val e = it.next() + if (e == screen) { + break + } else { + it.remove() + e.close() + asdHandles.remove(e.asdHandle) + } + } + } + } + + /** + * Clears [screens] then opens the `screen` + */ + class Reset(val screen: (RenderSystem.Handle) -> Screen) : ScreenOperation { + override fun apply( + handle: RenderSystem.Handle, + screens: ArrayDeque, + asdHandles: HashSet + ) { + screens.asReversed().forEach { it.close() } + screens.clear() + asdHandles.clear() + screens.add(screen(handle)) + } + } + } + + sealed interface Handle { + /** + * @see ScreenOperation.Exit + */ + fun exit(n: Int) + + /** + * @see ScreenOperation.Open + */ + fun open(screen: (Handle, AsdHandle.Screen, RenderSystem.Handle) -> Screen) + + /** + * @see ScreenOperation.Open + */ + fun open(screen: (Handle, AsdHandle.Screen, RenderSystem.Handle, InputStatesHandle) -> Screen) + + /** + * It is not recommended to use this in general scenarios. + * @see ScreenOperation.OpenBefore + */ + fun openBefore(target: Screen, screen: (Handle, AsdHandle.Screen, RenderSystem.Handle) -> Screen) + + /** + * It is not recommended to use this in general scenarios. + * @see ScreenOperation.OpenBefore + */ + fun openBefore( + target: Screen, + screen: (Handle, AsdHandle.Screen, RenderSystem.Handle, InputStatesHandle) -> Screen, + ) + + /** + * @see ScreenOperation.ExitTo + */ + fun exitTo(screen: Screen) + + /** + * @see ScreenOperation.Reset + */ + fun reset(screen: (Handle, AsdHandle.Screen, RenderSystem.Handle) -> Screen) + + /** + * @see ScreenOperation.Reset + */ + fun reset(screen: (Handle, AsdHandle.Screen, RenderSystem.Handle, InputStatesHandle) -> Screen) + + fun addMenu(menu: (MenuManager.Handle, AsdHandle.Menu) -> Menu) + + fun removeMenu(menu: Menu) + } + + private inner class HandleImpl : Handle { + override fun exit(n: Int) { + screenOpQueue.add(ScreenOperation.Exit(n)) + } + + override fun open(screen: (Handle, AsdHandle.Screen, RenderSystem.Handle) -> Screen) { + screenOpQueue.add(ScreenOperation.Open { screen(handle, ScreenAsdHandleImpl(), it) }) + } + + override fun open(screen: (Handle, AsdHandle.Screen, RenderSystem.Handle, InputStatesHandle) -> Screen) { + screenOpQueue.add(ScreenOperation.Open { screen(handle, ScreenAsdHandleImpl(), it, inputStatesHandle) }) + } + + override fun openBefore(target: Screen, screen: (Handle, AsdHandle.Screen, RenderSystem.Handle) -> Screen) { + screenOpQueue.add(ScreenOperation.OpenBefore(target) { screen(handle, ScreenAsdHandleImpl(), it) }) + } + + override fun openBefore( + target: Screen, + screen: (Handle, AsdHandle.Screen, RenderSystem.Handle, InputStatesHandle) -> Screen + ) { + screenOpQueue.add(ScreenOperation.OpenBefore(target) { + screen(handle, ScreenAsdHandleImpl(), it, inputStatesHandle) + }) + } + + override fun exitTo(screen: Screen) { + screenOpQueue.add(ScreenOperation.ExitTo(screen)) + } + + override fun reset(screen: (Handle, AsdHandle.Screen, RenderSystem.Handle) -> Screen) { + screenOpQueue.add(ScreenOperation.Reset { screen(handle, ScreenAsdHandleImpl(), it) }) + } + + override fun reset(screen: (Handle, AsdHandle.Screen, RenderSystem.Handle, InputStatesHandle) -> Screen) { + screenOpQueue.add(ScreenOperation.Reset { screen(handle, ScreenAsdHandleImpl(), it, inputStatesHandle) }) + } + + override fun addMenu(menu: (MenuManager.Handle, AsdHandle.Menu) -> Menu) = menuManager.handle.addMenu(menu) + + override fun removeMenu(menu: Menu) = menuManager.handle.removeMenu(menu) + } + + private inner class ScreenAsdHandleImpl : AsdHandle.Screen() { + init { + asdHandles.add(this) + properties.putProperty(BoundsProperty.KEY, BoundsProperty(viewportRect)) + properties.putProperty(RectangleProperty.KEY, RectangleProperty(viewportRect)) + observeRect { + properties.putProperty(BoundsProperty.KEY, BoundsProperty(viewportRect)) + properties.putProperty(RectangleProperty.KEY, RectangleProperty(viewportRect)) + } + } + + override var rect get() = viewportRect + set(value) = throw UnsupportedOperationException() // should never be invoked + + override fun registerAsdProcessor(processor: AsdProcessor<*>) = asdManagerHandle.registerProcessor(processor) + } + + /** + * MUI Interoperability Interface + */ + class MuiIoI internal constructor( + val renderSystem: RenderSystem, + val screenManager: ScreenManager, + val inputSystem: InputSystem, + ) + + internal fun update(muiManager: MuiManager) { + repeat(screenOpQueue.size) { + screenOpQueue.removeFirst().apply(renderSystemHandle, screens, asdHandles) + } + val ioi = MuiIoI(muiManager.guiManager.renderSystem, this, muiManager.kuiManager.inputSystem) + menuManager.update(ioi) + screens.forEach { it.update(ioi) } + } + + internal fun render(renderSystem: RenderSystem) { + screens.forEach { it.render(renderSystem, this) } + menuManager.render(renderSystem, this) + } + + internal fun visitScreens() = AgimoTreeVisitor.ScreenTreeVisitor { + screens.asSequence() + } + + internal fun visitMenus() = menuManager.visit() +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/BubblingEvent.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/BubblingEvent.kt new file mode 100644 index 00000000..7a2f34a4 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/BubblingEvent.kt @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.event + +sealed class BubblingEvent { + var bubbles: Boolean = true + private set + + fun stopPropagation() { + bubbles = false + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/ComponentEvent.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/ComponentEvent.kt new file mode 100644 index 00000000..f71c5800 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/ComponentEvent.kt @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.event + +import net.terramodulus.mui.gui.agim.ScreenManager + +sealed interface ComponentEvent { + // TODO Add Init and Close? + data class Update(val muiIoI: ScreenManager.MuiIoI) : ComponentEvent + data class Generic(val generic: T) : ComponentEvent +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/GenericEvent.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/GenericEvent.kt new file mode 100644 index 00000000..5b3a52ee --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/GenericEvent.kt @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.event + +// Must not be used in kernel +abstract class GenericEvent : BubblingEvent() { + override fun equals(other: Any?): Boolean { + return this === other + } + + override fun hashCode(): Int { + return System.identityHashCode(this) + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/GenericEvents.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/GenericEvents.kt new file mode 100644 index 00000000..1505c2ac --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/GenericEvents.kt @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.event + +object GenericEvents { + // TODO not sure whether this is really needed or useful in the future + // This should either provide sealed classes to be extended by (more than one) subclasses of AGIMO Events, + // or provide variants to be used with GenericEvent. +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/MenuEvent.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/MenuEvent.kt new file mode 100644 index 00000000..de00b7e3 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/MenuEvent.kt @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.event + +import net.terramodulus.mui.gui.agim.ScreenManager + +sealed interface MenuEvent { + data class Update(val muiIoI: ScreenManager.MuiIoI) : MenuEvent + data object Close : MenuEvent + data class Generic(val generic: T) : MenuEvent +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/ScreenEvent.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/ScreenEvent.kt new file mode 100644 index 00000000..2f6e8c7b --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/ScreenEvent.kt @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.event + +import net.terramodulus.mui.gui.agim.ScreenManager + +sealed interface ScreenEvent { + data class Update(val muiIoI: ScreenManager.MuiIoI) : ScreenEvent + data object Close : ScreenEvent + data class Generic(val generic: T) : ScreenEvent +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/BlankComponent.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/BlankComponent.kt new file mode 100644 index 00000000..76ad6f7c --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/BlankComponent.kt @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.impl + +import net.terramodulus.mui.gui.gfx.RenderSystem +import net.terramodulus.mui.gui.agim.Component +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.gfx.Dimension2F + +/** + * This can act as a placeholder [Component] in a [Layout][net.terramodulus.mui.gui.agim.Layout]. + */ +class BlankComponent(asdHandle: AsdHandle) : Component(asdHandle) { + override fun render(renderSystem: RenderSystem) {} +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/BorderedPane.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/BorderedPane.kt new file mode 100644 index 00000000..305b669b --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/BorderedPane.kt @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.impl + +import net.terramodulus.mui.gui.agim.AbstractPane +import net.terramodulus.mui.gui.agim.Component +import net.terramodulus.mui.gui.agim.Layout +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.gfx.InsetsD +import net.terramodulus.mui.gui.gfx.RenderSystem + +class BorderedPane(canvasHandle: RenderSystem.CanvasHandle, asdHandle: AsdHandle, component: Component, config: Config) : AbstractPane(asdHandle) { + private val _layout = (config.breadth + config.gap).let { + SingletonLayout(this, component, SingletonLayout.Config.Absolute.Insets(InsetsD(it, it, it, it))) + } + override val layout: Layout = CompositeLayout(this).apply { + update { + // TODO incompatible with current implementation of OutlineComponent +// add(SingletonLayout(this, OutlineComponent(), SingletonLayout.Config.Absolute.Full)) + add(_layout) + } + } + + fun update(component: Component) = _layout.update(component) + + class Config(val breadth: Double, val gap: Double) + + override fun render(renderSystem: RenderSystem) { + layout.render(renderSystem) + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/BoundsProperty.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/BoundsProperty.kt new file mode 100644 index 00000000..716d31ec --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/BoundsProperty.kt @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.impl + +import net.terramodulus.mui.gui.agim.AgimoProperty +import net.terramodulus.mui.gui.agim.AgimoPropertyMap +import net.terramodulus.mui.gui.gfx.RectangleD + +data class BoundsProperty(val value: RectangleD) : AgimoProperty() { + companion object { + val KEY = AgimoPropertyMap.Key(BoundsProperty::class.java) + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/ButtonComponent.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/ButtonComponent.kt new file mode 100644 index 00000000..25c7764f --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/ButtonComponent.kt @@ -0,0 +1,28 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.impl + +import net.terramodulus.mui.gui.InputStatesHandle +import net.terramodulus.mui.gui.MouseCtxStates +import net.terramodulus.mui.gui.agim.AbstractPane +import net.terramodulus.mui.gui.agim.Layout +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.gfx.RenderSystem +import net.terramodulus.mui.kui.MouseInputHandler + +class ButtonComponent( + asdHandle: AsdHandle, + inputStatesHandle: InputStatesHandle, + layout: ButtonComponent.() -> Layout, + action: () -> Unit, +) : AbstractPane(asdHandle) { + override val layout = layout(this) + private val mouseCtxStates = MouseCtxStates(inputStatesHandle.mouseGlobalStates, asdHandle).apply { + addListener(listenRectFullClick(MouseInputHandler.Buttons.Left.id) { action() }) + } + + override fun render(renderSystem: RenderSystem) = layout.render(renderSystem) +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/CheckboxComponent.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/CheckboxComponent.kt new file mode 100644 index 00000000..72efa622 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/CheckboxComponent.kt @@ -0,0 +1,78 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.impl + +import net.terramodulus.mui.gui.InputStatesHandle +import net.terramodulus.mui.gui.MouseCtxStates +import net.terramodulus.mui.gui.agim.AbstractPane +import net.terramodulus.mui.gui.agim.Layout +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.gfx.GuiLine +import net.terramodulus.mui.gui.gfx.RectangleD +import net.terramodulus.mui.gui.gfx.RenderSystem +import net.terramodulus.mui.kui.MouseInputHandler +import kotlin.properties.Delegates + +// TODO should actually be non-Pane +class CheckboxComponent( + asdHandle: AsdHandle, + inputStatesHandle: InputStatesHandle, + canvasHandle: RenderSystem.CanvasHandle, + init: Boolean, + callback: (Boolean) -> Unit, +) : AbstractPane(asdHandle) { + constructor( + asdHandle: AsdHandle, + inputStatesHandle: InputStatesHandle, + canvasHandle: RenderSystem.CanvasHandle, + callback: (Boolean) -> Unit, + ) : this(asdHandle, inputStatesHandle, canvasHandle, false, callback) + + /** + * Caveat: if this is externally modified, `callback` is never invoked. + */ + var checked: Boolean by Delegates.observable(init) { _, _, newValue -> + layout.update(if (newValue) { checkedFace } else { uncheckedFace }) + } + private val uncheckedFace = DrawablesComponent(sequenceOf( + DrawablesComponent.Drawable.Geom(GuiLine(canvasHandle, 0, 0, 0, 50, 255, 255, 255, 255)), + DrawablesComponent.Drawable.Geom(GuiLine(canvasHandle, 0, 0, 50, 0, 255, 255, 255, 255)), + DrawablesComponent.Drawable.Geom(GuiLine(canvasHandle, 0, 50, 50, 50, 255, 255, 255, 255)), + DrawablesComponent.Drawable.Geom(GuiLine(canvasHandle, 50, 0, 50, 50, 255, 255, 255, 255)), + ), RectangleD(0.0, 0.0, 50.0, 50.0), ComponentAsdHandleImpl()) + private val checkedFace = DrawablesComponent(sequenceOf( + DrawablesComponent.Drawable.Geom(GuiLine(canvasHandle, 0, 0, 0, 50, 255, 255, 255, 255)), + DrawablesComponent.Drawable.Geom(GuiLine(canvasHandle, 0, 0, 50, 0, 255, 255, 255, 255)), + DrawablesComponent.Drawable.Geom(GuiLine(canvasHandle, 0, 50, 50, 50, 255, 255, 255, 255)), + DrawablesComponent.Drawable.Geom(GuiLine(canvasHandle, 50, 0, 50, 50, 255, 255, 255, 255)), + DrawablesComponent.Drawable.Geom(GuiLine(canvasHandle, 10, 20, 20, 10, 255, 255, 255, 255)), + DrawablesComponent.Drawable.Geom(GuiLine(canvasHandle, 20, 10, 45, 45, 255, 255, 255, 255)), + ), RectangleD(0.0, 0.0, 50.0, 50.0), ComponentAsdHandleImpl()) + override val layout = SingletonLayout(this, if (checked) checkedFace else uncheckedFace, + SingletonLayout.Config.Absolute.Full) +// override val layout = SingletonLayout(this, DrawablesComponent(object : Sequence { +// private val outline = sequenceOf( +// DrawablesComponent.Drawable.Geom(GuiLine(canvasHandle, 0, 0, 0, 50, 255, 255, 255, 255)), +// DrawablesComponent.Drawable.Geom(GuiLine(canvasHandle, 0, 0, 50, 0, 255, 255, 255, 255)), +// DrawablesComponent.Drawable.Geom(GuiLine(canvasHandle, 0, 50, 50, 50, 255, 255, 255, 255)), +// DrawablesComponent.Drawable.Geom(GuiLine(canvasHandle, 50, 0, 50, 50, 255, 255, 255, 255)), +// ) +// private val mark = sequenceOf( +// DrawablesComponent.Drawable.Geom(GuiLine(canvasHandle, 10, 20, 20, 10, 255, 255, 255, 255)), +// DrawablesComponent.Drawable.Geom(GuiLine(canvasHandle, 20, 10, 45, 45, 255, 255, 255, 255)), +// ) +// override fun iterator(): Iterator = +// if (checked) { outline + mark } else { outline }.iterator() +// }, RectangleD(0.0, 0.0, 50.0, 50.0), ComponentAsdHandleImpl()), SingletonLayout.Config.Absolute.Full) + private val mouseCtxStates = MouseCtxStates(inputStatesHandle.mouseGlobalStates, asdHandle).apply { + addListener(listenRectFullClick(MouseInputHandler.Buttons.Left.id) { + checked = !checked + callback(checked) + }) + } + + override fun render(renderSystem: RenderSystem) = layout.render(renderSystem) +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/CollapsablePane.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/CollapsablePane.kt new file mode 100644 index 00000000..f02f1860 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/CollapsablePane.kt @@ -0,0 +1,268 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.impl + +import com.cout970.math.vec2.ImmVec2d +import net.terramodulus.mui.gui.agim.AbstractPane +import net.terramodulus.mui.gui.agim.AgimoPropertyMap +import net.terramodulus.mui.gui.agim.AnchorAlignmentHelper +import net.terramodulus.mui.gui.agim.Component +import net.terramodulus.mui.gui.agim.Layout +import net.terramodulus.mui.gui.agim.LayoutComputationGroup +import net.terramodulus.mui.gui.agim.LayoutComputationUnit +import net.terramodulus.mui.gui.agim.LayoutHandle +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.gfx.Dimension2D +import net.terramodulus.mui.gui.gfx.Direction2S +import net.terramodulus.mui.gui.gfx.Direction4A +import net.terramodulus.mui.gui.gfx.GeneralTransform +import net.terramodulus.mui.gui.gfx.GuiLine +import net.terramodulus.mui.gui.gfx.RectangleD +import net.terramodulus.mui.gui.gfx.RenderSystem +import kotlin.math.PI +import kotlin.math.max +import kotlin.properties.Delegates + +class CollapsablePane(canvasHandle: RenderSystem.CanvasHandle, asdHandle: AsdHandle, config: ConstructEnv.() -> ConstructEnv.Config) : AbstractPane(asdHandle) { + companion object { + private const val ROT_RIGHT = PI / 2 + private const val ROT_LEFT = -PI / 2 + private const val INDICATOR_SIZE = 12.0 + private val INDICATOR_DIMS = Dimension2D(INDICATOR_SIZE, INDICATOR_SIZE) + } + + object ConstructEnv { + class Config( + val headerPos: Direction4A, + val indicatorPos: Direction2S, + val header: Component, + val contents: Component, + ) + } + + private val _layout = CollapsableLayout(canvasHandle, config(ConstructEnv)) + override val layout: Layout = _layout + + var open: Boolean by _layout::open + + private inner class CollapsableLayout(canvasHandle: RenderSystem.CanvasHandle, config: ConstructEnv.Config) : + Layout(this@CollapsablePane) { + private val headerPos = config.headerPos + private val indicatorPos = config.indicatorPos + private val standardRot: GeneralTransform + private val transform = GeneralTransform() + // standard orientation: right/pos towards x + private val indicatorFace = DrawablesComponent(sequenceOf( + DrawablesComponent.Drawable.Geom(GuiLine(canvasHandle, 1, 1, 3, 2, 255, 255, 255, 255)), + DrawablesComponent.Drawable.Geom(GuiLine(canvasHandle, 1, 3, 3, 2, 255, 255, 255, 255)), + ), RectangleD(0.0, 0.0, 4.0, 4.0), ComponentAsdHandleImpl()) + private var header = config.header + private var contents = config.contents + + var open: Boolean by Delegates.observable(false) { _, _, newValue -> + operate { + transform.update { + angle = if (newValue) when (headerPos) { + Direction4A.XPos -> when (indicatorPos) { + Direction2S.Positive -> ROT_RIGHT + Direction2S.Negative -> ROT_LEFT + } + Direction4A.XNeg -> when (indicatorPos) { + Direction2S.Positive -> ROT_LEFT + Direction2S.Negative -> ROT_RIGHT + } + Direction4A.YPos -> when (indicatorPos) { + Direction2S.Positive -> ROT_LEFT + Direction2S.Negative -> ROT_RIGHT + } + Direction4A.YNeg -> when (indicatorPos) { + Direction2S.Positive -> ROT_RIGHT + Direction2S.Negative -> ROT_LEFT + } + } else 0.0 + } + } + } + + init { + val angle = when (headerPos) { + Direction4A.XPos, Direction4A.XNeg -> when (indicatorPos) { // y + Direction2S.Positive -> -PI / 2 // -90 degrees to upwards + Direction2S.Negative -> PI / 2 // +90 degrees to downwards + } + Direction4A.YPos, Direction4A.YNeg -> when (indicatorPos) { // x + Direction2S.Positive -> 0.0 // 0 to rightwards + Direction2S.Negative -> PI // 180 degrees to leftwards + } + } + standardRot = GeneralTransform(1.0, 1.0, angle, 0.0, 0.0) + indicatorFace.addTransform(standardRot) + indicatorFace.addTransform(transform) + } + + override val components = componentsSequence(::indicatorFace, ::header, ::contents) + + fun updateHeader(header: Component) = operate { this@CollapsableLayout.header = header } + + fun updateContents(contents: Component) = operate { this@CollapsableLayout.contents = contents } + + override fun layOut(handle: LayoutHandle) = sequenceOf(LayoutComputationGroup({}, { + setOf(LayoutComputationUnit({ + // indicatorFace can be ignored here + put(header.asdHandle, setOf(IntrinsicDimensionsProperty.KEY, DimensionsProperty.KEY)) + put(contents.asdHandle, setOf(IntrinsicDimensionsProperty.KEY, DimensionsProperty.KEY)) + }, { + put(this@CollapsablePane.asdHandle, setOf(DimensionsProperty.KEY)) + }, { + val headerDims = DimensionsProperty.getOrComputeValue(getUnit(header.asdHandle)) + val contentsDims = DimensionsProperty.getOrComputeValue(getUnit(contents.asdHandle)) + mapOf(this@CollapsablePane.asdHandle to AgimoPropertyMap().apply { + putProperty(DimensionsProperty.KEY, DimensionsProperty(when (headerPos) { + Direction4A.XPos, Direction4A.XNeg -> { + val height = max(headerDims.height + INDICATOR_SIZE, contentsDims.height) + val headerWidth = max(INDICATOR_SIZE, headerDims.width) + Dimension2D(headerWidth + contentsDims.width, height) + } + Direction4A.YPos, Direction4A.YNeg -> { + val width = max(headerDims.width + INDICATOR_SIZE, contentsDims.width) + val headerHeight = max(INDICATOR_SIZE, headerDims.height) + Dimension2D(width, headerHeight + contentsDims.height) + } + })) + }) + }), LayoutComputationUnit({ + put(header.asdHandle, setOf(IntrinsicDimensionsProperty.KEY, DimensionsProperty.KEY)) + put(contents.asdHandle, setOf(IntrinsicDimensionsProperty.KEY, DimensionsProperty.KEY)) + put(this@CollapsablePane.asdHandle, setOf( + BoundsProperty.KEY, + RectangleProperty.KEY, + DimensionsProperty.KEY, + )) + }, { + put(indicatorFace.asdHandle, setOf(BoundsProperty.KEY)) + put(header.asdHandle, setOf(BoundsProperty.KEY)) + put(contents.asdHandle, setOf(BoundsProperty.KEY)) + }, { + val headerDims = DimensionsProperty.getOrComputeValue(getUnit(header.asdHandle)) + val contentsDims = DimensionsProperty.getOrComputeValue(getUnit(contents.asdHandle)) + val prop = getUnit(this@CollapsablePane.asdHandle) + val containerRect = prop.getProperty(RectangleProperty.KEY)?.value + ?: prop.getProperty(BoundsProperty.KEY)!!.value + assert(prop.getProperty(DimensionsProperty.KEY)!!.value.let { + containerRect.width == it.width && containerRect.height == it.height + }) + val indicatorRect: RectangleD + val headerRect: RectangleD + val contentsRect: RectangleD + when (headerPos) { + Direction4A.XPos, Direction4A.XNeg -> { + val headerWidth = max(INDICATOR_SIZE, headerDims.width) + val headerSpace: RectangleD + when (headerPos) { + Direction4A.XPos -> { + headerSpace = RectangleD( + containerRect.x + containerRect.width - headerWidth, + containerRect.y, + headerWidth, + containerRect.height, + ) + contentsRect = AnchorAlignmentHelper.simple(RectangleD( + containerRect.x, + containerRect.y, + contentsDims.width, + containerRect.height, + ), contentsDims, ImmVec2d(0.5)) + } + Direction4A.XNeg -> { + headerSpace = RectangleD( + containerRect.x, + containerRect.y, + headerWidth, + containerRect.height, + ) + contentsRect = AnchorAlignmentHelper.simple(RectangleD( + containerRect.x + headerWidth, + containerRect.y, + contentsDims.width, + containerRect.height, + ), contentsDims, ImmVec2d(0.5)) + } + } + when (indicatorPos) { + Direction2S.Positive -> { + indicatorRect = + AnchorAlignmentHelper.simple(headerSpace, INDICATOR_DIMS, ImmVec2d(0.5, 1.0)) + headerRect = AnchorAlignmentHelper.simple(headerSpace, headerDims, ImmVec2d(0.5, 0.0)) + } + Direction2S.Negative -> { + indicatorRect = + AnchorAlignmentHelper.simple(headerSpace, INDICATOR_DIMS, ImmVec2d(0.5, 0.0)) + headerRect = AnchorAlignmentHelper.simple(headerSpace, headerDims, ImmVec2d(0.5, 1.0)) + } + } + } + Direction4A.YPos, Direction4A.YNeg -> { + val headerHeight = max(INDICATOR_SIZE, headerDims.height) + val headerSpace: RectangleD + when (headerPos) { + Direction4A.YPos -> { + headerSpace = RectangleD( + containerRect.x, + containerRect.y + containerRect.height - headerHeight, + containerRect.width, + headerHeight, + ) + contentsRect = AnchorAlignmentHelper.simple(RectangleD( + containerRect.x, + containerRect.y, + containerRect.width, + contentsDims.height, + ), contentsDims, ImmVec2d(0.5)) + } + Direction4A.YNeg -> { + headerSpace = RectangleD( + containerRect.x, + containerRect.y, + containerRect.width, + headerHeight, + ) + contentsRect = AnchorAlignmentHelper.simple(RectangleD( + containerRect.x, + containerRect.y + headerHeight, + containerRect.width, + contentsDims.height, + ), contentsDims, ImmVec2d(0.5)) + } + } + when (indicatorPos) { + Direction2S.Positive -> { + indicatorRect = + AnchorAlignmentHelper.simple(headerSpace, INDICATOR_DIMS, ImmVec2d(1.0, 0.5)) + headerRect = AnchorAlignmentHelper.simple(headerSpace, headerDims, ImmVec2d(0.0, 0.5)) + } + Direction2S.Negative -> { + indicatorRect = + AnchorAlignmentHelper.simple(headerSpace, INDICATOR_DIMS, ImmVec2d(0.0, 0.5)) + headerRect = AnchorAlignmentHelper.simple(headerSpace, headerDims, ImmVec2d(1.0, 0.5)) + } + } + } + } + mapOf( + indicatorFace.asdHandle to AgimoPropertyMap().apply { putProperty(BoundsProperty(indicatorRect)) }, + header.asdHandle to AgimoPropertyMap().apply { putProperty(BoundsProperty(headerRect)) }, + contents.asdHandle to AgimoPropertyMap().apply { putProperty(BoundsProperty(contentsRect)) }, + ) + })) + })) + } + + fun updateHeader(header: Component) = _layout.updateHeader(header) + + fun updateContents(contents: Component) = _layout.updateContents(contents) + + override fun render(renderSystem: RenderSystem) = _layout.render(renderSystem) +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/CompositeLayout.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/CompositeLayout.kt new file mode 100644 index 00000000..a9a16f7e --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/CompositeLayout.kt @@ -0,0 +1,29 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.impl + +import net.terramodulus.mui.gui.agim.Container +import net.terramodulus.mui.gui.agim.Layout +import net.terramodulus.mui.gui.agim.LayoutComputationGroup +import net.terramodulus.mui.gui.agim.LayoutHandle +import net.terramodulus.mui.gui.asd.AsdHandle + +class CompositeLayout(container: Container) : Layout(container) { + private val layouts = ArrayDeque() + + constructor(container: Container, init: ArrayDeque.() -> Unit) : this(container) { init(layouts) } + + override val components = layouts.asSequence().flatMap { it.components } + + fun update(operation: ArrayDeque.() -> Unit) { + operate { + operation(layouts) + layouts.forEach { it.update() } + } + } + + override fun layOut(handle: LayoutHandle) = layouts.flatMap { it.layOutInternal(handle) }.asSequence() +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/DimensionsProperty.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/DimensionsProperty.kt new file mode 100644 index 00000000..054dfad1 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/DimensionsProperty.kt @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.impl + +import net.terramodulus.mui.gui.agim.AgimoProperty +import net.terramodulus.mui.gui.agim.AgimoPropertyMap +import net.terramodulus.mui.gui.agim.LayoutHandle +import net.terramodulus.mui.gui.gfx.Dimension2D + +data class DimensionsProperty(val value: Dimension2D) : AgimoProperty() { + companion object { + val KEY = AgimoPropertyMap.Key(DimensionsProperty::class.java) + + fun getOrComputeValue(map: LayoutHandle.Unit): Dimension2D { + return map.getProperty(KEY)?.value ?: map.getProperty(IntrinsicDimensionsProperty.KEY)!!.let { + Dimension2D(it.width.toDouble(), it.height.toDouble()) + } + } + + fun getOrComputeRatio(map: LayoutHandle.Unit): Dimension2D { + return map.getProperty(IntrinsicRatioProperty.KEY)?.let { + Dimension2D(it.width.toDouble(), it.height.toDouble()) + } ?: map.getProperty(KEY)?.value ?: map.getProperty(IntrinsicDimensionsProperty.KEY)!!.let { + Dimension2D(it.width.toDouble(), it.height.toDouble()) + } + } + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/FlexibleBoxLayout.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/FlexibleBoxLayout.kt new file mode 100644 index 00000000..1758d165 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/FlexibleBoxLayout.kt @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.impl + +import net.terramodulus.mui.gui.agim.Container +import net.terramodulus.mui.gui.agim.Layout +import net.terramodulus.mui.gui.agim.LayoutComputationGroup +import net.terramodulus.mui.gui.agim.LayoutHandle +import net.terramodulus.mui.gui.asd.AsdHandle + +class FlexibleBoxLayout(container: Container) : Layout(container) { + override val components = TODO("Not yet implemented") + + override fun layOut(handle: LayoutHandle): Sequence { + TODO("Not yet implemented") + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GameplayScreen.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GameplayScreen.kt new file mode 100644 index 00000000..7f25817c --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GameplayScreen.kt @@ -0,0 +1,945 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.impl + +import com.cout970.math.quaternion.ImmQuatd +import com.cout970.math.vec2.Vec2d +import com.cout970.math.vec3.ImmVec3d +import com.cout970.math.vec3.Vec3d +import com.cout970.math.vec3.Vec3f +import com.cout970.math.vec3.div +import com.cout970.math.vec3.dot +import com.cout970.math.vec3.normalized +import com.cout970.math.vec3.plus +import com.cout970.math.vec3.times +import com.cout970.math.vec3.toImmVec3f +import com.cout970.math.vec3.toMutVec3d +import com.cout970.math.vec4.ImmVec4i +import net.terramodulus.core.TerraModulus +import net.terramodulus.core.getResourceAsString +import net.terramodulus.engine.Camera3D +import net.terramodulus.engine.PhyBody +import net.terramodulus.engine.PhyGeom +import net.terramodulus.engine.SimpleMesh3dGeomCube +import net.terramodulus.engine.SimpleMesh3dGeomSphere +import net.terramodulus.engine.WorldObjDrawable +import net.terramodulus.engine.common.ZeroImmVec3d +import net.terramodulus.mui.gui.InputStatesHandle +import net.terramodulus.mui.gui.MouseCtxStates +import net.terramodulus.mui.gui.MouseState +import net.terramodulus.mui.gui.agim.Component +import net.terramodulus.mui.gui.agim.Menu +import net.terramodulus.mui.gui.agim.Screen +import net.terramodulus.mui.gui.agim.ScreenManager +import net.terramodulus.mui.gui.agim.event.MenuEvent +import net.terramodulus.mui.gui.agim.event.ScreenEvent +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.gfx.AlphaFilter +import net.terramodulus.mui.gui.gfx.Direction2S +import net.terramodulus.mui.gui.gfx.Direction6C +import net.terramodulus.mui.gui.gfx.GuiLine +import net.terramodulus.mui.gui.gfx.GuiRect +import net.terramodulus.mui.gui.gfx.RectangleD +import net.terramodulus.mui.gui.gfx.RenderSystem +import net.terramodulus.mui.gui.gfx.TextContext +import net.terramodulus.mui.kui.KeyboardInputHandler +import net.terramodulus.util.logging.logger +import net.terramodulus.void.World +import kotlin.math.PI +import kotlin.math.roundToInt +import kotlin.properties.Delegates +import kotlin.random.Random +import kotlin.random.nextInt +import kotlin.time.Duration.Companion.seconds +import kotlin.time.TimeSource +import kotlin.to + +private val WHITE = ImmVec4i(255, 255, 255, 255) +private val RED = ImmVec4i(255, 0, 0, 255) +private val GREEN = ImmVec4i(0, 255, 0, 255) +private val BLUE = ImmVec4i(0, 0, 255, 255) +private val STD_SCALE = ImmVec3d(.5, .5, .5) +private val IDENT_ROT = ImmQuatd(1.0, .0, .0, .0) +private const val MASS = 1.0 +private const val MAX_SPEED = PI * PI // reachable by autonomous movement +private const val MAX_ACC = PI * PI // without other forces, reaching MAX_SPEED in one second +private const val MOVE_EPSILON = .1 // smallest acc to apply +private const val MIN_GRAVITY = 1.0 +private const val MAX_GRAVITY = 20.0 +private const val MIN_FRICTION = 1.0 / 16.0 +private const val MAX_FRICTION = 64.0 +private const val MIN_ZOOM = 1.0 / 4.0 +private const val MAX_ZOOM = 4 + +private val logger = logger {} + +internal class GameplayScreen( + worldOptions: WorldCreateScreen.WorldOptions, + private val core: TerraModulus, + private val camera: Camera3D, + renderSystemHandle: RenderSystem.Handle, + managerHandle: ScreenManager.Handle, + asdHandle: AsdHandle.Container, + inputStatesHandle: InputStatesHandle, +) : Screen(managerHandle, asdHandle) { + private val geoShaders = camera.loadGeoShaders( + getResourceAsString("/gwr_geo.vsh"), + getResourceAsString("/gwr_geo.fsh"), + ) + + private val canvasHandle = renderSystemHandle.canvasHandle + + private lateinit var player: PlayerVoidGeom + override val layout = CompositeLayout(this) + private val mouseDebugTrackingLayer = MouseDebugTrackingLayer(renderSystemHandle, inputStatesHandle) + + private var hotkeysEnabled = false + + init { + renderSystemHandle.setBackgroundColor(0F, 0F, 0F, 0F) + managerHandle.open { p1: ScreenManager.Handle, p2: AsdHandle.Container, p3: RenderSystem.Handle -> + // In production, this screen should be placed separately. + WorldInitScreen(p1, p2, p3).apply { + core.world = World(object : World.Ymir.Builder { + override fun build(agent: World.YmirAgent) = Ymir(worldOptions, agent) + }, progressBar) + addListener(ScreenEvent.Close::class.java) { + this@GameplayScreen.layout.update { + add(SingletonLayout( + this@GameplayScreen, + GameplayRenderer(renderSystemHandle), + SingletonLayout.Config.Absolute.Full, + )) + add(SingletonLayout( + this@GameplayScreen, + SimplePane(ComponentAsdHandleImpl()) { + ColumnLayout.withElements( + ButtonComponent(ComponentAsdHandleImpl(), inputStatesHandle, { + CompositeLayout(this) { + add(SingletonLayout(this@ButtonComponent, GeomComponent( + GuiRect(canvasHandle, 0, 0, 1, 1, 122, 122, 0, 255), + RectangleD(0.0, 0.0, 1.0, 1.0), + ComponentAsdHandleImpl(), + ), SingletonLayout.Config.Absolute.Full)) + add(SingletonLayout( + this@ButtonComponent, TextDisplayComponent( + ComponentAsdHandleImpl(), + renderSystemHandle, + TextContext.Config(20F, 20F, ImmVec4i(255)), + ).apply { + text = "Query..." + }, SingletonLayout.Config.Sole(SingletonLayout.Config.Scaled.Scale(1.0)))) + } + }) { + managerHandle.addMenu { handle, asdHandle -> + object : Menu(handle, asdHandle) { + override val layout = with(this) menu@ { + fun command(label: String, action: () -> Unit) = + ButtonComponent(ComponentAsdHandleImpl(), inputStatesHandle, { + SingletonLayout(this@ButtonComponent, TextDisplayComponent( + ComponentAsdHandleImpl(), + renderSystemHandle, + TextContext.Config(20F, 20F, ImmVec4i(255)), + ).apply { text = label }, SingletonLayout.Config.Sole( + SingletonLayout.Config.Scaled.Scale(1.0) + )) + }) { + action() + exit() + } + SingletonLayout(this, SimplePane(ComponentAsdHandleImpl()) parent@ { + CompositeLayout(this) { + add(SingletonLayout(this@parent, GeomComponent( + GuiRect(canvasHandle, 0, 0, 1, 1, 122, 122, 128, 255), + RectangleD(0.0, 0.0, 1.0, 1.0), + ComponentAsdHandleImpl(), + ), SingletonLayout.Config.Absolute.Full)) + add(ColumnLayout.withComponents(listOf( + command("Position of Sphere", ::queryPos), + command("Velocity of Sphere", ::queryVec), + command("Gravity of World", ::queryGravity), + command("Friction of World", ::queryFriction), + ), config = SequenceLayout.Config( + Direction2S.Negative, + intrinsic = true, + ))(this@parent)) + } + }, SingletonLayout.Config.Auto( + SingletonLayout.Config.Auto.Side(Direction2S.Positive, 0.0), + SingletonLayout.Config.Auto.Side(Direction2S.Positive, 20.0), + )) + } + + init { + // just a quick hack but this certainly needs to be changed + asdHandle.properties.putProperty( + BoundsProperty.KEY, + BoundsProperty(this@GameplayScreen.asdHandle.rect) + ) + this@GameplayScreen.asdHandle.observeRect { + asdHandle.properties.putProperty( + BoundsProperty.KEY, + BoundsProperty(this@GameplayScreen.asdHandle.rect) + ) + } + + addListener(MenuEvent.Update::class.java) { + if (it.muiIoI.inputSystem.condition { keyboard { Escape.justDown } }) + exit() + } + } + + fun exit() = handle.removeMenu(this) + } + } + } to SequenceLayout.Element(1.0), + ButtonComponent(ComponentAsdHandleImpl(), inputStatesHandle, button@ { + CompositeLayout(this) { + add(SingletonLayout(this@button, GeomComponent(GuiRect(canvasHandle, + 0, 0, 1, 1, 122, 122, 255, 255, + ), RectangleD(0.0, 0.0, 1.0, 1.0), ComponentAsdHandleImpl()), + SingletonLayout.Config.Absolute.Full)) + add(SingletonLayout(this@button, TextDisplayComponent( + ComponentAsdHandleImpl(), + renderSystemHandle, + TextContext.Config(20F, 20F, ImmVec4i(255)), + ).apply { + text = "Reset Velocity to 0" + }, SingletonLayout.Config.Sole(SingletonLayout.Config.Scaled.Scale(1.0)))) + } + }, ::resetVel) to SequenceLayout.Element(1.0), + config = SequenceLayout.Config(Direction2S.Negative, intrinsic = true) + )(this) + }, + SingletonLayout.Config.Aligned( + SingletonLayout.Config.Scaled.Scale(1.0), + SingletonLayout.Config.AlignmentConfig(1.0, 1.0), + ), + )) + add(SingletonLayout( + this@GameplayScreen, + SimplePane(ComponentAsdHandleImpl()) { + CompositeLayout(this).apply { + add(SingletonLayout(this@SimplePane, GeomComponent( + GuiRect(canvasHandle, 0, 0, 1, 1, 10, 10, 255, 255), + RectangleD(0.0, 0.0, 1.0, 1.0), + ComponentAsdHandleImpl(), + ), SingletonLayout.Config.Absolute.Full)) + add(RowLayout.withElements( + SimplePane(ComponentAsdHandleImpl()) { + ColumnLayout.withComponents(listOf( + TextDisplayComponent(ComponentAsdHandleImpl(), renderSystemHandle, + TextContext.Config(20F, 20F, ImmVec4i(255)), + ).apply { text = "Legacy Hotkeys" }, + TextDisplayComponent(ComponentAsdHandleImpl(), renderSystemHandle, + TextContext.Config(20F, 20F, ImmVec4i(255)), + ).apply { text = "Mouse Debug Tracking" }, + TextDisplayComponent(ComponentAsdHandleImpl(), renderSystemHandle, + TextContext.Config(20F, 20F, ImmVec4i(255)), + ).apply { text = "Gravity Influence" }, + TextDisplayComponent(ComponentAsdHandleImpl(), renderSystemHandle, + TextContext.Config(20F, 20F, ImmVec4i(255)), + ).apply { text = "Gravity (-y)" }, + TextDisplayComponent(ComponentAsdHandleImpl(), renderSystemHandle, + TextContext.Config(20F, 20F, ImmVec4i(255)), + ).apply { text = "Friction Mode" }, + TextDisplayComponent(ComponentAsdHandleImpl(), renderSystemHandle, + TextContext.Config(20F, 20F, ImmVec4i(255)), + ).apply { text = "Friction (Limited mode)" }, + TextDisplayComponent(ComponentAsdHandleImpl(), renderSystemHandle, + TextContext.Config(20F, 20F, ImmVec4i(255)), + ).apply { text = "Zoom Level" }, + ), SequenceLayout.Config(Direction2S.Negative, intrinsic = true))(this) + } to SequenceLayout.Element(1.0), + SimplePane(ComponentAsdHandleImpl()) { + ColumnLayout.withComponents(listOf( + SizedPane(ComponentAsdHandleImpl(), CheckboxComponent( + ComponentAsdHandleImpl(), inputStatesHandle, canvasHandle + ) { hotkeysEnabled = it }, SizedPane.Config(20u, 20u)), + SizedPane(ComponentAsdHandleImpl(), CheckboxComponent( + ComponentAsdHandleImpl(), inputStatesHandle, canvasHandle, + ) { mouseDebugTrackingLayer.enabled = it }, SizedPane.Config(20u, 20u)), + SizedPane(ComponentAsdHandleImpl(), CheckboxComponent( + ComponentAsdHandleImpl(), + inputStatesHandle, + canvasHandle, + player.phyBody.gravityMode, + ) { player.phyBody.gravityMode = it }.apply { + gravityModeListener = { checked = player.phyBody.gravityMode } + }, SizedPane.Config(20u, 20u)), + SizedPane(ComponentAsdHandleImpl(), SimplePane(ComponentAsdHandleImpl()) + parent@ { + CompositeLayout(this).apply { + lateinit var listener: (Double) -> Unit + add(SingletonLayout(this@parent, SliderComponent( + canvasHandle, inputStatesHandle, ComponentAsdHandleImpl() + ) { + config(withRanged( + MIN_GRAVITY..MAX_GRAVITY, + -core.world!!.gravity.y, + ) { + core.world!!.gravity = core.world!!.gravity + .toMutVec3d().apply { y = -it } + listener(it) + }, xPos, ImmVec4i(123, 234, 56, 255), + ImmVec4i(50, 50, 250, 255), + ) + }.apply { + gravityListener = { + val v = -core.world!!.gravity.y + fraction = SliderComponent.SliderMode.Ranged.Transform + .Linear.project(v, MIN_GRAVITY..MAX_GRAVITY) + listener(v) + } + }, SingletonLayout.Config.Absolute.Full)) + add(SingletonLayout(this@parent, TextDisplayComponent( + ComponentAsdHandleImpl(), renderSystemHandle, + TextContext.Config(20F, 20F, ImmVec4i(255)) + ).apply { + listener = { it: Double -> + text = String.format("%.2f", it) + }.apply { this(-core.world!!.gravity.y) } + }, SingletonLayout.Config.Absolute.Full)) + } + }, SizedPane.Config(100u, 20u)), + ButtonComponent( + ComponentAsdHandleImpl(), + inputStatesHandle, + { + lateinit var layout: SingletonLayout + SingletonLayout(this, TextDisplayComponent( + ComponentAsdHandleImpl(), + renderSystemHandle, + TextContext.Config(20F, 20F, ImmVec4i(255)), + ).apply { + val listener = { + text = core.world!!.frictionMode.toString() + }.apply { this() } + frictionModeListener = { + layout.operate { listener() } + } + }, SingletonLayout.Config.Sole( + SingletonLayout.Config.Scaled.Scale(1.0) + )).apply { layout = this } + }, + ) { + core.world!!.frictionMode = World.FrictionMode.entries[ + (core.world!!.frictionMode.ordinal + 1) % World.FrictionMode.entries.size + ] + frictionModeListener() + }, + SizedPane(ComponentAsdHandleImpl(), SimplePane(ComponentAsdHandleImpl()) + parent@ { + CompositeLayout(this).apply { + lateinit var listener: (Double) -> Unit + add(SingletonLayout(this@parent, SliderComponent( + canvasHandle, inputStatesHandle, ComponentAsdHandleImpl() + ) { + config(withRanged( + MIN_FRICTION..MAX_FRICTION, + core.world!!.friction, + transformLinearExponential(2.0), + ) { + core.world!!.friction = it + listener(it) + }, xPos, ImmVec4i(123, 234, 56, 255), + ImmVec4i(50, 50, 250, 255), + ) + }.apply { + frictionListener = { + val v = core.world!!.friction + fraction = SliderComponent.SliderMode.Ranged.Transform + .LinearExponential(2.0) + .project(v, MIN_FRICTION..MAX_FRICTION) + listener(v) + } + }, SingletonLayout.Config.Absolute.Full)) + add(SingletonLayout(this@parent, TextDisplayComponent( + ComponentAsdHandleImpl(), renderSystemHandle, + TextContext.Config(20F, 20F, ImmVec4i(255)) + ).apply { + listener = { it: Double -> + text = String.format("%.2f", it) + }.apply { this(core.world!!.friction) } + }, SingletonLayout.Config.Absolute.Full)) + } + }, SizedPane.Config(100u, 20u)), + SimplePane(ComponentAsdHandleImpl()) { + lateinit var listener1: () -> Unit + lateinit var listener2: () -> Unit + lateinit var listenerTxt: () -> Unit + zoomLvlListener = { + listener1() + listener2() + listenerTxt() + } + val filter1 = AlphaFilter(1F) + val filter2 = AlphaFilter(1F) + lateinit var layout: RowLayout + RowLayout.withComponents(listOf( + ButtonComponent( + ComponentAsdHandleImpl(), inputStatesHandle, + { + SingletonLayout( + this, DrawablesComponent( + sequenceOf( + DrawablesComponent.Drawable( + GuiLine(canvasHandle, + 1, 2, 3, 2, 255, 255, 255, 255 + ) + ), + ), RectangleD( + 0.0, 0.0, 4.0, 4.0 + ), ComponentAsdHandleImpl() + ).apply { + addFilter(filter1) + listener1 = { + if (camera.zoomLevel > MIN_ZOOM) + filter1.alpha = 1F + else + filter1.alpha = .5F + } + }, + SingletonLayout.Config.Sole( + SingletonLayout.Config.Scaled.Scale(20 / 4.0) + ) + ) + }, + ) { + if (camera.zoomLevel > MIN_ZOOM) { + camera.zoomLevel /= 2 + zoomLvlListener() + } + }, + TextDisplayComponent(ComponentAsdHandleImpl(), + renderSystemHandle, + TextContext.Config(20F, 20F, ImmVec4i(255)), + ).apply { + val listener = { + text = "${camera.zoomLevel}" + }.apply { this() } + listenerTxt = { + layout.operate { listener() } + } + }, + ButtonComponent( + ComponentAsdHandleImpl(), inputStatesHandle, + { + SingletonLayout( + this, DrawablesComponent( + sequenceOf( + DrawablesComponent.Drawable( + GuiLine( + canvasHandle, + 1, 2, 3, 2, 255, 255, 255, 255 + ) + ), + DrawablesComponent.Drawable( + GuiLine( + canvasHandle, + 2, 1, 2, 3, 255, 255, 255, 255 + ) + ), + ), RectangleD( + 0.0, 0.0, 4.0, 4.0 + ), ComponentAsdHandleImpl() + ).apply { + addFilter(filter2) + listener2 = { + if (camera.zoomLevel < MAX_ZOOM) + filter2.alpha = 1F + else + filter2.alpha = .5F + } + }, + SingletonLayout.Config.Sole( + SingletonLayout.Config.Scaled.Scale(20 / 4.0) + ) + ) + }, + ) { + if (camera.zoomLevel < MAX_ZOOM) { + camera.zoomLevel *= 2 + zoomLvlListener() + } + }, + ), SequenceLayout.Config(Direction2S.Positive, intrinsic = true) + )(this).apply { layout = this } + }, + ), SequenceLayout.Config(Direction2S.Negative, intrinsic = true))(this) + } to SequenceLayout.Element(1.0), + config = SequenceLayout.Config(Direction2S.Positive, 2.0, 2.0, true), + )(this@SimplePane)) + } + }, + SingletonLayout.Config.Aligned( + SingletonLayout.Config.Scaled.Scale(1.0), + SingletonLayout.Config.AlignmentConfig(0.0, 1.0), + ), + )) + add(SingletonLayout( + this@GameplayScreen, + mouseDebugTrackingLayer, + SingletonLayout.Config.Absolute.Full, + )) + } + this@GameplayScreen.addListener(ScreenEvent.Update::class.java) { + update0(it.muiIoI) + } + } + } + } + } + + private inner class MouseDebugTrackingLayer( + private val handle: RenderSystem.Handle, + inputStatesHandle: InputStatesHandle, + ) : Component(ComponentAsdHandleImpl()) { + private var prevPos: Vec2d? = null + private var label: TextContext? = null + private val lines = ArrayDeque() + private val timeSource = TimeSource.Monotonic + private val mouseCtxStates = MouseCtxStates(inputStatesHandle.mouseGlobalStates, asdHandle) + private val threshold = 3.seconds + var enabled: Boolean by Delegates.observable(false) { _, _, newValue -> if (!newValue) lines.clear() } + + private inner class Element(val timestamp: TimeSource.Monotonic.ValueTimeMark, val geom: GuiLine) + + init { + asdHandle.observeRect { + lines.clear() + prevPos = null + label = null + } + mouseCtxStates.addListener(MouseState.Listener( + setOf(MouseState.Trigger(MouseState.Key.Movement) { true }) + ) { + if (enabled) when (it) { + is MouseState.Movement -> { + if (prevPos != null) { + val handle = handle.canvasHandle + // Standard tracking aligned with SDL + lines.add(Element( + timeSource.markNow(), + GuiLine(handle, prevPos!!.xi, prevPos!!.yi, it.pos.xi, it.pos.yi, 255, 165, 0, 255), + )) + // Secondary tracking by relative values from event polling + val x = (prevPos!!.xd + it.delX).roundToInt() + val y = (prevPos!!.yd + it.delY).roundToInt() + lines.add(Element( + timeSource.markNow(), + GuiLine(handle, prevPos!!.xi, prevPos!!.yi, x, y, 0, 255, 0, 255), + )) + } + prevPos = it.pos + (label ?: TextContext(handle, TextContext.Config(16F, 16F, ImmVec4i(255))).apply { + label = this + }).apply { + setText("(${it.pos.x}, ${it.pos.y})") + update(RectangleD(it.pos.x, it.pos.y, asdHandle.rect.width, asdHandle.rect.height)) + } + } + else -> throw AssertionError() + } + }) + } + + override fun render(renderSystem: RenderSystem) { + if (enabled) { + val now = timeSource.markNow() + lines.iterator().apply { + while (hasNext()) { + val it = next() + if (now - it.timestamp > threshold) remove() + else it.geom.render(renderSystem) + } + } + label?.render() + } + } + } + + private inner class Ymir( + private val options: WorldCreateScreen.WorldOptions, + private val agent: World.YmirAgent, + ) : World.Ymir { + private val cubeGeom = SimpleMesh3dGeomCube(canvasHandle.canvas, 2F) + private val sphereGeom = SimpleMesh3dGeomSphere(canvasHandle.canvas, 1F) + + override fun wrapCube(phyGeom: PhyGeom, pos: Vec3d) = + EnvVoidGeom(phyGeom, WorldObjDrawable(cubeGeom, randomColor(), pos, STD_SCALE, IDENT_ROT), pos) + + private fun randomColor() = when (Random.nextInt(3)) { + 0 -> RED + 1 -> GREEN + 2 -> BLUE + else -> throw AssertionError("Invalid color") + } + + override fun wrapChar(phyBody: PhyBody, pos: Vec3d) = + PlayerVoidGeom(phyBody, WorldObjDrawable(sphereGeom, WHITE, pos, STD_SCALE, IDENT_ROT)).apply { + player = this + } + + override fun generateWorld(progressBar: World.ProgressBar) { + when (options.worldType) { + WorldCreateScreen.WorldOptions.WorldType.CubeSets -> { + // Spawn point + agent.genCube(this, ImmVec3d(.0)) + // Main Character + agent.genChar(this, ImmVec3d(0.0, 1.0, 0.0)) + // Test Objects + randomCubes(progressBar) + } + WorldCreateScreen.WorldOptions.WorldType.Flat -> { + // Main Character + agent.genChar(this, ImmVec3d(0.0, 1.0, 0.0)) + // Floor + val radius = 100 + for (x in -radius..radius) { + progressBar.setProgress(x / (radius * 2 + 1).toDouble() * .9) + for (z in -radius..radius) { + agent.genCube(this, ImmVec3d(x.toDouble(), .0, z.toDouble())) + } + } + // Random Walls + for (x in -radius..radius) { + progressBar.setProgress(x / 5.toDouble() * .1 + .9) + for (z in -radius..radius) { + if (x != 0 || z != 0) + if (Random.nextInt(10) < 1) + agent.genCube(this, ImmVec3d(x.toDouble(), 1.0, z.toDouble())) + } + } + progressBar.setProgress(1.0) + } + } + // TODO char type + } + + // Reference: https://en.wikipedia.org/wiki/Maze_generation_algorithm + private fun randomCubes(progressBar: World.ProgressBar) { + var i = 0 +// val radius = 12 + val radius = 5 + val total = radius * radius * 2 * 2 * 7 + val intervalHor = 5.0 + val intervalVert = 8 + val max = 5 * 5 * 5 // 125 for each set + val directions = arrayOf( + ImmVec3d(1.0, 0.0, 0.0), + ImmVec3d(-1.0, 0.0, 0.0), + ImmVec3d(0.0, 1.0, 0.0), + ImmVec3d(0.0, -1.0, 0.0), + ImmVec3d(0.0, 0.0, 1.0), + ImmVec3d(0.0, 0.0, -1.0), + ) + for (x in 1..radius) { + for (y in -3..3) { + for (z in 1..radius) { + for (xs in booleanArrayOf(false, true)) { + for (zs in booleanArrayOf(false, true)) { + progressBar.setProgress(++i / total.toDouble()) + val xx = (if (xs) x else -x).toDouble() * intervalHor + val zz = (if (zs) z else -z).toDouble() * intervalHor + val yy = (y * intervalVert).toDouble() + val origin = ImmVec3d(xx, yy + Random.nextInt(-3..3).toDouble(), zz) + val visited = mutableSetOf(ImmVec3d(0.0, 0.0, 0.0)) + val heads = ArrayDeque() + heads.addLast(ImmVec3d(0.0, 0.0, 0.0)) + while (!heads.isEmpty()) { + val head = heads.removeFirst() + for (d in directions) { + val cur = head + d + if (Random.nextInt(max) > visited.size && cur !in visited) { + visited.add(cur) + if (Random.nextInt(max) > visited.size) { + heads.addLast(cur) + } + } + } + } + for (p in visited) { + val pt = origin + p + agent.genCube(this, pt) + } + } + } + } + } + } + } + } + + private abstract inner class VoidGeom(val drawable: WorldObjDrawable) : World.VoidGeom { + override fun render() { + renderGwrGeo(drawable) + } + } + + private inner class EnvVoidGeom(override val phyGeom: PhyGeom, drawable: WorldObjDrawable, override val pos: Vec3d) : + VoidGeom(drawable), World.EnvVoidGeom + + private inner class PlayerVoidGeom( + override val phyBody: PhyBody, + drawable: WorldObjDrawable, + ) : VoidGeom(drawable), World.PlayerVoidGeom { + fun move(dir: Vec3d) { + if (dir == ZeroImmVec3d) return // avoid math errors and computations + val dir = ImmVec3d(dir.x, dir.y, dir.z).normalized() + val curVel = phyBody.linearVel + // Let d be the unit vector of autonomous movement target direction, + // v_c be the current velocity of body, + // v_p be the scalar projection of v_c on d. + // v_p = v_c * d, may be negative + // Autonomous acceleration is made only if v_p < MAX_SPEED. + val projVel = curVel dot dir + if (projVel < MAX_SPEED) { + // Let v_d be the delta velocity in direction of d, + // a_d be the delta acceleration to be made. + // v_t = MAX_SPEED - v_p, must be positive + // a_d = dir * clamp(v_t / 1 s, EPSILON, MAX) + val deltaVel = MAX_SPEED - projVel + val deltaAcc = dir * deltaVel.coerceIn(MOVE_EPSILON, MAX_ACC) + phyBody.addForce(deltaAcc * MASS) + } + } + + override fun render() { + drawable.setPos(phyBody.pos) + camera.refreshPos(phyBody.pos.toImmVec3f().toArray()) + super.render() + } + + override var pos: Vec3d by phyBody::pos + } + + private fun Vec3f.toArray() = floatArrayOf(x, y, z) + + private fun Direction6C.toKey() = when (this) { + Direction6C.North -> KeyboardInputHandler.Keys.W + Direction6C.South -> KeyboardInputHandler.Keys.S + Direction6C.West -> KeyboardInputHandler.Keys.A + Direction6C.East -> KeyboardInputHandler.Keys.D + Direction6C.Up -> KeyboardInputHandler.Keys.Space + Direction6C.Down -> KeyboardInputHandler.Keys.LShift + } + + private fun Direction6C.toVector() = when (this) { + Direction6C.North -> ImmVec3d(.0, .0, -1.0) + Direction6C.South -> ImmVec3d(.0, .0, 1.0) + Direction6C.West -> ImmVec3d(-1.0, .0, .0) + Direction6C.East -> ImmVec3d(1.0, .0, .0) + Direction6C.Up -> ImmVec3d(.0, 1.0, .0) + Direction6C.Down -> ImmVec3d(.0, -1.0, .0) + } + + private fun Vec3d.display() = "[$x, $y, $z]" + + /** + * Query position of sphere + */ + private fun queryPos() { + logger.info { "Position: ${player.pos.display()}" } + } + + /** + * Query velocity of sphere + * + * Note: Acceleration is hard to be queried as force is zeroed after each world step + */ + private fun queryVec() { + logger.info { "Velocity: ${player.phyBody.linearVel.display()}" } + } + + /** + * Query gravity of world and gravity mode of (influence to) sphere + */ + private fun queryGravity() { + logger.info { "Gravity: ${core.world!!.gravity.display()}; influence: ${player.phyBody.gravityMode}" } + } + + /** + * Query friction states + */ + private fun queryFriction() { + logger.info { "Friction: ${core.world!!.friction}; mode: ${core.world!!.frictionMode}" } + } + + /** + * Reset velocity of sphere to zero + */ + private fun resetVel() { + player.phyBody.linearVel = ZeroImmVec3d + logger.info { "Reset velocity to zero" } + } + + private lateinit var gravityModeListener: () -> Unit + private lateinit var gravityListener: () -> Unit + private lateinit var frictionModeListener: () -> Unit + private lateinit var frictionListener: () -> Unit + private lateinit var zoomLvlListener: () -> Unit + + private fun update0(muiIoI: ScreenManager.MuiIoI) { + val inputSystem = muiIoI.inputSystem + if (hotkeysEnabled) { // Those keys are not related to GUI, so they are fine to be here. + if (inputSystem.condition { keyboard { Q.justDown } }) queryPos() + if (inputSystem.condition { keyboard { R.justDown } }) queryVec() + if (inputSystem.condition { keyboard { U.justDown } }) queryGravity() + if (inputSystem.condition { keyboard { I.justDown } }) { + // Toggle gravity mode of (influence to) sphere + player.phyBody.gravityMode = !player.phyBody.gravityMode + logger.info { "Gravity influence toggled: ${player.phyBody.gravityMode}" } + gravityModeListener() + } + if (inputSystem.condition { keyboard { O.justDown } }) { + // Increase world gravity + if (-core.world!!.gravity.y < MAX_GRAVITY) { + core.world!!.gravity *= 2.0 + logger.info { + "Gravity increased: ${core.world!!.gravity.display()}".let { + if (!player.phyBody.gravityMode) "$it (ineffective)" else it + } + } + gravityListener() + } else { + logger.info { + "Gravity maximized: ${core.world!!.gravity.display()}".let { + if (!player.phyBody.gravityMode) "$it (ineffective)" else it + } + } + } + } + if (inputSystem.condition { keyboard { P.justDown } }) { + // Decrease world gravity + if (-core.world!!.gravity.y > MIN_GRAVITY) { + core.world!!.gravity /= 2.0 + logger.info { + "Gravity decreased: ${core.world!!.gravity.display()}".let { + if (!player.phyBody.gravityMode) "$it (ineffective)" else it + } + } + gravityListener() + } else { + logger.info { + "Gravity minimized: ${core.world!!.gravity.display()}".let { + if (!player.phyBody.gravityMode) "$it (ineffective)" else it + } + } + } + } + if (inputSystem.condition { keyboard { J.justDown } }) queryFriction() + if (inputSystem.condition { keyboard { K.justDown } }) { + // Toggle friction mode + core.world!!.frictionMode = World.FrictionMode.entries[ + (core.world!!.frictionMode.ordinal + 1) % World.FrictionMode.entries.size + ] + logger.info { + "Friction mode toggled: ${core.world!!.frictionMode}".let { + if (core.world!!.frictionMode == World.FrictionMode.Limited) "$it ; friction: ${core.world!!.friction}" else it + } + } + frictionModeListener() + } + if (inputSystem.condition { keyboard { L.justDown } }) { + // Increase friction (for Limited mode) + if (core.world!!.friction < MAX_FRICTION) { + core.world!!.friction *= 2 + logger.info { + "Friction increased: ${core.world!!.friction}".let { + if (core.world!!.frictionMode != World.FrictionMode.Limited) "$it (ineffective)" else it + } + } + frictionListener() + } else { + logger.info { + "Friction maximized: ${core.world!!.friction}".let { + if (core.world!!.frictionMode != World.FrictionMode.Limited) "$it (ineffective)" else it + } + } + } + } + if (inputSystem.condition { keyboard { M.justDown } }) { + // Decrease friction (for Limited mode) + if (core.world!!.friction > MIN_FRICTION) { + core.world!!.friction /= 2 + logger.info { + "Friction decreased: ${core.world!!.friction}".let { + if (core.world!!.frictionMode != World.FrictionMode.Limited) "$it (ineffective)" else it + } + } + frictionListener() + } else { + logger.info { + "Friction minimized: ${core.world!!.friction}".let { + if (core.world!!.frictionMode != World.FrictionMode.Limited) "$it (ineffective)" else it + } + } + } + } + if (inputSystem.condition { keyboard { N.justDown } }) resetVel() + // This is problematic and difficult to be resolved. + // if (inputSystem.condition { Z.justDown() }) { + // // Reset position of sphere to spawn point + // player.pos = Vec3D(0.0, 1.0, 0.0) + // logger.info { "Reset position to spawn point" } + // } + if (inputSystem.condition { keyboard { Equals.justDown } }) { + // Zoom in camera + if (camera.zoomLevel < MAX_ZOOM) { + camera.zoomLevel *= 2 + logger.info { "Zoomed in: ${camera.zoomLevel}" } + zoomLvlListener() + } else { + logger.info { "Zoom maximized: ${camera.zoomLevel}" } + } + } + if (inputSystem.condition { keyboard { Minus.justDown } }) { + // Zoom out camera + if (camera.zoomLevel > MIN_ZOOM) { + camera.zoomLevel /= 2 + logger.info { "Zoomed out: ${camera.zoomLevel}" } + zoomLvlListener() + } else { + logger.info { "Zoom minimized: ${camera.zoomLevel}" } + } + } + } + + val dirs = ArrayList() + Direction6C.entries.forEach { if (inputSystem.condition { keyboard { it.toKey().down } }) dirs.add(it.toVector()) } + player.move(dirs.fold(ZeroImmVec3d, Vec3d::plus)) + } + + private inner class GameplayRenderer(renderSystemHandle: RenderSystem.Handle) : + Component(ComponentAsdHandleImpl()) { + private val tpsText = TextContext(renderSystemHandle, TextContext.Config(16F, 16F, ImmVec4i(255))) + + init { + asdHandle.observeRect { + tpsText.update(asdHandle.rect) + } + } + + override fun render(renderSystem: RenderSystem) { + if (core.world != null) { +// val range = camera.getSpace() * 1.1 // with little tolerance +// val ceil = 3 +// val floor = 10 +// worldStates.pos = player.pos.toMutVec3d().apply { y -= floor - (ceil + floor).toDouble() / 2 } +// worldStates.dims = ImmVec3d(range.x, (ceil + floor).toDouble(), range.y) + core.world!!.objects.values.sortedWith( + compareBy { it.pos.y }.thenBy { it.pos.z } + ).forEach { it.render() } + } + + tpsText.setText("${core.tps} FPS") + tpsText.render() + } + } + + internal fun renderGwrGeo(drawable: WorldObjDrawable) = camera.renderGwrGeo(drawable, geoShaders) +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GeomComponent.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GeomComponent.kt new file mode 100644 index 00000000..01044fa5 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GeomComponent.kt @@ -0,0 +1,36 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.impl + +import net.terramodulus.mui.gui.agim.Component +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.gfx.GeneralTransform +import net.terramodulus.mui.gui.gfx.GuiGeometry +import net.terramodulus.mui.gui.gfx.RectStParams +import net.terramodulus.mui.gui.gfx.RectangleD +import net.terramodulus.mui.gui.gfx.RectangleI +import net.terramodulus.mui.gui.gfx.RenderSystem + +class GeomComponent( + val geom: GuiGeometry, + private val bounds: RectangleD, + asdHandle: AsdHandle, +) : Component(asdHandle) { + private val transform = GeneralTransform().apply { geom.add(this) } + + init { + val dim = IntrinsicDimensionsProperty(bounds.width.toUInt(), bounds.height.toUInt()) + asdHandle.properties.putProperty(IntrinsicDimensionsProperty.KEY, dim) + asdHandle.properties.putProperty(IntrinsicRatioProperty.KEY, dim.computeRatio()) + asdHandle.observeRect { + RectStParams.fromRects(bounds, asdHandle.rect).applyToGeneralTransform(transform) + } + } + + override fun render(renderSystem: RenderSystem) { + geom.render(renderSystem) + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GraphicsComponent.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GraphicsComponent.kt new file mode 100644 index 00000000..824c76cd --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GraphicsComponent.kt @@ -0,0 +1,151 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.impl + +import com.cout970.math.vec4.Vec4i +import net.terramodulus.mui.gui.agim.AbstractPane +import net.terramodulus.mui.gui.agim.Component +import net.terramodulus.mui.gui.agim.Layout +import net.terramodulus.mui.gui.agim.impl.DrawablesComponent.Drawable +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.gfx.Anchor5 +import net.terramodulus.mui.gui.gfx.ColorFilter +import net.terramodulus.mui.gui.gfx.GeneralTransform +import net.terramodulus.mui.gui.gfx.GuiGeometry +import net.terramodulus.mui.gui.gfx.GuiRect +import net.terramodulus.mui.gui.gfx.GuiSprite +import net.terramodulus.mui.gui.gfx.ModelTransform +import net.terramodulus.mui.gui.gfx.RectStParams +import net.terramodulus.mui.gui.gfx.RectangleD +import net.terramodulus.mui.gui.gfx.RenderSystem +import kotlin.math.roundToInt +import kotlin.sequences.forEach + +sealed interface GraphicsComponent + +class SpriteComponent(val sprite: GuiSprite, asdHandle: AsdHandle) : Component(asdHandle), GraphicsComponent { + private val transform = GeneralTransform().apply { sprite.add(this) } + + init { + val dim = IntrinsicDimensionsProperty(sprite.rect.width.toUInt(), sprite.rect.height.toUInt()) + asdHandle.properties.putProperty(IntrinsicDimensionsProperty.KEY, dim) + asdHandle.properties.putProperty(IntrinsicRatioProperty.KEY, dim.computeRatio()) + asdHandle.observeRect { + RectStParams.fromRects(sprite.rect.toDouble(), asdHandle.rect).applyToGeneralTransform(transform) + } + } + + override fun render(renderSystem: RenderSystem) { + sprite.render(renderSystem) + } +} + +class DrawablesComponent( + val drawables: Sequence, + private val bounds: RectangleD, + asdHandle: AsdHandle, +) : Component(asdHandle) { + private val transform = GeneralTransform().apply { addTransform(this) } + + fun addTransform(transform: ModelTransform) = drawables.forEach { drawable -> + when (drawable) { + is Drawable.Geom -> drawable.geom.add(transform) + is Drawable.Sprite -> drawable.sprite.add(transform) + } + } + + fun addFilter(filter: ColorFilter) = drawables.forEach { drawable -> + when (drawable) { + is Drawable.Geom -> drawable.geom.add(filter) + is Drawable.Sprite -> drawable.sprite.add(filter) + } + } + + sealed interface Drawable { + companion object { + operator fun invoke(geom: GuiGeometry) = Geom(geom) + operator fun invoke(sprite: GuiSprite) = Sprite(sprite) + } + + class Geom(val geom: GuiGeometry) : Drawable + class Sprite(val sprite: GuiSprite) : Drawable + } + + init { + val dim = IntrinsicDimensionsProperty(bounds.width.toUInt(), bounds.height.toUInt()) + asdHandle.properties.putProperty(IntrinsicDimensionsProperty.KEY, dim) + asdHandle.properties.putProperty(IntrinsicRatioProperty.KEY, dim.computeRatio()) + asdHandle.observeRect { + RectStParams.fromRects(bounds, asdHandle.rect).applyToGeneralTransform(transform) + } + } + + override fun render(renderSystem: RenderSystem) { + drawables.forEach { drawable -> + when (drawable) { + is Drawable.Geom -> drawable.geom.render(renderSystem) + is Drawable.Sprite -> drawable.sprite.render(renderSystem) + } + } + } +} + +/** + * @param bounds Outer bounds of the outline + */ +class OutlineComponent( + canvasHandle: RenderSystem.CanvasHandle, + private val bounds: RectangleD, + breadth: Double, + color: Vec4i, + asdHandle: AsdHandle, +) : Component(asdHandle) { + private val transform = GeneralTransform() + private val geoms: Array + + init { + val tr = bounds.anchor(Anchor5.TopRight) + val bl = bounds.anchor(Anchor5.BottomLeft) + geoms = arrayOf( + GuiRect(canvasHandle, + bl.x.roundToInt(), (tr.y - breadth).roundToInt(), (tr.x - breadth).roundToInt(), tr.y.roundToInt(), + color.x, color.y, color.z, color.w, + ), + GuiRect(canvasHandle, + (tr.x - breadth).roundToInt(), (bl.y + breadth).roundToInt(), tr.x.roundToInt(), tr.y.roundToInt(), + color.x, color.y, color.z, color.w, + ), + GuiRect(canvasHandle, + (bl.x + breadth).roundToInt(), bl.y.roundToInt(), tr.x.roundToInt(), (bl.y + breadth).roundToInt(), + color.x, color.y, color.z, color.w, + ), + GuiRect(canvasHandle, + bl.x.roundToInt(), bl.y.roundToInt(), (bl.x + breadth).roundToInt(), (bl.y - breadth).roundToInt(), + color.x, color.y, color.z, color.w, + ), + ) + geoms.forEach { it.add(transform) } + + val dim = IntrinsicDimensionsProperty(bounds.width.toUInt(), bounds.height.toUInt()) + asdHandle.properties.putProperty(IntrinsicDimensionsProperty.KEY, dim) + asdHandle.properties.putProperty(IntrinsicRatioProperty.KEY, dim.computeRatio()) + asdHandle.observeRect { + RectStParams.fromRects(bounds, asdHandle.rect).applyToGeneralTransform(transform) + } + } + + fun addFilter(filter: ColorFilter) = geoms.forEach { it.add(filter) } + + override fun render(renderSystem: RenderSystem) { + geoms.forEach { it.render(renderSystem) } + } +} + +class CanvasComponent(override val layout: Layout, asdHandle: AsdHandle) : AbstractPane(asdHandle), GraphicsComponent { + override fun render(renderSystem: RenderSystem) { + TODO("Not yet implemented") + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/IntrinsicProperties.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/IntrinsicProperties.kt new file mode 100644 index 00000000..6f71f9bd --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/IntrinsicProperties.kt @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.impl + +import net.terramodulus.mui.gui.agim.AgimoProperty +import net.terramodulus.mui.gui.agim.AgimoPropertyMap +import net.terramodulus.util.gcd + +/** + * @constructor Simple constructor without any preprocessing + */ +data class IntrinsicRatioProperty(val width: UInt, val height: UInt) : AgimoProperty() { + companion object { + val KEY = AgimoPropertyMap.Key(IntrinsicRatioProperty::class.java) + fun compute(width: UInt, height: UInt): IntrinsicRatioProperty { + val gcd = gcd(width, height) + return IntrinsicRatioProperty(width / gcd, height / gcd) + } + } +} + +data class IntrinsicDimensionsProperty(val width: UInt, val height: UInt) : AgimoProperty() { + companion object { + val KEY = AgimoPropertyMap.Key(IntrinsicDimensionsProperty::class.java) + } + fun computeRatio() = IntrinsicRatioProperty.compute(width, height) +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/LaunchingScreen.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/LaunchingScreen.kt new file mode 100644 index 00000000..a0ea674b --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/LaunchingScreen.kt @@ -0,0 +1,83 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.impl + +import net.terramodulus.mui.gui.agim.Screen +import net.terramodulus.mui.gui.agim.ScreenManager +import net.terramodulus.mui.gui.agim.event.ScreenEvent +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.gfx.AlphaFilter +import net.terramodulus.mui.gui.gfx.GuiRect +import net.terramodulus.mui.gui.gfx.GuiSprite +import net.terramodulus.mui.gui.gfx.RectangleD +import net.terramodulus.mui.gui.gfx.RectangleI +import net.terramodulus.mui.gui.gfx.RenderSystem + +private val BG_COLOR = floatArrayOf(.145F, .776F, .768F) + +private const val ANI_DURATION = .75F // in second + +private const val PAUSE_DURATION = 2 // in second + +internal class LaunchingScreen( + managerHandle: ScreenManager.Handle, + asdHandle: AsdHandle.Container, + renderSystemHandle: RenderSystem.Handle, +) : Screen(managerHandle, asdHandle) { + private var stage = 0 + private var last = System.currentTimeMillis() // timestamp in milliseconds + private var alphaFilter = AlphaFilter(0F) + override val layout = CompositeLayout(this) + + init { + layout.update { + add(SingletonLayout(this@LaunchingScreen, GeomComponent(GuiRect( + renderSystemHandle.canvasHandle, 0, 0, 1, 1, 37, 198, 196, 255 + ), RectangleD(0.0, 0.0, 1.0, 1.0), ComponentAsdHandleImpl()).apply { + geom.add(alphaFilter) + }, SingletonLayout.Config.Absolute.Full)) + add(SingletonLayout(this@LaunchingScreen, SpriteComponent(GuiSprite( + renderSystemHandle.canvasHandle, + RectangleI(0, 0, 512, 128), + renderSystemHandle.loadTexture("/studio_logo.png"), + ), ComponentAsdHandleImpl()).apply { + sprite.add(alphaFilter) + }, SingletonLayout.Config.Aligned( + SingletonLayout.Config.ObjectFit.Contain, + SingletonLayout.Config.AlignmentConfig.DEFAULT, + ))) + } + + addListener(ScreenEvent.Update::class.java) { + val current = System.currentTimeMillis() + val elapsed = (current - last) / 1000F // elapsed time for this stage + when (stage) { + 0 -> if (elapsed >= ANI_DURATION) { + stage = 1 + last = current + alphaFilter.alpha = 1F + } else { + alphaFilter.alpha = elapsed / ANI_DURATION + } + + 1 -> if (elapsed >= PAUSE_DURATION) { + stage = 2 + last = current + } + + 2 -> if (elapsed >= ANI_DURATION) { + stage = 3 + last = current + alphaFilter.alpha = 0F + } else { + alphaFilter.alpha = 1 - elapsed / ANI_DURATION + } + + 3 -> it.muiIoI.screenManager.handle.reset(::ResourceLoadingScreen) + } + } + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/RectangleProperty.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/RectangleProperty.kt new file mode 100644 index 00000000..1e17b41c --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/RectangleProperty.kt @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.impl + +import net.terramodulus.mui.gui.agim.AgimoProperty +import net.terramodulus.mui.gui.agim.AgimoPropertyMap +import net.terramodulus.mui.gui.gfx.RectangleD + +data class RectangleProperty(val value: RectangleD) : AgimoProperty() { + companion object { + val KEY = AgimoPropertyMap.Key(RectangleProperty::class.java) + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/ResourceLoadingScreen.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/ResourceLoadingScreen.kt new file mode 100644 index 00000000..1a8051d5 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/ResourceLoadingScreen.kt @@ -0,0 +1,123 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.impl + +import net.terramodulus.engine.common.ZeroImmVec3f +import net.terramodulus.mui.gui.agim.Screen +import net.terramodulus.mui.gui.agim.ScreenManager +import net.terramodulus.mui.gui.agim.event.ScreenEvent +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.gfx.AlphaFilter +import net.terramodulus.mui.gui.gfx.Dimension2I +import net.terramodulus.mui.gui.gfx.GuiRect +import net.terramodulus.mui.gui.gfx.GuiSprite +import net.terramodulus.mui.gui.gfx.Rectangle +import net.terramodulus.mui.gui.gfx.RectangleD +import net.terramodulus.mui.gui.gfx.RectangleI +import net.terramodulus.mui.gui.gfx.RenderSystem +import kotlin.math.min +import kotlin.math.pow +import kotlin.properties.Delegates + +private val BG_COLOR = floatArrayOf(.145F, .776F, 0.768F) + +private const val ANI_DURATION = 1F // in second +private const val PAUSE_DURATION = 2F // in second + +class ResourceLoadingScreen( + managerHandle: ScreenManager.Handle, + asdHandle: AsdHandle.Container, + renderSystemHandle: RenderSystem.Handle, +) : Screen(managerHandle, asdHandle) { + private var stage = 0 + private var last = System.currentTimeMillis() // timestamp in milliseconds + private var alphaFilter = AlphaFilter(0F) + private val progressBar = ProgressBar(renderSystemHandle) + override val layout = CompositeLayout(this) + + init { + layout.update { + add(SingletonLayout(this@ResourceLoadingScreen, GeomComponent(GuiRect( + renderSystemHandle.canvasHandle, 0, 0, 1, 1, 0, 255, 213, 255 + ), RectangleD(0.0, 0.0, 1.0, 1.0), ComponentAsdHandleImpl()).apply { + geom.add(alphaFilter) + }, SingletonLayout.Config.Absolute.Full)) + add(SingletonLayout(this@ResourceLoadingScreen, SimplePane(ComponentAsdHandleImpl()) { + SingletonLayout(this, DrawablesComponent(sequenceOf( + DrawablesComponent.Drawable( + GuiSprite( + renderSystemHandle.canvasHandle, + RectangleI(0, 100, 400, 100), + renderSystemHandle.loadTexture("/game_logo.png") + ) + ), + DrawablesComponent.Drawable( + GuiRect(renderSystemHandle.canvasHandle, 0, 0, 400, 40, 240, 240, 240, 255) + ), + DrawablesComponent.Drawable( + GuiRect(renderSystemHandle.canvasHandle, 5, 5, 395, 35, 0, 255, 213, 255) + ), + DrawablesComponent.Drawable(progressBar.rect), + ), RectangleD(0.0, 0.0, 400.0, 200.0), ComponentAsdHandleImpl()).apply { + addFilter(alphaFilter) + }, SingletonLayout.Config.Aligned( + SingletonLayout.Config.ObjectFit.Contain, + SingletonLayout.Config.AlignmentConfig.DEFAULT, + )) + }, SingletonLayout.Config.Aligned( + SingletonLayout.Config.Relative.Simple(0.5), + SingletonLayout.Config.AlignmentConfig.DEFAULT, + ))) + } + + addListener(ScreenEvent.Update::class.java) { + val current = System.currentTimeMillis() + val elapsed = (current - last) / 1000F // elapsed time in second at this stage + when (stage) { + 0 -> if (elapsed >= ANI_DURATION) { + stage = 1 + last = current + alphaFilter.alpha = 1F + } else { + alphaFilter.alpha = elapsed / ANI_DURATION + } + + 1 -> { + // TODO when there is something to load, stay at this stage until ready + val x = elapsed / PAUSE_DURATION + // S-curve animation, but this will not look good if speed is not constant + progressBar.progress = min(1 - (1 - x.pow(3.5F)).pow(12), 1F) + if (progressBar.progress >= 1F) { + stage = 2 + last = current + } + } + + 2 -> if (elapsed >= ANI_DURATION) { + stage = 3 + last = current + alphaFilter.alpha = 0F + } else { + alphaFilter.alpha = 1 - elapsed / ANI_DURATION + } + +// 3 -> screenManager.handle.openBefore(::TitleScreen, this) + 3 -> it.muiIoI.screenManager.handle.reset { p0, p1, p2, p3 -> + WorldCreateScreen(it.muiIoI.renderSystem, p0, p1, p2, p3) + } + } + } + } + + private class ProgressBar(renderSystemHandle: RenderSystem.Handle) { + val rectDim = Rectangle.withPoints(7, 7, 393, 33) + val length = rectDim.width + var progress: Float by Delegates.observable(0f) { _, _, _ -> + rect.setPos(7, 7, rectDim.x + (progress * length).toInt(), 33) + } + val rect = GuiRect(renderSystemHandle.canvasHandle, 7, 7, 7, 33, 240, 240, 240, 255) + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/ScaledBarComponent.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/ScaledBarComponent.kt new file mode 100644 index 00000000..e2890859 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/ScaledBarComponent.kt @@ -0,0 +1,85 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.impl + +import com.cout970.math.vec4.Vec4i +import net.terramodulus.engine.GeneralTransform +import net.terramodulus.mui.gui.agim.Component +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.gfx.ColorFilter +import net.terramodulus.mui.gui.gfx.Direction4A +import net.terramodulus.mui.gui.gfx.GuiRect +import net.terramodulus.mui.gui.gfx.RectStParams +import net.terramodulus.mui.gui.gfx.RectangleI +import net.terramodulus.mui.gui.gfx.RenderSystem +import net.terramodulus.util.LateInitObservable + +/** + * A single scaled bar with the provided dimensions as the bounds of the bar. + */ +open class ScaledBarComponent protected constructor( + canvasHandle: RenderSystem.CanvasHandle, + asdHandle: AsdHandle, + config: Config, +) : Component(asdHandle) { + constructor( + canvasHandle: RenderSystem.CanvasHandle, + asdHandle: AsdHandle, + config: ConstructEnvImpl.() -> Config, + ) : this(canvasHandle, asdHandle, config(ConstructEnvImpl)) + + companion object { + @JvmStatic + protected val BOUNDS = RectangleI(0, 0, 1, 1) + } + + private val scaleTransform = GeneralTransform() + protected val boundsTransform = GeneralTransform() + protected val dir = config.dir + protected val bar = GuiRect(canvasHandle, + BOUNDS.x, BOUNDS.y, BOUNDS.width, BOUNDS.height, + config.color.x, config.color.y, config.color.z, config.color.w, + ).apply { + add(scaleTransform) + add(boundsTransform) + } + + var fraction by LateInitObservable { _, _, new -> update(new) } + + init { + if (config.fraction != null) fraction = config.fraction + asdHandle.observeRect { + RectStParams.fromRects(BOUNDS.toDouble(), asdHandle.rect).applyToGeneralTransform(boundsTransform) + } + } + + fun addFilter(filter: ColorFilter) = bar.add(filter) + + @Suppress("unused") + interface ConstructEnv { + val xPos get() = Direction4A.XPos + val xNeg get() = Direction4A.XNeg + val yPos get() = Direction4A.YPos + val yNeg get() = Direction4A.YNeg + } + + object ConstructEnvImpl : ConstructEnv { + fun config(dir: Direction4A, color: Vec4i, fraction: Double? = null) = + Config(dir, color, fraction) + } + + open class Config( + val dir: Direction4A, + val color: Vec4i, + val fraction: Double? = null + ) + + private fun update(fraction: Double) { + RectStParams.withScale(BOUNDS.toDouble(), dir, fraction).applyToGeneralTransform(scaleTransform) + } + + override fun render(renderSystem: RenderSystem) = bar.render(renderSystem) +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/ScrollPane.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/ScrollPane.kt new file mode 100644 index 00000000..73587106 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/ScrollPane.kt @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.impl + +import net.terramodulus.mui.gui.agim.AbstractPane +import net.terramodulus.mui.gui.agim.Layout +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.gfx.RenderSystem + +class ScrollPane(asdHandle: AsdHandle) : AbstractPane(asdHandle) { + override fun render(renderSystem: RenderSystem) { + TODO("Not yet implemented") + } + + override val layout: Layout + get() = TODO("Not yet implemented") +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/SequenceLayout.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/SequenceLayout.kt new file mode 100644 index 00000000..da0df95d --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/SequenceLayout.kt @@ -0,0 +1,261 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.impl + +import com.cout970.math.vec2.ImmVec2d +import com.cout970.math.vec2.MutVec2d +import net.terramodulus.mui.gui.agim.AgimoPropertyMap +import net.terramodulus.mui.gui.agim.AnchorAlignmentHelper +import net.terramodulus.mui.gui.agim.Component +import net.terramodulus.mui.gui.agim.Container +import net.terramodulus.mui.gui.agim.Layout +import net.terramodulus.mui.gui.agim.LayoutComputationGroup +import net.terramodulus.mui.gui.agim.LayoutComputationUnit +import net.terramodulus.mui.gui.agim.LayoutHandle +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.gfx.Dimension2D +import net.terramodulus.mui.gui.gfx.Direction2S +import net.terramodulus.mui.gui.gfx.RectangleD +import kotlin.math.max + +/** + * Common implementation that is either [ColumnLayout] or [RowLayout]. + * + * This is an optimized special version of [FlexibleBoxLayout] without any expected + * multiple *sequences* of components in a single layout. + */ +sealed class SequenceLayout( + container: Container, + elements: ElementList, + protected var config: Config, +) : Layout.ElementGroup(container, elements) { + data class Element(val alignment: Double) { + companion object { + fun default() = Element(0.5) + } + } + + /** + * [padding] is the paddings from the four edges. + * [gap] is the gaps only in between elements. + */ + data class Config( + val direction: Direction2S, + val gap: Double = 0.0, + val padding: Double = 0.0, + // whether to rapidly calculate intrinsic dimensions for this Layout + val intrinsic: Boolean = false, + ) + + interface ConfigEnv { + var config: Config + } + + fun update(operation: ConfigEnv.() -> Unit) { + operate { operation(object : ConfigEnv { + override var config: Config by this@SequenceLayout::config + }) } + } + + override fun add(component: Component) = elements.add(component, Element.default()) + + override fun addBefore(target: Component, component: Component) = + elements.addBefore(target, component, Element.default()) + + override fun addAfter(target: Component, component: Component) = + elements.addAfter(target, component, Element.default()) + + override fun replace(target: Component, component: Component) = + elements.replace(target, component, Element.default()) +} + +/** + * **Column** case of [SequenceLayout]. + */ +class ColumnLayout private constructor(container: Container, elements: ElementList, config: Config) : + SequenceLayout(container, elements, config) { + companion object { + fun withComponents(vararg components: Component, config: Config) = { it: Container -> + ColumnLayout(it, ElementList.withComponentsDefault(Element::default, *components), config) + } + + fun withComponents(components: Collection, config: Config) = { it: Container -> + ColumnLayout(it, ElementList.withComponentsDefault(Element::default, components), config) + } + + fun withElements(vararg elements: Pair, config: Config) = { it: Container -> + ColumnLayout(it, ElementList.withElements(*elements), config) + } + + fun withElements(elements: Map, config: Config) = { it: Container -> + ColumnLayout(it, ElementList.withElements(elements), config) + } + } + + override fun layOut(handle: LayoutHandle) = sequenceOf(LayoutComputationGroup({}, { + // If all elements are separated into respective Units, race conditions may occur. + mutableSetOf(LayoutComputationUnit({ + put(container.asdHandle, setOf(BoundsProperty.KEY)) + elements.forEach { put(it.first.asdHandle, setOf(IntrinsicDimensionsProperty.KEY, DimensionsProperty.KEY)) } + }, { + elements.forEach { put(it.first.asdHandle, setOf(BoundsProperty.KEY)) } + put(container.asdHandle, setOf(RectangleProperty.KEY)) + }, { + val containerRect = getUnit(container.asdHandle).getProperty(BoundsProperty.KEY)!!.value + val anchor = MutVec2d(containerRect.x + config.padding, when (config.direction) { + Direction2S.Positive -> containerRect.y + config.padding + Direction2S.Negative -> containerRect.y + containerRect.height - config.padding + }) + var width = 0.0 + var height = 0.0 + val map = mutableMapOf() + elements.forEach { + val dim = DimensionsProperty.getOrComputeValue(getUnit(it.first.asdHandle)) + width = max(width, dim.width) + height += dim.height + config.gap + } + height = max(height - config.gap, 0.0) + elements.forEach { + val dim = DimensionsProperty.getOrComputeValue(getUnit(it.first.asdHandle)) + if (config.direction == Direction2S.Negative) anchor.y -= dim.height + map[it.first.asdHandle] = AgimoPropertyMap().apply { + putProperty(BoundsProperty.KEY, BoundsProperty(AnchorAlignmentHelper.simple( + RectangleD(anchor.x, anchor.y, width, dim.height), + Dimension2D(dim.width, dim.height), + ImmVec2d(it.second.alignment, 0.0), // y should make no effect + ))) + } + when (config.direction) { + Direction2S.Positive -> anchor.y += dim.height + config.gap + Direction2S.Negative -> anchor.y -= config.gap + } + } + map[container.asdHandle] = AgimoPropertyMap().apply { + putProperty(RectangleProperty.KEY, RectangleProperty(RectangleD( + containerRect.x, containerRect.y, width + config.padding * 2, height + config.padding * 2, + ))) + } + map + })).apply { + if (config.intrinsic) add(LayoutComputationUnit({ + elements.forEach { + put(it.first.asdHandle, setOf(IntrinsicDimensionsProperty.KEY, DimensionsProperty.KEY)) + } + }, { + put(container.asdHandle, setOf(DimensionsProperty.KEY)) + }, { + var width = 0.0 + var height = 0.0 + elements.forEach { + val dim = DimensionsProperty.getOrComputeValue(getUnit(it.first.asdHandle)) + width = max(width, dim.width) + height += dim.height + config.gap + } + height = max(height - config.gap, 0.0) + mapOf(container.asdHandle to AgimoPropertyMap().apply { + putProperty(DimensionsProperty.KEY, DimensionsProperty(Dimension2D( + width + config.padding * 2, + height + config.padding * 2, + ))) + }) + })) + } + })) +} + +/** + * **Row** case of [SequenceLayout]. + */ +class RowLayout private constructor(container: Container, elements: ElementList, config: Config) : + SequenceLayout(container, elements, config) { + companion object { + fun withComponents(vararg components: Component, config: Config) = { it: Container -> + RowLayout(it, ElementList.withComponentsDefault(Element::default, *components), config) + } + + fun withComponents(components: Collection, config: Config) = { it: Container -> + RowLayout(it, ElementList.withComponentsDefault(Element::default, components), config) + } + + fun withElements(vararg elements: Pair, config: Config) = { it: Container -> + RowLayout(it, ElementList.withElements(*elements), config) + } + + fun withElements(elements: Map, config: Config) = { it: Container -> + RowLayout(it, ElementList.withElements(elements), config) + } + } + + override fun layOut(handle: LayoutHandle) = sequenceOf(LayoutComputationGroup({}, { + // If all elements are separated into respective Units, race conditions may occur. + mutableSetOf(LayoutComputationUnit({ + put(container.asdHandle, setOf(BoundsProperty.KEY)) + elements.forEach { put(it.first.asdHandle, setOf(IntrinsicDimensionsProperty.KEY, DimensionsProperty.KEY)) } + }, { + elements.forEach { put(it.first.asdHandle, setOf(BoundsProperty.KEY)) } + put(container.asdHandle, setOf(RectangleProperty.KEY)) + }, { + val containerRect = getUnit(container.asdHandle).getProperty(BoundsProperty.KEY)!!.value + val anchor = MutVec2d(when (config.direction) { + Direction2S.Positive -> containerRect.x + config.padding + Direction2S.Negative -> containerRect.x + containerRect.width - config.padding + }, containerRect.y + config.gap) + var width = 0.0 + var height = 0.0 + val map = mutableMapOf() + elements.forEach { + val dim = DimensionsProperty.getOrComputeValue(getUnit(it.first.asdHandle)) + width += dim.width + config.gap + height = max(height, dim.height) + } + width = max(width - config.gap, 0.0) + elements.forEach { + val dim = DimensionsProperty.getOrComputeValue(getUnit(it.first.asdHandle)) + if (config.direction == Direction2S.Negative) anchor.x -= dim.width + map[it.first.asdHandle] = AgimoPropertyMap().apply { + putProperty(BoundsProperty.KEY, BoundsProperty(AnchorAlignmentHelper.simple( + RectangleD(anchor.x, anchor.y, dim.width, height), + Dimension2D(dim.width, dim.height), + ImmVec2d(0.0, it.second.alignment), // x should make no effect + ))) + } + when (config.direction) { + Direction2S.Positive -> anchor.x += dim.width + config.gap + Direction2S.Negative -> anchor.x -= config.gap + } + } + map[container.asdHandle] = AgimoPropertyMap().apply { + putProperty(RectangleProperty.KEY, RectangleProperty(RectangleD( + containerRect.x, containerRect.y, width + config.padding * 2, height + config.padding * 2, + ))) + } + map + })).apply { + if (config.intrinsic) add(LayoutComputationUnit({ + elements.forEach { + put(it.first.asdHandle, setOf(IntrinsicDimensionsProperty.KEY, DimensionsProperty.KEY)) + } + }, { + put(container.asdHandle, setOf(DimensionsProperty.KEY)) + }, { + var width = 0.0 + var height = 0.0 + elements.forEach { + val dim = DimensionsProperty.getOrComputeValue(getUnit(it.first.asdHandle)) + width += dim.width + config.gap + height = max(height, dim.height) + } + width = max(width - config.gap, 0.0) + mapOf(container.asdHandle to AgimoPropertyMap().apply { + putProperty(DimensionsProperty.KEY, DimensionsProperty(Dimension2D( + width + config.padding * 2, + height + config.padding * 2, + ))) + }) + })) + } + })) +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/SimplePane.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/SimplePane.kt new file mode 100644 index 00000000..6dfa52e0 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/SimplePane.kt @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.impl + +import net.terramodulus.mui.gui.agim.AbstractPane +import net.terramodulus.mui.gui.agim.Layout +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.gfx.RenderSystem + +class SimplePane(asdHandle: AsdHandle, layout: SimplePane.() -> Layout) : AbstractPane(asdHandle) { + override var layout = layout(this) + override fun render(renderSystem: RenderSystem) { + layout.render(renderSystem) + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/SingletonLayout.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/SingletonLayout.kt new file mode 100644 index 00000000..d07a59a6 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/SingletonLayout.kt @@ -0,0 +1,350 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.impl + +import com.cout970.math.vec2.ImmVec2d +import net.terramodulus.mui.gui.agim.AgimoPropertyMap +import net.terramodulus.mui.gui.agim.AnchorAlignmentHelper +import net.terramodulus.mui.gui.agim.Component +import net.terramodulus.mui.gui.agim.Container +import net.terramodulus.mui.gui.agim.Layout +import net.terramodulus.mui.gui.agim.LayoutComputationGroup +import net.terramodulus.mui.gui.agim.LayoutComputationUnit +import net.terramodulus.mui.gui.agim.LayoutHandle +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.gfx.Dimension2D +import net.terramodulus.mui.gui.gfx.Direction2S +import net.terramodulus.mui.gui.gfx.InsetsD +import net.terramodulus.mui.gui.gfx.RectangleD +import kotlin.math.max +import kotlin.math.min + +class SingletonLayout(container: Container, component: Component, private var config: Config) : Layout(container) { + override val components = componentsSequence(::component) + var component = component + private set + + sealed class Config private constructor() { + abstract fun layOut(layout: SingletonLayout): Set + + sealed class Absolute private constructor() : Config() { + override fun layOut(layout: SingletonLayout): Set = + setOf(LayoutComputationUnit({ + put(layout.container.asdHandle, setOf(RectangleProperty.KEY, BoundsProperty.KEY)) + }, { + put(layout.component.asdHandle, setOf(BoundsProperty.KEY)) + }, { + mapOf(layout.component.asdHandle to AgimoPropertyMap().apply { + val prop = getUnit(layout.container.asdHandle) + putProperty(BoundsProperty.KEY, BoundsProperty(compute( + prop.getProperty(RectangleProperty.KEY)?.value + ?: prop.getProperty(BoundsProperty.KEY)!!.value) + )) + }) + })) + + abstract fun compute(container: RectangleD): RectangleD + + data object Full : Absolute() { + override fun compute(container: RectangleD) = container + } + + data class Insets(var insets: InsetsD) : Absolute() { + override fun compute(container: RectangleD) = container - insets + } + } + + /** + * Automatically inducing full insets by only two distinct sides of insets and intrinsic dimensions. + */ + data class Auto(val x: Side, val y: Side) : Config() { + data class Side(val dir: Direction2S, val offset: Double) + + override fun layOut(layout: SingletonLayout) = setOf(LayoutComputationUnit({ + put(layout.container.asdHandle, setOf(RectangleProperty.KEY, BoundsProperty.KEY)) + put(layout.component.asdHandle, setOf(IntrinsicDimensionsProperty.KEY, DimensionsProperty.KEY)) + }, { + put(layout.component.asdHandle, setOf(BoundsProperty.KEY)) + }, { + mapOf(layout.component.asdHandle to AgimoPropertyMap().apply { + val prop = getUnit(layout.container.asdHandle) + val rect = prop.getProperty(RectangleProperty.KEY)?.value + ?: prop.getProperty(BoundsProperty.KEY)!!.value + val dims = DimensionsProperty.getOrComputeValue(getUnit(layout.component.asdHandle)) + val left: Double + val top: Double + val right: Double + val bottom: Double + when (x.dir) { + Direction2S.Positive -> { + right = x.offset + left = rect.width - x.offset - dims.width + } + Direction2S.Negative -> { + left = x.offset + right = rect.width - x.offset - dims.width + } + } + when (y.dir) { + Direction2S.Positive -> { + top = y.offset + bottom = rect.height - y.offset - dims.height + } + Direction2S.Negative -> { + bottom = y.offset + top = rect.height - y.offset - dims.height + } + } + putProperty(BoundsProperty.KEY, BoundsProperty(rect - InsetsD(left, top, right, bottom))) + }) + })) + } + + data class Aligned(val config: Relative, val alignment: AlignmentConfig) : Config() { + override fun layOut(layout: SingletonLayout): Set = + setOf(LayoutComputationUnit(config.dependencies(layout), { + put(layout.component.asdHandle, setOf(BoundsProperty.KEY)) + }, { + mapOf(layout.component.asdHandle to AgimoPropertyMap().apply { + val prop = getUnit(layout.container.asdHandle) + val rect = prop.getProperty(RectangleProperty.KEY)?.value + ?: prop.getProperty(BoundsProperty.KEY)!!.value + val target = config.compute(layout, this@LayoutComputationUnit) + putProperty(BoundsProperty.KEY, BoundsProperty( + AnchorAlignmentHelper.simple(rect, target, ImmVec2d(alignment.x, alignment.y)) + )) + }) + })) + } + + /** + * Configuration where both intrinsic properties and bounds are transmissive: + * - intrinsic properties are transmitted from inner to outer + * - bounds and rectangle are transmitted from outer to inner + */ + data class Sole(val config: Scaled) : Config() { + override fun layOut(layout: SingletonLayout): Set = setOf( + LayoutComputationUnit({ + put(layout.component.asdHandle, setOf( + IntrinsicDimensionsProperty.KEY, + IntrinsicRatioProperty.KEY, + DimensionsProperty.KEY, + )) + }, { + put(layout.container.asdHandle, setOf( + IntrinsicDimensionsProperty.KEY, + IntrinsicRatioProperty.KEY, + DimensionsProperty.KEY, + )) + }, { + mapOf(layout.container.asdHandle to AgimoPropertyMap().apply { + val target = config.compute(layout, this@LayoutComputationUnit) + val dims = IntrinsicDimensionsProperty(target.width.toUInt(), target.height.toUInt()) + putProperty(IntrinsicDimensionsProperty.KEY, dims) + putProperty(IntrinsicRatioProperty.KEY, dims.computeRatio()) + putProperty(DimensionsProperty.KEY, DimensionsProperty(target)) + }) + }), + LayoutComputationUnit({ + put(layout.container.asdHandle, setOf(BoundsProperty.KEY, RectangleProperty.KEY)) + }, { + put(layout.component.asdHandle, setOf(BoundsProperty.KEY)) + }, { + mapOf(layout.component.asdHandle to AgimoPropertyMap().apply { + val prop = getUnit(layout.container.asdHandle) + val rect = prop.getProperty(RectangleProperty.KEY)?.value + ?: prop.getProperty(BoundsProperty.KEY)!!.value + putProperty(BoundsProperty.KEY, BoundsProperty(rect)) + }) + }), + ) + } + + sealed class Relative { + // Must include Container Bounds & Rectangle + abstract fun dependencies(layout: SingletonLayout): + MutableMap>>.() -> Unit + + abstract fun compute(layout: SingletonLayout, handle: LayoutHandle): Dimension2D + + data class Simple(val scale: Double) : Relative() { + override fun dependencies(layout: SingletonLayout): + MutableMap>>.() -> Unit = { + put(layout.container.asdHandle, setOf(RectangleProperty.KEY, BoundsProperty.KEY)) + } + + override fun compute(layout: SingletonLayout, handle: LayoutHandle): Dimension2D { + val prop = handle.getUnit(layout.container.asdHandle) + val rect = prop.getProperty(RectangleProperty.KEY)?.value + ?: prop.getProperty(BoundsProperty.KEY)!!.value + return Dimension2D(rect.width * scale, rect.height * scale) + } + } + + data class Both(val scaleX: Double, val scaleY: Double) : Relative() { + override fun dependencies(layout: SingletonLayout): + MutableMap>>.() -> Unit = { + put(layout.container.asdHandle, setOf(RectangleProperty.KEY, BoundsProperty.KEY)) + } + + override fun compute(layout: SingletonLayout, handle: LayoutHandle): Dimension2D { + val prop = handle.getUnit(layout.container.asdHandle) + val rect = prop.getProperty(RectangleProperty.KEY)?.value + ?: prop.getProperty(BoundsProperty.KEY)!!.value + return Dimension2D(rect.width * scaleX, rect.height * scaleY) + } + } + } + + sealed class ObjectFit private constructor() : Relative() { + override fun dependencies(layout: SingletonLayout): + MutableMap>>.() -> Unit = { + put(layout.container.asdHandle, setOf(RectangleProperty.KEY, BoundsProperty.KEY)) + put(layout.component.asdHandle, setOf(IntrinsicRatioProperty.KEY, DimensionsProperty.KEY)) + } + + override fun compute(layout: SingletonLayout, handle: LayoutHandle): Dimension2D { + val prop = handle.getUnit(layout.container.asdHandle) + return compute( + prop.getProperty(RectangleProperty.KEY)?.value + ?: prop.getProperty(BoundsProperty.KEY)!!.value, + DimensionsProperty.getOrComputeRatio(handle.getUnit(layout.component.asdHandle)) + ) + } + + abstract fun compute(container: RectangleD, component: Dimension2D): Dimension2D + + data object Contain : ObjectFit() { + override fun compute(container: RectangleD, component: Dimension2D): Dimension2D { + val w = container.width / component.width + val h = container.height / component.height + val scale = min(w, h) + return Dimension2D( + component.width * scale, + component.height * scale, + ) + } + } + + data object Cover : ObjectFit() { + override fun compute(container: RectangleD, component: Dimension2D): Dimension2D { + val w = container.width / component.width + val h = container.height / component.height + val scale = max(w, h) + return Dimension2D( + component.width * scale, + component.height * scale, + ) + } + } + } + + sealed class Scaled private constructor() : Relative() { + override fun dependencies(layout: SingletonLayout): + MutableMap>>.() -> Unit = { + put(layout.container.asdHandle, setOf(RectangleProperty.KEY, BoundsProperty.KEY)) + put(layout.component.asdHandle, setOf(IntrinsicDimensionsProperty.KEY, DimensionsProperty.KEY)) + } + + override fun compute(layout: SingletonLayout, handle: LayoutHandle) = compute( + DimensionsProperty.getOrComputeValue(handle.getUnit(layout.component.asdHandle)) + ) + + abstract fun compute(dim: Dimension2D): Dimension2D + + /** + * Scale both dimensions by the same scaling + * @param scale `> 0` + */ + data class Scale(val scale: Double) : Scaled() { + override fun compute(dim: Dimension2D) = + Dimension2D(dim.width * scale, dim.height * scale) + } + + class Compute private constructor(private val x: Value, private val y: Value) : Scaled() { + private object MathEnvImpl : MathEnv + + constructor(x: MathEnv.() -> Value, y: MathEnv.() -> Value) : this(x(MathEnvImpl), y(MathEnvImpl)) + + sealed interface Value { + fun compute(dim: Dimension2D): Double + + operator fun plus(that: Value) = Operator.Plus(this, that) + operator fun minus(that: Value) = Operator.Minus(this, that) + operator fun times(that: Value) = Operator.Times(this, that) + operator fun div(that: Value) = Operator.Div(this, that) + } + + sealed class Operator private constructor() : Value { + data class Plus(val a: Value, val b: Value) : Operator() { + override fun compute(dim: Dimension2D) = a.compute(dim) + b.compute(dim) + } + data class Minus(val a: Value, val b: Value) : Operator() { + override fun compute(dim: Dimension2D) = a.compute(dim) - b.compute(dim) + } + data class Times(val a: Value, val b: Value) : Operator() { + override fun compute(dim: Dimension2D) = a.compute(dim) * b.compute(dim) + } + data class Div(val a: Value, val b: Value) : Operator() { + override fun compute(dim: Dimension2D) = a.compute(dim) / b.compute(dim) + } + } + + sealed class Param private constructor() : Value { + data class Num(val value: Double) : Param() { + override fun compute(dim: Dimension2D) = value + } + data object DimX : Param() { + override fun compute(dim: Dimension2D) = dim.width + } + data object DimY : Param() { + override fun compute(dim: Dimension2D) = dim.height + } + } + + sealed interface MathEnv { + val dimX: Param get() = Param.DimX + val dimY: Param get() = Param.DimY + fun num(value: Double) = Param.Num(value) + } + + override fun compute(dim: Dimension2D) = Dimension2D(x.compute(dim), y.compute(dim)) + } + } + + /** + * Range within `[0,1]` + */ + data class AlignmentConfig(val x: Double, val y: Double) { + companion object { + val DEFAULT = AlignmentConfig(0.5, 0.5) + fun withX(x: Double) = AlignmentConfig(x, 0.5) + fun withY(y: Double) = AlignmentConfig(y, 0.5) + } + } + } + + fun update(component: Component) { + operate { + this@SingletonLayout.component = component + } + } + + interface ConfigEnv { + var config: Config + } + + fun update(operation: ConfigEnv.() -> Unit) { + operate { + operation(object : ConfigEnv { + override var config: Config by this@SingletonLayout::config + }) + } + } + + override fun layOut(handle: LayoutHandle) = + sequenceOf(LayoutComputationGroup({}, { config.layOut(this@SingletonLayout) })) +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/SizedPane.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/SizedPane.kt new file mode 100644 index 00000000..698a8de7 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/SizedPane.kt @@ -0,0 +1,101 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.impl + +import net.terramodulus.mui.gui.agim.AbstractPane +import net.terramodulus.mui.gui.agim.AgimoPropertyMap +import net.terramodulus.mui.gui.agim.Component +import net.terramodulus.mui.gui.agim.Layout +import net.terramodulus.mui.gui.agim.LayoutComputationGroup +import net.terramodulus.mui.gui.agim.LayoutComputationUnit +import net.terramodulus.mui.gui.agim.LayoutHandle +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.gfx.Dimension2D +import net.terramodulus.mui.gui.gfx.RenderSystem + +/** + * Provide intrinsic information for this container and its [component]. + */ +class SizedPane(asdHandle: AsdHandle, component: Component, private var config: Config) : AbstractPane(asdHandle) { + private val _layout = SizedLayout(component) + override val layout: Layout get() = _layout + + private inner class SizedLayout(var component: Component) : Layout(this@SizedPane) { + override val components = componentsSequence(::component) + + override fun layOut(handle: LayoutHandle) = sequenceOf(LayoutComputationGroup({}, { + setOf(LayoutComputationUnit({}, { + put(component.asdHandle, setOf( + IntrinsicRatioProperty.KEY, + IntrinsicDimensionsProperty.KEY, + DimensionsProperty.KEY, + )) + put(container.asdHandle, setOf( + IntrinsicRatioProperty.KEY, + IntrinsicDimensionsProperty.KEY, + DimensionsProperty.KEY, + )) + }, { + val dim = IntrinsicDimensionsProperty(config.width, config.height) + val ratio = dim.computeRatio() + mapOf( + component.asdHandle to AgimoPropertyMap().apply { + putProperty(IntrinsicRatioProperty.KEY, ratio) + putProperty(IntrinsicDimensionsProperty.KEY, dim) + putProperty(DimensionsProperty.KEY, DimensionsProperty(Dimension2D( + config.width.toDouble(), + config.height.toDouble(), + ))) + }, + container.asdHandle to AgimoPropertyMap().apply { + putProperty(IntrinsicRatioProperty.KEY, ratio) + putProperty(IntrinsicDimensionsProperty.KEY, dim) + putProperty(DimensionsProperty.KEY, DimensionsProperty(Dimension2D( + config.width.toDouble(), + config.height.toDouble(), + ))) + } + ) + }), LayoutComputationUnit({ // Forwarding Rect to component + put(container.asdHandle, setOf(RectangleProperty.KEY, BoundsProperty.KEY)) + }, { + put(component.asdHandle, setOf(BoundsProperty.KEY)) + }, { + mapOf(component.asdHandle to AgimoPropertyMap().apply { + val prop = getUnit(container.asdHandle) + putProperty(BoundsProperty.KEY, BoundsProperty( + prop.getProperty(RectangleProperty.KEY)?.value + ?: prop.getProperty(BoundsProperty.KEY)!!.value) + ) + }) + })) + })) + } + + data class Config(val width: UInt, val height: UInt) + + fun update(component: Component) { + _layout.operate { + _layout.component = component + } + } + + interface ConfigEnv { + var config: Config + } + + fun update(operation: ConfigEnv.() -> Unit) { + _layout.operate { + operation(object : ConfigEnv { + override var config: Config by this@SizedPane::config + }) + } + } + + override fun render(renderSystem: RenderSystem) { + layout.render(renderSystem) + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/SliderComponent.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/SliderComponent.kt new file mode 100644 index 00000000..6dae77c5 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/SliderComponent.kt @@ -0,0 +1,225 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.impl + +import com.cout970.math.vec2.Vec2d +import com.cout970.math.vec4.Vec4i +import net.terramodulus.mui.gui.InputStatesHandle +import net.terramodulus.mui.gui.MouseCtxStates +import net.terramodulus.mui.gui.MouseState +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.gfx.Direction4A +import net.terramodulus.mui.gui.gfx.GuiRect +import net.terramodulus.mui.gui.gfx.RenderSystem +import net.terramodulus.mui.kui.MouseInputHandler +import kotlin.math.abs +import kotlin.math.log +import kotlin.math.log2 +import kotlin.math.pow +import kotlin.math.roundToInt + +/** + * Interactive Slider + * + * Caveat: if [fraction] is externally modified, `reaction` is never invoked. + */ +class SliderComponent private constructor( + canvasHandle: RenderSystem.CanvasHandle, + inputStatesHandle: InputStatesHandle, + asdHandle: AsdHandle, + config: Config, +) : ScaledBarComponent(canvasHandle, asdHandle, config) { + constructor( + canvasHandle: RenderSystem.CanvasHandle, + inputStatesHandle: InputStatesHandle, + asdHandle: AsdHandle, + config: ConstructEnv.() -> Config, + ) : this(canvasHandle, inputStatesHandle, asdHandle, config(ConstructEnv)) + + sealed class SliderMode { + /** + * Translates `fraction` to a desired value point when necessary. + */ + internal abstract fun translate(fraction: Double): Double? + + internal abstract fun react(fraction: Double) + + // Hard to specify type here, so Any is used instead, but should be careful + internal abstract fun getFraction(value: Any): Double + + /** + * @param length must be positive + */ + class Points(val length: Int, val reaction: (Int) -> Unit) : SliderMode() { + override fun translate(fraction: Double) = + (fraction * length).roundToInt().coerceIn(0, length).toDouble() / length + + override fun react(fraction: Double) = reaction((fraction * length).roundToInt().coerceIn(0, length)) + + /** + * @param value must be [Int] in `0`..[length] + */ + override fun getFraction(value: Any): Double { + require(value is Int && value in 0..length) + return value / length.toDouble() + } + } + + class Ranged( + val range: ClosedFloatingPointRange, + val reaction: (Double) -> Unit, + val transform: Transform = Transform.Linear, + ) : SliderMode() { + interface Transform { + /** + * Projects a value in the [range] to a fraction in `[0,1]`. + */ + fun project(value: Double, range: ClosedFloatingPointRange): Double + + /** + * Back-projects a fraction in `[0,1]` to a value in the [range]. + */ + fun backProject(value: Double, range: ClosedFloatingPointRange): Double + + data object Linear : Transform { + override fun project(value: Double, range: ClosedFloatingPointRange) = + (value - range.start) / (range.endInclusive - range.start) + + override fun backProject(value: Double, range: ClosedFloatingPointRange) = + value * (range.endInclusive - range.start) + range.start + } + + data class Exponential(val base: Double) : Transform { + init { + require(base > 0.0 && base != 1.0) + } + + override fun project(value: Double, range: ClosedFloatingPointRange) = + log2((value - range.start) / (range.endInclusive - range.start) * (base - 1) + 1) / log2(base) + + override fun backProject(value: Double, range: ClosedFloatingPointRange) = + (base.pow(value) - 1) / (base - 1) * (range.endInclusive - range.start) + range.start + } + + data class LinearExponential(val base: Double) : Transform { + init { + require(base > 0.0 && base != 1.0) + } + + override fun project(value: Double, range: ClosedFloatingPointRange): Double { + val m = log(range.start, base) + val n = log(range.endInclusive, base) + return (log(value, base) - m) / (n - m) + } + + override fun backProject(value: Double, range: ClosedFloatingPointRange): Double { + val m = log(range.start, base) + val n = log(range.endInclusive, base) + return base.pow(m + (n - m) * value) + } + } + + data class Logarithmic(val base: Double) : Transform { + init { + require(base > 0.0 && base != 1.0) + } + + override fun project(value: Double, range: ClosedFloatingPointRange) = + (2.0.pow((value - range.start) / (range.endInclusive - range.start) * log2(base)) - 1) / + (base - 1) + + override fun backProject(value: Double, range: ClosedFloatingPointRange) = + log2(value * (base - 1) + 1) / log2(base) * (range.endInclusive - range.start) + range.start + } + } + + override fun translate(fraction: Double) = null + + override fun react(fraction: Double) = reaction(transform.backProject(fraction, range)) + + /** + * @param value must be [Double] in [range] + */ + override fun getFraction(value: Any): Double { + require(value is Double && value in range) + return transform.project(value, range) + } + } + } + + private val sliderMode = config.mode + + private val background = GuiRect(canvasHandle, + BOUNDS.x, BOUNDS.y, BOUNDS.width, BOUNDS.height, + config.bgColor.x, config.bgColor.y, config.bgColor.z, config.bgColor.w, + ).apply { add(boundsTransform) } + + object ConstructEnv : ScaledBarComponent.ConstructEnv { + fun withPoints(length: Int, init: Int, reaction: (Int) -> Unit) = + SliderMode.Points(length, reaction).let { it to it.getFraction(init) } + fun withRanged(range: ClosedFloatingPointRange, init: Double, reaction: (Double) -> Unit) = + SliderMode.Ranged(range, reaction).let { it to it.getFraction(init) } + fun withRanged( + range: ClosedFloatingPointRange, + init: Double, + transform: SliderMode.Ranged.Transform, + reaction: (Double) -> Unit, + ) = SliderMode.Ranged(range, reaction, transform).let { it to it.getFraction(init) } + fun transformExponential(base: Double) = SliderMode.Ranged.Transform.Exponential(base) + fun transformLinearExponential(base: Double) = SliderMode.Ranged.Transform.LinearExponential(base) + fun transformLogarithmic(base: Double) = SliderMode.Ranged.Transform.Logarithmic(base) + fun config(mode: Pair, dir: Direction4A, bgColor: Vec4i, fgColor: Vec4i) = + Config(mode.first, dir, bgColor, fgColor, mode.second) + } + + class Config(val mode: SliderMode, dir: Direction4A, val bgColor: Vec4i, fgColor: Vec4i, fraction: Double) : + ScaledBarComponent.Config(dir, fgColor, fraction) + + private val mouseCtxStates = MouseCtxStates(inputStatesHandle.mouseGlobalStates, asdHandle).apply { + var reacting = false + addListener(MouseState.Listener( + setOf(MouseState.Trigger(MouseState.Key.ButtonJustDown(MouseInputHandler.Buttons.Left.id)) { true }) + ) { + assert(it is MouseState.ButtonJustDown && MouseInputHandler.Buttons.Left.matches(it.id)) + assert(!reacting) + if (ctxRange.rect.contains((it as MouseState.ButtonJustDown).pos)) reacting = true + }) + addListener(MouseState.Listener(setOf( + MouseState.Trigger(MouseState.Key.Movement) { reacting }, + MouseState.Trigger(MouseState.Key.ButtonJustUp(MouseInputHandler.Buttons.Left.id)) { reacting }, + )) { + assert(reacting) + val pos: Vec2d + when (it) { + is MouseState.Movement -> { + pos = it.pos + } + is MouseState.ButtonJustUp -> { + assert(MouseInputHandler.Buttons.Left.matches(it.id)) + pos = it.pos + reacting = false + } + else -> throw AssertionError() + } + val rect = asdHandle.rect + val fraction = when (dir) { + Direction4A.XPos -> ((pos.x - rect.x) / rect.width).coerceIn(0.0, 1.0) + // optimized from: (rect.x + rect.width - pos.x) / rect.width + Direction4A.XNeg -> ((rect.x - pos.x) / rect.width + 1).coerceIn(0.0, 1.0) + Direction4A.YPos -> ((pos.y - rect.y) / rect.height).coerceIn(0.0, 1.0) + // optimized from: (rect.y + rect.height - pos.y) / rect.height + Direction4A.YNeg -> ((rect.y - pos.y) / rect.height + 1).coerceIn(0.0, 1.0) + } + this@SliderComponent.fraction = sliderMode.translate(fraction) ?: fraction + sliderMode.react(fraction) + }) + } + + override fun render(renderSystem: RenderSystem) { + background.render(renderSystem) + super.render(renderSystem) + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/TextDisplayComponent.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/TextDisplayComponent.kt new file mode 100644 index 00000000..472e129e --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/TextDisplayComponent.kt @@ -0,0 +1,43 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.impl + +import net.terramodulus.mui.gui.agim.Component +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.gfx.RenderSystem +import net.terramodulus.mui.gui.gfx.TextContext +import net.terramodulus.util.lateInitObservable + +class TextDisplayComponent( + asdHandle: AsdHandle, + renderSystemHandle: RenderSystem.Handle, + config: TextContext.Config, +) : Component(asdHandle) { + private val context = TextContext(renderSystemHandle, config) + var text: String by lateInitObservable { _, _, new -> + context.setText(new) + refreshDims() + } + + init { + asdHandle.observeRect { + context.update(asdHandle.rect) + } + } + + private fun refreshDims() { + asdHandle.properties.putProperty(DimensionsProperty.KEY, DimensionsProperty(context.size)) + } + + fun update(operation: TextContext.ConfigEnv.() -> Unit) { + context.update(operation) + refreshDims() + } + + override fun render(renderSystem: RenderSystem) { + context.render() + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/TitleScreen.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/TitleScreen.kt new file mode 100644 index 00000000..a93109a9 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/TitleScreen.kt @@ -0,0 +1,21 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.impl + +import net.terramodulus.mui.gui.agim.Layout +import net.terramodulus.mui.gui.gfx.RenderSystem +import net.terramodulus.mui.gui.agim.Screen +import net.terramodulus.mui.gui.agim.ScreenManager +import net.terramodulus.mui.gui.asd.AsdHandle + +class TitleScreen( + managerHandle: ScreenManager.Handle, + asdHandle: AsdHandle.Container, + renderSystemHandle: RenderSystem.Handle, +) : Screen(managerHandle, asdHandle) { + override val layout = + SingletonLayout(this, BlankComponent(ComponentAsdHandleImpl()), SingletonLayout.Config.Absolute.Full) +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/TrackBarComponent.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/TrackBarComponent.kt new file mode 100644 index 00000000..06ccfd10 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/TrackBarComponent.kt @@ -0,0 +1,186 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.impl + +import com.cout970.math.vec4.Vec4i +import net.terramodulus.mui.gui.agim.Component +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.gfx.Dimension2D +import net.terramodulus.mui.gui.gfx.Direction4A +import net.terramodulus.mui.gui.gfx.GeneralTransform +import net.terramodulus.mui.gui.gfx.GuiRect +import net.terramodulus.mui.gui.gfx.ModelTransform +import net.terramodulus.mui.gui.gfx.RectStParams +import net.terramodulus.mui.gui.gfx.RectangleD +import net.terramodulus.mui.gui.gfx.RectangleI +import net.terramodulus.mui.gui.gfx.RenderSystem +import net.terramodulus.util.LateInitObservable + +/** + * Track Bar, with marks on the bar. + */ +// TODO Add decoration marks like the handle +class TrackBarComponent( + canvasHandle: RenderSystem.CanvasHandle, + asdHandle: AsdHandle, + config: ConstructEnv.() -> ConstructEnv.Config, +) : Component(asdHandle) { + private sealed class BarMode private constructor() { + companion object { + val BOUNDS = RectangleI(0, 0, 1, 1) + } + + abstract fun addModel(modelTransform: ModelTransform) + + abstract fun updateBar(fraction: Double) + + abstract fun renderBar(renderSystem: RenderSystem) + + class Singleton(canvasHandle: RenderSystem.CanvasHandle, color: Vec4i) : BarMode() { + private val bar = GuiRect(canvasHandle, + BOUNDS.x, BOUNDS.y, BOUNDS.width, BOUNDS.height, + color.x, color.y, color.z, color.w, + ) + + override fun addModel(modelTransform: ModelTransform) = bar.add(modelTransform) + + override fun updateBar(fraction: Double) {} + + override fun renderBar(renderSystem: RenderSystem) = bar.render(renderSystem) + } + + class Sectioned( + canvasHandle: RenderSystem.CanvasHandle, + private val dir: Direction4A, + priColor: Vec4i, + secColor: Vec4i, + ) : BarMode() { + // Primary section refers to the section uncovered by the fraction and the direction. + // Secondary section refers to the section directed by the fraction with the direction. + private val priBar = GuiRect(canvasHandle, + BOUNDS.x, BOUNDS.y, BOUNDS.width, BOUNDS.height, + priColor.x, priColor.y, priColor.z, priColor.w, + ) + private val secBar = GuiRect(canvasHandle, + BOUNDS.x, BOUNDS.y, BOUNDS.width, BOUNDS.height, + secColor.x, secColor.y, secColor.z, secColor.w, + ) + private val priTransform = GeneralTransform().apply { priBar.add(this) } + private val secTransform = GeneralTransform().apply { secBar.add(this) } + + override fun addModel(modelTransform: ModelTransform) { + priBar.add(modelTransform) + secBar.add(modelTransform) + } + + override fun updateBar(fraction: Double) { + RectStParams.withScale(BOUNDS.toDouble(), dir.toOppo(), fraction).applyToGeneralTransform(priTransform) + RectStParams.withScale(BOUNDS.toDouble(), dir, fraction).applyToGeneralTransform(secTransform) + } + + override fun renderBar(renderSystem: RenderSystem) { + priBar.render(renderSystem) + secBar.render(renderSystem) + } + } + } + + private val barMode: BarMode + private val size: Dimension2D + private val bounds: RectangleD // calculated from `size` with origin (0, 0) + private val barRect: RectangleD + private val dir: Direction4A + + private val barTransform = GeneralTransform() + private val mainTransform = GeneralTransform() + + var fraction by LateInitObservable { _, _, new -> update(new) } + + init { + val config = config(ConstructEnv) + barMode = when (config.mode) { + is ConstructEnv.BarMode.Sectioned -> + BarMode.Sectioned(canvasHandle, config.dir, config.mode.priColor, config.mode.secColor) + is ConstructEnv.BarMode.Singleton -> BarMode.Singleton(canvasHandle, config.mode.color) + } + barMode.addModel(barTransform) + barMode.addModel(mainTransform) + size = config.size + bounds = RectangleD(0.0, 0.0, size.width, size.height) + barRect = when (config.dir) { + Direction4A.XPos -> RectangleD( + config.offMin, + (size.height - config.breadth) / 2, + size.width - config.offMin - config.offMax, + config.breadth, + ) + Direction4A.XNeg -> RectangleD( + config.offMax, + (size.height - config.breadth) / 2, + size.width - config.offMin - config.offMax, + config.breadth, + ) + Direction4A.YPos -> RectangleD( + (size.width - config.breadth) / 2, + config.offMin, + config.breadth, + size.height - config.offMin - config.offMax, + ) + Direction4A.YNeg -> RectangleD( + (size.width - config.breadth) / 2, + config.offMax, + config.breadth, + size.height - config.offMin - config.offMax, + ) + } + RectStParams.fromRects(BarMode.BOUNDS.toDouble(), barRect).applyToGeneralTransform(barTransform) + dir = config.dir + if (config.fraction != null) fraction = config.fraction + + val dim = IntrinsicDimensionsProperty(size.width.toUInt(), size.height.toUInt()) + asdHandle.properties.putProperty(IntrinsicDimensionsProperty.KEY, dim) + asdHandle.properties.putProperty(IntrinsicRatioProperty.KEY, dim.computeRatio()) + asdHandle.observeRect { + RectStParams.fromRects(bounds, asdHandle.rect).applyToGeneralTransform(mainTransform) + } + } + + @Suppress("unused") + object ConstructEnv { + val XPos = Direction4A.XPos + val XNeg = Direction4A.XNeg + val YPos = Direction4A.YPos + val YNeg = Direction4A.YNeg + sealed class BarMode { + data class Singleton(val color: Vec4i) : BarMode() + data class Sectioned(val priColor: Vec4i, val secColor: Vec4i) : BarMode() + } + + /** + * Offsets must be non-negative, as insets from bounds. + * Targets of offsets depend on the direction. + * The dimension in the cross axis should be larger or equal than the breadth. + */ + class Config( + val mode: BarMode, + // TODO Currently fixed sized, but later should be dynamic + val size: Dimension2D, + val dir: Direction4A, + val offMin: Double, + val offMax: Double, + val breadth: Double, + val fraction: Double? = null + ) + } + + private fun update(fraction: Double) { + barMode.updateBar(fraction) + } + + override fun render(renderSystem: RenderSystem) { + barMode.renderBar(renderSystem) + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/WorldCreateScreen.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/WorldCreateScreen.kt new file mode 100644 index 00000000..052cc029 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/WorldCreateScreen.kt @@ -0,0 +1,135 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.impl + +import com.cout970.math.vec4.ImmVec4i +import net.terramodulus.engine.common.ZeroImmVec3f +import net.terramodulus.mui.gui.InputStatesHandle +import net.terramodulus.mui.gui.agim.Screen +import net.terramodulus.mui.gui.agim.ScreenManager +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.gfx.Direction2S +import net.terramodulus.mui.gui.gfx.RenderSystem +import net.terramodulus.mui.gui.gfx.TextContext +import net.terramodulus.util.nextEntry + +internal class WorldCreateScreen( + renderSystem: RenderSystem, + managerHandle: ScreenManager.Handle, + asdHandle: AsdHandle.Container, + renderSystemHandle: RenderSystem.Handle, + inputStatesHandle: InputStatesHandle, +) : Screen(managerHandle, asdHandle) { + override val layout = CompositeLayout(this) { + add(SingletonLayout(this@WorldCreateScreen, TextDisplayComponent(ComponentAsdHandleImpl(), renderSystemHandle, + TextContext.Config(26F, 26F, ImmVec4i(255)), + ).apply { + text = "World Options" + }, SingletonLayout.Config.Auto( + SingletonLayout.Config.Auto.Side(Direction2S.Negative, 0.0), + SingletonLayout.Config.Auto.Side(Direction2S.Positive, 0.0), + ))) + val options = WorldOptions.Builder() + add(SingletonLayout(this@WorldCreateScreen, SimplePane(ComponentAsdHandleImpl()) { + ColumnLayout.withComponents(listOf( + TextDisplayComponent(ComponentAsdHandleImpl(), renderSystemHandle, + TextContext.Config(22F, 22F, ImmVec4i(255)) + ).apply { text = "World Type" }, + run { + lateinit var listener: () -> Unit + ButtonComponent(ComponentAsdHandleImpl(), inputStatesHandle, { + lateinit var layout: SingletonLayout + SingletonLayout( + this, TextDisplayComponent( + ComponentAsdHandleImpl(), renderSystemHandle, + TextContext.Config(20F, 20F, ImmVec4i(240)) + ).apply { + val listener1 = { + text = when (options.worldType) { + WorldOptions.WorldType.CubeSets -> "Cube Sets" + WorldOptions.WorldType.Flat -> "Flat" + } + }.apply { this() } + listener = { + layout.operate { listener1() } + } + }, SingletonLayout.Config.Sole(SingletonLayout.Config.Scaled.Scale(1.0))).apply { + layout = this + } + }) { + options.worldType = WorldOptions.WorldType.entries.nextEntry(options.worldType) + listener() + } + }, + SizedPane( + ComponentAsdHandleImpl(), + BlankComponent(ComponentAsdHandleImpl()), + SizedPane.Config(1u, 20u), + ), + TextDisplayComponent(ComponentAsdHandleImpl(), renderSystemHandle, + TextContext.Config(22F, 22F, ImmVec4i(255)) + ).apply { text = "Character Type" }, + run { + lateinit var listener: () -> Unit + ButtonComponent(ComponentAsdHandleImpl(), inputStatesHandle, { + lateinit var layout: SingletonLayout + SingletonLayout( + this, TextDisplayComponent( + ComponentAsdHandleImpl(), renderSystemHandle, + TextContext.Config(20F, 20F, ImmVec4i(240)) + ).apply { + val listener1 = { + text = when (options.charType) { + WorldOptions.CharacterType.Sphere -> "Sphere" + WorldOptions.CharacterType.Complex -> "Complex" + } + }.apply { this() } + listener = { + layout.operate { listener1() } + } + }, SingletonLayout.Config.Sole(SingletonLayout.Config.Scaled.Scale(1.0))).apply { + layout = this + } + }) { + options.charType = WorldOptions.CharacterType.entries.nextEntry(options.charType) + listener() + } + }, + ), SequenceLayout.Config(Direction2S.Negative, 3.0, intrinsic = true))(this) + }, SingletonLayout.Config.Aligned( + SingletonLayout.Config.Scaled.Scale(1.0), + SingletonLayout.Config.AlignmentConfig.DEFAULT, + ))) + add(SingletonLayout(this@WorldCreateScreen, ButtonComponent(ComponentAsdHandleImpl(), inputStatesHandle, { + SingletonLayout(this, TextDisplayComponent(ComponentAsdHandleImpl(), renderSystemHandle, + TextContext.Config(24F, 24F, ImmVec4i(255)), + ).apply { + text = "Create World" + }, SingletonLayout.Config.Sole(SingletonLayout.Config.Scaled.Scale(1.0))) + }) { + managerHandle.reset(renderSystem.newGameplayScreen(options.build(), ZeroImmVec3f)) + }, SingletonLayout.Config.Auto( + SingletonLayout.Config.Auto.Side(Direction2S.Positive, 0.0), + SingletonLayout.Config.Auto.Side(Direction2S.Negative, 0.0), + ))) + } + + init { + renderSystemHandle.setBackgroundColor(0F, 0F, 0F, 0F) + } + + data class WorldOptions(val worldType: WorldType, val charType: CharacterType) { + enum class WorldType { CubeSets, Flat } + enum class CharacterType { Sphere, Complex } + + class Builder { + var worldType = WorldType.CubeSets + var charType = CharacterType.Sphere + + fun build() = WorldOptions(worldType, charType) + } + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/WorldInitScreen.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/WorldInitScreen.kt new file mode 100644 index 00000000..65c0dc4f --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/WorldInitScreen.kt @@ -0,0 +1,187 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.impl + +import com.cout970.math.vec4.ImmVec4i +import net.terramodulus.mui.gui.agim.Screen +import net.terramodulus.mui.gui.agim.ScreenManager +import net.terramodulus.mui.gui.agim.event.ScreenEvent +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.gfx.AlphaFilter +import net.terramodulus.mui.gui.gfx.Dimension2D +import net.terramodulus.mui.gui.gfx.Direction4A +import net.terramodulus.mui.gui.gfx.GuiRect +import net.terramodulus.mui.gui.gfx.GuiSprite +import net.terramodulus.mui.gui.gfx.InsetsD +import net.terramodulus.mui.gui.gfx.RectangleD +import net.terramodulus.mui.gui.gfx.RectangleI +import net.terramodulus.mui.gui.gfx.RenderSystem +import net.terramodulus.mui.gui.gfx.TextContext +import net.terramodulus.void.World +import kotlin.math.roundToInt + +private const val ANI_DURATION = 1F // in second + +class WorldInitScreen internal constructor( + managerHandle: ScreenManager.Handle, + asdHandle: AsdHandle.Container, + renderSystemHandle: RenderSystem.Handle, +) : Screen(managerHandle, asdHandle) { + private var stage = 0 + private var last = System.currentTimeMillis() // timestamp in milliseconds + private var alphaFilter = AlphaFilter(0F) +// private val progressBar = ProgressBar(renderSystemHandle) + private val progressBarComponent = ScaledBarComponent( + renderSystemHandle.canvasHandle, + ComponentAsdHandleImpl(), + ) { config(xPos, ImmVec4i(59, 12, 120, 255), 0.0) }.apply { + addFilter(alphaFilter) + } + override val layout = CompositeLayout(this) + private val progressBarImpl = ProgressBarImpl() + internal val progressBar: World.ProgressBar = progressBarImpl + private var onAlphaChange: () -> Unit = {} + + init { + layout.update { + add(SingletonLayout(this@WorldInitScreen, GeomComponent(GuiRect( + renderSystemHandle.canvasHandle, 0, 0, 1, 1, 56, 255, 252, 255 + ), RectangleD(0.0, 0.0, 1.0, 1.0), ComponentAsdHandleImpl()).apply { + geom.add(alphaFilter) + }, SingletonLayout.Config.Absolute.Full)) + add(SingletonLayout(this@WorldInitScreen, SimplePane(ComponentAsdHandleImpl()) { + SingletonLayout(this, DrawablesComponent(sequenceOf( + DrawablesComponent.Drawable( + GuiSprite( + renderSystemHandle.canvasHandle, + RectangleI(0, 100, 400, 100), + renderSystemHandle.loadTexture("/game_logo.png") + ) + ), + DrawablesComponent.Drawable( + GuiRect(renderSystemHandle.canvasHandle, 0, 0, 400, 40, 59, 12, 120, 255) + ), + DrawablesComponent.Drawable( + GuiRect(renderSystemHandle.canvasHandle, 5, 5, 395, 35, 56, 255, 252, 255) + ), +// DrawablesComponent.Drawable(progressBar.rect), + ), RectangleD(0.0, 0.0, 400.0, 200.0), ComponentAsdHandleImpl()).apply { + addFilter(alphaFilter) + }, SingletonLayout.Config.Aligned( + SingletonLayout.Config.ObjectFit.Contain, + SingletonLayout.Config.AlignmentConfig.DEFAULT, + )) + }, SingletonLayout.Config.Aligned( + SingletonLayout.Config.Relative.Simple(0.5), + SingletonLayout.Config.AlignmentConfig.DEFAULT, + ))) + add(SingletonLayout(this@WorldInitScreen, SimplePane(ComponentAsdHandleImpl()) { + SingletonLayout(this, SizedPane(ComponentAsdHandleImpl(), SimplePane(ComponentAsdHandleImpl()) { + SingletonLayout(this, progressBarComponent, + SingletonLayout.Config.Absolute.Insets(InsetsD(7.0, 167.0, 7.0, 7.0)) + ) + }, + SizedPane.Config(400u, 200u)), SingletonLayout.Config.Aligned( + SingletonLayout.Config.ObjectFit.Contain, + SingletonLayout.Config.AlignmentConfig.DEFAULT, + ) + ) + }, SingletonLayout.Config.Aligned( + SingletonLayout.Config.Relative.Simple(0.5), + SingletonLayout.Config.AlignmentConfig.DEFAULT, + ))) + add(SingletonLayout(this@WorldInitScreen, SimplePane(ComponentAsdHandleImpl()) { + SingletonLayout(this, SizedPane(ComponentAsdHandleImpl(), SimplePane(ComponentAsdHandleImpl()) { + SingletonLayout(this, TextDisplayComponent( + ComponentAsdHandleImpl(), + renderSystemHandle, + TextContext.Config(16.0F, 16.0F, ImmVec4i(255, 255, 255, 0)), + ).apply { + text = "Initializing Demo World..." + onAlphaChange = { + update { + color = ImmVec4i(255, 255, 255, (alphaFilter.alpha * 255).roundToInt()) + } + } + }, SingletonLayout.Config.Absolute.Insets(InsetsD(7.0, 142.0, 7.0, 42.0))) + }, + SizedPane.Config(400u, 200u)), SingletonLayout.Config.Aligned( + SingletonLayout.Config.ObjectFit.Contain, + SingletonLayout.Config.AlignmentConfig.DEFAULT, + ) + ) + }, SingletonLayout.Config.Aligned( + SingletonLayout.Config.Relative.Simple(0.5), + SingletonLayout.Config.AlignmentConfig.DEFAULT, + ))) + } + + addListener(ScreenEvent.Update::class.java) { + val current = System.currentTimeMillis() + val elapsed = (current - last) / 1000F // elapsed time in second at this stage + when (stage) { + 0 -> if (elapsed >= ANI_DURATION) { + stage = 1 + last = current + alphaFilter.alpha = 1F + onAlphaChange() + progressBarImpl.ready = true + } else { + alphaFilter.alpha = elapsed / ANI_DURATION + onAlphaChange() + } + + 1 -> { + if (progressBarComponent.fraction >= 1) { + progressBarComponent.fraction = 1.0 + stage = 2 + last = current + } + } + + 2 -> if (elapsed >= ANI_DURATION) { + stage = 3 + last = current + alphaFilter.alpha = 0F + onAlphaChange() + } else { + alphaFilter.alpha = 1 - elapsed / ANI_DURATION + onAlphaChange() + } + +// 3 -> screenManager.handle.openBefore(::TitleScreen, this) + 3 -> it.muiIoI.screenManager.handle.exit(1) + } + } + } + + private inner class ProgressBarImpl : World.ProgressBar { + var ready = false + set(value) { + field = value + readyListener?.invoke() + } + var readyListener: (() -> Unit)? = null + + override fun addReadyListener(listener: () -> Unit) { + readyListener = listener + if (ready) listener() + } + + override fun setProgress(progress: Double) { + progressBarComponent.fraction = progress + } + } + +// internal class ProgressBar(renderSystemHandle: RenderSystem.Handle) { +// val rectDim = Rectangle.withPoints(7, 7, 393, 33) +// val length = rectDim.width +// var progress: Float by Delegates.observable(0f) { _, _, _ -> +// rect.setPos(7, 7, rectDim.x + (progress * length).toInt(), 33) +// } +// val rect = GuiRect(renderSystemHandle.canvasHandle, 7, 7, 7, 33, 240, 240, 240, 255) +// } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/AsdHandle.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/AsdHandle.kt new file mode 100644 index 00000000..660d2c0f --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/AsdHandle.kt @@ -0,0 +1,59 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.asd + +import net.terramodulus.mui.gui.agim.AgimoProperty +import net.terramodulus.mui.gui.agim.AgimoPropertyMap +import net.terramodulus.mui.gui.gfx.RectangleD +import net.terramodulus.mui.gui.gfx.RectangleF +import net.terramodulus.util.TypedAnchorMap +import java.util.function.BiFunction + +/** + * Since rectangles are modified only during layout processing, + * further follow-ups must not be deferred to next tick. + */ +abstract class AsdHandle internal constructor() { + /** + * Caveat: Must only be modified by [Layout][net.terramodulus.mui.gui.agim.Layout]. + * When modified, [triggerRectObservers] must be invoked. + */ + abstract var rect: RectangleD + internal set + + protected val rectObservers = LinkedHashSet<() -> Unit>() + + internal fun observeRect(observer: () -> Unit) { + rectObservers.add(observer) + } + + internal fun unobserveRect(observer: () -> Unit) { + rectObservers.remove(observer) + } + + internal fun triggerRectObservers() { + rectObservers.forEach { it() } + } + + /** + * Permanent AGIMO Properties + */ + val properties = AgimoPropertyMap() + + /** + * Registers ASD Processors from AGIMOs to [AsdManager]. + */ + abstract fun registerAsdProcessor(processor: AsdProcessor<*>) + + // TODO likely those below are useless + abstract class Container : AsdHandle() {} + + abstract class Menu : Container() {} + + abstract class Screen : Container() {} + +// interface Component : LayoutHandle {} // Do we need this? +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/AsdIr.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/AsdIr.kt new file mode 100644 index 00000000..355c613c --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/AsdIr.kt @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.asd + +/** + * ASD Intermediate Representation (IR) + */ +class AsdIr { +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/AsdManager.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/AsdManager.kt new file mode 100644 index 00000000..6d6e75af --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/AsdManager.kt @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.asd + +import net.terramodulus.mui.gui.agim.AgimoTreeVisitor + +// TODO Should ASD affect choices of Layout? +// There are two proposals: +// - Each Container manages a Layout, then the Layout is put into LayoutManager/LayoutViewport +// - Each Container is totally managed by AsdManager except for Facets, but this rather complicated for ASD +internal class AsdManager internal constructor() { + companion object { + // TODO temporary demonstrative testing default + internal fun default(): AsdManager = AsdManager() + } + + private val processors = HashSet>() + + internal fun registerProcessor(processor: AsdProcessor<*>) { + processors.add(processor) + } + + internal inner class AgimHandle { + internal fun registerProcessor(processor: AsdProcessor<*>) = this@AsdManager.registerProcessor(processor) + } + + internal fun process() { + fun process(processor: AsdProcessor) = processor.processDefined(processor.produceDefined()) + processors.forEach { process(it) } + } + + // TODO What should be the use of this + private inner class TreeVisitor(visitorScreens: ScreenTreeVisitor, visitorMenus: MenuTreeVisitor) : AgimoTreeVisitor() { + init { + val root = RootNode(visitorScreens.visit(), visitorMenus.visit()) + } + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/AsdProcessor.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/AsdProcessor.kt new file mode 100644 index 00000000..4a50dc05 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/AsdProcessor.kt @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.asd + +import net.terramodulus.engine.BaseAsdProcessor + +// TODO in reality, only binary data are consumed, but +// in testing environment, objects are created directly +// - most likely, each AGIMO and Layout register necessary Processor +// to consume input data and configure its managed instance of AGIMO/Layout +abstract class AsdProcessor { + companion object { + fun get(): BaseAsdProcessor = TODO("This processor's only use is to provide InputStream") + } + + // TODO Temporary implementation to configure ASD without binary data but produced configs + abstract fun processDefined(definitions: T) + + // TODO Temporary implementation to produce defined configurations for testing and demo + abstract fun produceDefined(): T +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/MenuStyles.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/MenuStyles.kt new file mode 100644 index 00000000..0022ce5d --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/MenuStyles.kt @@ -0,0 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.asd + +class MenuStyles { +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/ScreenStyles.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/ScreenStyles.kt new file mode 100644 index 00000000..76571c6e --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/ScreenStyles.kt @@ -0,0 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.asd + +class ScreenStyles { +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/Anchor.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Anchor.kt similarity index 83% rename from src/kernel/client/kotlin/net/terramodulus/mui/gfx/Anchor.kt rename to src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Anchor.kt index b000efe7..b3880d64 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/Anchor.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Anchor.kt @@ -1,9 +1,9 @@ /* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors * SPDX-License-Identifier: LGPL-3.0-only */ -package net.terramodulus.mui.gfx +package net.terramodulus.mui.gui.gfx /* * Every set of anchor position directions has different meanings in different context, diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/ColorFilter.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/ColorFilter.kt new file mode 100644 index 00000000..fa70486f --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/ColorFilter.kt @@ -0,0 +1,13 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.gfx + +import net.terramodulus.engine.AlphaFilter +import net.terramodulus.engine.ColorFilter + +typealias ColorFilter = ColorFilter + +typealias AlphaFilter = AlphaFilter diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/Dimension.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Dimension.kt similarity index 65% rename from src/kernel/client/kotlin/net/terramodulus/mui/gfx/Dimension.kt rename to src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Dimension.kt index 6825bcb0..ede9a02c 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/Dimension.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Dimension.kt @@ -1,14 +1,16 @@ /* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors * SPDX-License-Identifier: LGPL-3.0-only */ -package net.terramodulus.mui.gfx +package net.terramodulus.mui.gui.gfx data class Dimension2I(val width: Int, val height: Int) data class Dimension2F(val width: Float, val height: Float) +data class Dimension2D(val width: Double, val height: Double) + data class Dimension3I(val width: Int, val height: Int, val length: Int) data class Dimension3F(val width: Float, val height: Float, val length: Float) diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/Direction.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Direction.kt similarity index 63% rename from src/kernel/client/kotlin/net/terramodulus/mui/gfx/Direction.kt rename to src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Direction.kt index 621776f8..8061932b 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/Direction.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Direction.kt @@ -1,9 +1,9 @@ /* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors * SPDX-License-Identifier: LGPL-3.0-only */ -package net.terramodulus.mui.gfx +package net.terramodulus.mui.gui.gfx /* * Every set of directions has different meanings in different context, @@ -11,6 +11,34 @@ package net.terramodulus.mui.gfx * Those should be used with care since they are not already interconvertible. */ +/** + * Set of 2 (numeric) signed directions + */ +enum class Direction2S { + Positive, Negative; +} + +/** + * Set of 2 absolute directions + */ +enum class Direction2A { + Start, End; +} + +/** + * Set of 2 diagonal directions + */ +enum class Direction2D { + Horizontal, Vertical; + + companion object { // aliases + val Horiz = Horizontal + val Hor = Horizontal + val Vert = Vertical + val Vrt = Vertical + } +} + /** * Set of 4 compass directions. */ @@ -37,6 +65,20 @@ enum class Direction4H { */ enum class Direction4A { XPos, XNeg, YPos, YNeg; + + fun toOppo() = when (this) { + XPos -> XNeg + XNeg -> XPos + YPos -> YNeg + YNeg -> YPos + } +} + +/** + * Set of 4 axial diagonal directions, by Cartesian quadrants. + */ +enum class Direction4AD { + QuadOne, QuadTwo, QuadThree, QuadFour; } /** diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/GuiGeometry.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/GuiGeometry.kt similarity index 55% rename from src/kernel/client/kotlin/net/terramodulus/mui/gfx/GuiGeometry.kt rename to src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/GuiGeometry.kt index cc6a61b4..6515e0ff 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/GuiGeometry.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/GuiGeometry.kt @@ -1,9 +1,9 @@ /* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors * SPDX-License-Identifier: LGPL-3.0-only */ -package net.terramodulus.mui.gfx +package net.terramodulus.mui.gui.gfx import net.terramodulus.engine.GeomDrawable import net.terramodulus.engine.SimpleLineGeom @@ -14,19 +14,19 @@ sealed class GuiGeometry(protected val geom: GeomDrawable) { fun add(filter: ColorFilter) = geom.add(filter) - fun setPos(pos: FloatArray) = geom.setPos(pos) + protected fun setPos(pos: FloatArray) = geom.setPos(pos) fun render(renderSystem: RenderSystem) = renderSystem.renderGuiGeo(geom) } -class GuiLine(x0: Int, y0: Int, x1: Int, y1: Int, r: Int, g: Int, b: Int, a: Int) : - GuiGeometry(SimpleLineGeom(x0, y0, x1, y1, r, g, b, a)) { +class GuiLine(handle: RenderSystem.CanvasHandle, x0: Int, y0: Int, x1: Int, y1: Int, r: Int, g: Int, b: Int, a: Int) : + GuiGeometry(SimpleLineGeom(handle.canvas, x0, y0, x1, y1, r, g, b, a)) { fun setPos(x0: Int, y0: Int, x1: Int, y1: Int) = setPos(floatArrayOf(x0.toFloat(), y0.toFloat(), x1.toFloat(), y1.toFloat())) } -class GuiRect(x0: Int, y0: Int, x1: Int, y1: Int, r: Int, g: Int, b: Int, a: Int) : - GuiGeometry(SimpleRectGeom(x0, y0, x1, y1, r, g, b, a)) { +class GuiRect(handle: RenderSystem.CanvasHandle, x0: Int, y0: Int, x1: Int, y1: Int, r: Int, g: Int, b: Int, a: Int) : + GuiGeometry(SimpleRectGeom(handle.canvas, x0, y0, x1, y1, r, g, b, a)) { fun setPos(x0: Int, y0: Int, x1: Int, y1: Int) = setPos(floatArrayOf(x0.toFloat(), y0.toFloat(), x1.toFloat(), y1.toFloat())) } diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/GuiSprite.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/GuiSprite.kt new file mode 100644 index 00000000..67f869ad --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/GuiSprite.kt @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.gfx + +import net.terramodulus.engine.SpriteMesh + +class GuiSprite(handle: RenderSystem.CanvasHandle, val rect: RectangleI, private val texture: UInt) { + private val mesh = SpriteMesh(handle.canvas, rect.x, rect.y, rect.x + rect.width, rect.y + rect.height) + + fun add(model: ModelTransform) = mesh.add(model) + + fun add(filter: ColorFilter) = mesh.add(filter) + + fun render(renderSystem: RenderSystem) = renderSystem.renderGuiTex(mesh, texture) +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Insets.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Insets.kt new file mode 100644 index 00000000..623d7554 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Insets.kt @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.gfx + +abstract class Insets(open val left: N, open val top: N, open val right: N, open val bottom: N) { + +} + +data class InsetsI( + override val left: Int, + override val top: Int, + override val right: Int, + override val bottom: Int, +) : Insets(left, top, right, bottom) + +data class InsetsF( + override val left: Float, + override val top: Float, + override val right: Float, + override val bottom: Float, +) : Insets(left, top, right, bottom) { + operator fun plus(other: InsetsF) = InsetsF(left + other.left, top + other.top, right + other.right, bottom + other.bottom) +} + +data class InsetsD( + override val left: Double, + override val top: Double, + override val right: Double, + override val bottom: Double, +) : Insets(left, top, right, bottom) { + operator fun plus(other: InsetsD) = InsetsD(left + other.left, top + other.top, right + other.right, bottom + other.bottom) +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/ModelTransform.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/ModelTransform.kt new file mode 100644 index 00000000..590c4280 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/ModelTransform.kt @@ -0,0 +1,75 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.gfx + +import com.cout970.math.vec2.ImmVec2d +import net.terramodulus.engine.GeneralTransform +import net.terramodulus.engine.ModelTransform + +typealias ModelTransform = ModelTransform + +typealias GeneralTransform = GeneralTransform + +/** + * Scaling and Translation transform parameters (for SRT Transform) for a rectangle. + */ +data class RectStParams(val scaleX: Double, val scaleY: Double, val translateX: Double, val translateY: Double) { + companion object { + /** + * @param a source rectangle + * @param b target rectangle + */ + fun fromRects(a: RectangleD, b: RectangleD): RectStParams { + val sx = b.width / a.width + val sy = b.height / a.height + val tx = b.x - a.x * sx + val ty = b.y - a.y * sy + return RectStParams(sx, sy, tx, ty) + } + + fun withScale(bounds: RectangleD, dir: Direction4A, scale: Double): RectStParams { + val sx: Double + val sy: Double + val tx: Double + val ty: Double + when (dir) { + Direction4A.XPos -> { + sx = scale + sy = 1.0 + tx = 0.0 + ty = 0.0 + } + Direction4A.XNeg -> { + sx = scale + sy = 1.0 + tx = bounds.width * (1 - sx) + ty = 0.0 + } + Direction4A.YPos -> { + sx = 1.0 + sy = scale + tx = 0.0 + ty = 0.0 + } + Direction4A.YNeg -> { + sx = 1.0 + sy = scale + tx = 0.0 + ty = bounds.height * (1 - sy) + } + } + return RectStParams(sx, sy, tx, ty) + } + } + + fun applyToGeneralTransform(generalTransform: GeneralTransform) { + generalTransform.update { + scale = ImmVec2d(scaleX, scaleY) + angle = 0.0 + pos = ImmVec2d(translateX, translateY) + } + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Rectangle.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Rectangle.kt new file mode 100644 index 00000000..3cb54388 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Rectangle.kt @@ -0,0 +1,242 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.gfx + +import com.cout970.math.vec2.ImmVec2d +import com.cout970.math.vec2.ImmVec2f +import com.cout970.math.vec2.ImmVec2i +import com.cout970.math.vec2.Vec2 +import com.cout970.math.vec2.Vec2d +import com.cout970.math.vec2.Vec2f +import com.cout970.math.vec2.Vec2i + +/** + * Rectangle in a coordinate system with (0, 0) on the bottom left. + * The anchor of the rectangle is the bottom-left corner. + */ +sealed class Rectangle, N: Number, V: Vec2, D>( + open val x: N, + open val y: N, + open val width: N, + open val height: N, +) { + companion object { + fun withPoints(x0: Int, y0: Int, x1: Int, y1: Int): RectangleI { + val minX: Int; + val maxX: Int; + if (x0 < x1) { + minX = x0; + maxX = x1; + } else { + maxX = x0; + minX = x1; + } + val minY: Int; + val maxY: Int; + if (y0 < y1) { + minY = y0; + maxY = y1; + } else { + maxY = y0; + minY = y1; + } + return RectangleI(minX, minY, maxX - minX, maxY - minY) + } + + fun withPoints(x0: Float, y0: Float, x1: Float, y1: Float): RectangleF { + val minX: Float; + val maxX: Float; + if (x0 < x1) { + minX = x0; + maxX = x1; + } else { + maxX = x0; + minX = x1; + } + val minY: Float; + val maxY: Float; + if (y0 < y1) { + minY = y0; + maxY = y1; + } else { + maxY = y0; + minY = y1; + } + return RectangleF(minX, maxX, minY, maxY) + } + + fun withDirection(x: Int, y: Int, width: Int, height: Int, dir: Direction4AD) = when (dir) { + Direction4AD.QuadOne -> RectangleI(x, y, width, height) + Direction4AD.QuadTwo -> RectangleI(x - width, y, width, height) + Direction4AD.QuadThree -> RectangleI(x - width, y - height, width, height) + Direction4AD.QuadFour -> RectangleI(x, y - height, width, height) + } + + fun withDirection(x: Float, y: Float, width: Float, height: Float, dir: Direction4AD) = when (dir) { + Direction4AD.QuadOne -> RectangleF(x, y, width, height) + Direction4AD.QuadTwo -> RectangleF(x - width, y, width, height) + Direction4AD.QuadThree -> RectangleF(x - width, y - height, width, height) + Direction4AD.QuadFour -> RectangleF(x, y - height, width, height) + } + } + + protected abstract fun constructor(x: N, y: N, width: N, height: N): T + protected abstract fun vec2(x: N, y: N): V + protected abstract operator fun N.plus(other: N): N + protected abstract operator fun N.minus(other: N): N + protected abstract operator fun N.div(other: Int): N + protected abstract val V.x: N + protected abstract val V.y: N + + abstract val size: D + + fun anchor(pos: Anchor5) = when (pos) { + Anchor5.TopLeft -> vec2(x, y + width) + Anchor5.TopRight -> vec2(x + width, y + height) + Anchor5.BottomLeft -> vec2(x, y) + Anchor5.BottomRight -> vec2(x + width, y) + Anchor5.Center -> vec2(x + width / 2, y + height / 2) + } + + fun translateBy(pos: V) = constructor(x + pos.x, y + pos.y, width, height) + + fun translateBy(x: N, y: N) = constructor(this.x + x, this.y + y, width, height) + + fun translateByY(y: N) = constructor(x, this.y + y, width, height) + + fun translateByX(x: N) = constructor(this.x + x, y, width, height) + + fun translateToY(y: N) = constructor(x, y, width, height) + + fun translateToX(x: N) = constructor(x, y, width, height) + + fun translateTo(pos: V) = constructor(pos.x, pos.y, width, height) + + fun translateTo(x: N, y: N) = constructor(x, y, width, height) + + /** Inflates the [Rectangle] with the [Insets] */ + operator fun plus(other: Insets) = constructor( + x - other.left, + y - other.bottom, + width + other.left + other.right, + height + other.bottom + other.top, + ) + + /** Deflates the [Rectangle] with the [Insets] */ + operator fun minus(other: Insets) = constructor( + x + other.left, + y + other.bottom, + width - other.left - other.right, + height - other.bottom - other.top, + ) + + abstract fun toInt(): RectangleI + abstract fun toFloat(): RectangleF + abstract fun toDouble(): RectangleD +} + +data class RectangleI( + override val x: Int, + override val y: Int, + override val width: Int, + override val height: Int +) : Rectangle(x, y, width, height) { + override fun constructor(x: Int, y: Int, width: Int, height: Int) = RectangleI(x, y, width, height) + + override fun vec2(x: Int, y: Int) = ImmVec2i(x, y) + + override fun Int.plus(other: Int) = this + other + override fun Int.minus(other: Int) = this - other + override fun Int.div(other: Int) = this / other + + override val Vec2i.x: Int by ::x + override val Vec2i.y: Int by ::y + override val size = Dimension2I(width, height) + + override fun toInt() = this + override fun toFloat() = RectangleF(x.toFloat(), y.toFloat(), width.toFloat(), height.toFloat()) + override fun toDouble() = RectangleD(x.toDouble(), y.toDouble(), width.toDouble(), height.toDouble()) +} + +data class RectangleF( + override val x: Float, + override val y: Float, + override val width: Float, + override val height: Float +) : Rectangle(x, y, width, height) { + override fun constructor( + x: Float, + y: Float, + width: Float, + height: Float + ) = RectangleF(x, y, width, height) + + override fun vec2(x: Float, y: Float) = ImmVec2f(x, y) + + override fun Float.plus(other: Float) = this + other + override fun Float.minus(other: Float) = this - other + override fun Float.div(other: Int) = this / other + + override val Vec2f.x: Float by ::x + override val Vec2f.y: Float by ::y + override val size = Dimension2F(width, height) + + override fun toInt() = RectangleI(x.toInt(), y.toInt(), width.toInt(), height.toInt()) + override fun toFloat() = this + override fun toDouble() = RectangleD(x.toDouble(), y.toDouble(), width.toDouble(), height.toDouble()) +} + +data class RectangleD( + override val x: Double, + override val y: Double, + override val width: Double, + override val height: Double +) : Rectangle(x, y, width, height) { + override fun constructor( + x: Double, + y: Double, + width: Double, + height: Double + ) = RectangleD(x, y, width, height) + + override fun vec2(x: Double, y: Double) = ImmVec2d(x, y) + + override fun Double.plus(other: Double) = this + other + override fun Double.minus(other: Double) = this - other + override fun Double.div(other: Int) = this / other + + override val Vec2d.x: Double by ::x + override val Vec2d.y: Double by ::y + override val size = Dimension2D(width, height) + + override fun toInt() = RectangleI(x.toInt(), y.toInt(), width.toInt(), height.toInt()) + override fun toFloat() = RectangleF(x.toFloat(), y.toFloat(), width.toFloat(), height.toFloat()) + override fun toDouble() = this +} + +@ExposedCopyVisibility +data class RectRange private constructor(val rect: RectangleD, internal val type: Type) { + internal enum class Type { Inclusive, Range, Exclusive } + companion object { + fun inclusive(rect: RectangleD) = RectRange(rect, Type.Inclusive) + fun exclusive(rect: RectangleD) = RectRange(rect, Type.Exclusive) + + /** + * For each axis, inclusive for the lower bound and exclusive for the upper bound. + */ + fun range(rect: RectangleD) = RectRange(rect, Type.Range) + } + + fun contains(pt: Vec2d): Boolean { + val lower = ImmVec2d(rect.x, rect.y) + val upper = ImmVec2d(rect.x + rect.width, rect.y + rect.height) + return when (type) { + Type.Inclusive -> pt.x in lower.x..upper.x && pt.y in lower.y..upper.y + Type.Exclusive -> pt.x > lower.x && pt.x < upper.x && pt.y > lower.y && pt.y < upper.y + Type.Range -> pt.x in lower.x.. withScissor(pos: Vec2i, size: Dimension2I, block: () -> R): R + } + + inner class ScissorSession internal constructor(pos: Vec2i, size: Dimension2I) : AutoCloseable { + init { + canvas.enableScissor(pos.x, pos.y, size.width.toUInt(), size.height.toUInt()) + } + + override fun close() { + canvas.disableScissor() + } + } + + inner class CanvasHandle internal constructor() { + internal val canvas = this@RenderSystem.canvas + } + + private inner class HandleImpl : Handle { + override val canvasHandle = CanvasHandle() + + override fun loadTexture(path: String) = canvas.loadImage(getResourceAsBytes(path)) + + override fun setBackgroundColor(red: Float, green: Float, blue: Float, alpha: Float) { + canvas.setClearColor(red, green, blue, alpha) + } + + override fun renderText(ctx: TextRenderingContext, pos: Vec2f) { + textRenderer.renderText(ctx, canvas, glyphManager, fontManager, pos) + } + + override fun newTextRenderingContext(fontSize: Float, lineHeight: Float, color: Vec4i) = + fontManager.newTextRenderingManager(fontSize, lineHeight, color) + + override fun withScissor(pos: Vec2i, size: Dimension2I, block: () -> R) = + ScissorSession(pos, size).use { _ -> block() } + } + + internal fun newGameplayScreen(options: WorldCreateScreen.WorldOptions, pos: Vec3f) = + { mh: ScreenManager.Handle, ah: AsdHandle.Container, it: Handle, ish: InputStatesHandle -> + GameplayScreen(options, core, canvas.createCamera(floatArrayOf(pos.x, pos.y, pos.z)), it, mh, ah, ish) + } + + internal fun renderGuiTex(drawable: MeshDrawable, texture: UInt) = canvas.renderGuiTex(drawable, texShaders, texture) + + internal fun renderGuiGeo(drawable: GeomDrawable) = canvas.renderGuiGeo(drawable, geoShaders) + + internal fun render() { + + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/TextContext.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/TextContext.kt new file mode 100644 index 00000000..fff755a8 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/TextContext.kt @@ -0,0 +1,68 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.gfx + +import com.cout970.math.vec2.ImmVec2d +import com.cout970.math.vec2.Vec2d +import com.cout970.math.vec2.toImmVec2f +import com.cout970.math.vec4.Vec4i +import kotlin.properties.Delegates + +class TextContext(private val renderSystemHandle: RenderSystem.Handle, private var config: Config) { + private val context = renderSystemHandle.newTextRenderingContext(config.fontSize, config.lineHeight, config.color) + private lateinit var containerSize: Dimension2D + private lateinit var pos: Vec2d + + var size = Dimension2D(0.0, 0.0) + private set + + data class Config(val fontSize: Float, val lineHeight: Float, val color: Vec4i) + + interface ConfigEnv { + var fontSize: Float + var lineHeight: Float + var color: Vec4i + } + + fun update(operation: ConfigEnv.() -> Unit) { + object : ConfigEnv { + var fontSizeChanged = false + override var fontSize: Float by Delegates.observable(config.fontSize) { _, _, _ -> + fontSizeChanged = true + } + var lineHeightChanged = false + override var lineHeight: Float by Delegates.observable(config.lineHeight) { _, _, _ -> + lineHeightChanged = true + } + var colorChanged = false + override var color: Vec4i by Delegates.observable(config.color) { _, _, _ -> + colorChanged = true + } + }.apply(operation).apply { + if (fontSizeChanged || lineHeightChanged) context.setMetrics(fontSize, lineHeight) + if (colorChanged) context.setColor(color) + if (fontSizeChanged || lineHeightChanged || colorChanged) + size = context.fetchSize().let { Dimension2D(it[0].toDouble(), it[1].toDouble()) } + config = Config(fontSize, lineHeight, color) + } + } + + fun update(rect: RectangleD) { + val prevSize = try { containerSize } catch (_: UninitializedPropertyAccessException) { null } + val prevPos = try { pos } catch (_: UninitializedPropertyAccessException) { null } + if (prevSize == null || prevSize.width != rect.width || prevSize.height != rect.height) + containerSize = Dimension2D(rect.width, rect.height) + if (prevPos == null || prevPos.x != rect.x || prevPos.y != rect.y) + pos = ImmVec2d(rect.x, rect.y) + } + + fun setText(text: String) { + context.setText(text) + size = context.fetchSize().let { Dimension2D(it[0].toDouble(), it[1].toDouble()) } + } + + fun render() = renderSystemHandle.renderText(context, pos.toImmVec2f()) +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Vector.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Vector.kt new file mode 100644 index 00000000..47f84c19 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Vector.kt @@ -0,0 +1,6 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.gfx diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/hui/HuiManager.kt b/src/kernel/client/kotlin/net/terramodulus/mui/hui/HuiManager.kt new file mode 100644 index 00000000..f3963b9a --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/hui/HuiManager.kt @@ -0,0 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.hui + +class HuiManager { +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/input/InputSystem.kt b/src/kernel/client/kotlin/net/terramodulus/mui/input/InputSystem.kt deleted file mode 100644 index 7656eed8..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/input/InputSystem.kt +++ /dev/null @@ -1,158 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.mui.input - -// TODO Temporary solution, should be rewritten in next update. -class InputSystem internal constructor() { - private val keys = HashMap() - - // Values refer to ferricia::mui::KeyboardKey - enum class Keys(private val id: KeyId) { - A(KeyId(0u)), - B(KeyId(1u)), - C(KeyId(2u)), - D(KeyId(3u)), - E(KeyId(4u)), - F(KeyId(5u)), - G(KeyId(6u)), - H(KeyId(7u)), - I(KeyId(8u)), - J(KeyId(9u)), - K(KeyId(10u)), - L(KeyId(11u)), - M(KeyId(12u)), - N(KeyId(13u)), - O(KeyId(14u)), - P(KeyId(15u)), - Q(KeyId(16u)), - R(KeyId(17u)), - S(KeyId(18u)), - T(KeyId(19u)), - U(KeyId(20u)), - V(KeyId(21u)), - W(KeyId(22u)), - X(KeyId(23u)), - Y(KeyId(24u)), - Z(KeyId(25u)), - Space(KeyId(40u)), - Minus(KeyId(41u)), - Equals(KeyId(42u)), - LShift(KeyId(205u)), - ; - fun down() = KeyPredicate.Down(id) - fun justDown() = KeyPredicate.JustDown(id) - fun justUp() = KeyPredicate.JustUp(id) - } - - sealed class KeyPredicate { - // Automatically asserted helper - internal class Helper(private val keys: Map) { - operator fun get(key: KeyId) = keys[key]!! - } - - internal abstract fun test(keys: Helper): Boolean - - data class Down(val x: KeyId) : KeyPredicate() { - override fun test(keys: Helper) = keys[x].down - } - - data class JustDown(val x: KeyId) : KeyPredicate() { - override fun test(keys: Helper) = keys[x].isJustDown() - } - - data class JustUp(val x: KeyId) : KeyPredicate() { - override fun test(keys: Helper) = keys[x].isJustUp() - } - - data class And(val x: KeyPredicate, val y: KeyPredicate) : KeyPredicate() { - override fun test(keys: Helper) = x.test(keys) && y.test(keys) - } - - data class Or(val x: KeyPredicate, val y: KeyPredicate) : KeyPredicate() { - override fun test(keys: Helper) = x.test(keys) || y.test(keys) - } - - data class Not(val x: KeyPredicate) : KeyPredicate() { - override fun test(keys: Helper) = !x.test(keys) - } - - operator fun not() = Not(this) - infix fun and(other: KeyPredicate) = And(this, other) - infix fun or(other: KeyPredicate) = Or(this, other) - } - - private val keysScope = KeysScope() - - // This class may be programmatically generated - inner class KeysScope internal constructor() { - val A = Keys.A - val B = Keys.B - val C = Keys.C - val D = Keys.D - val E = Keys.E - val F = Keys.F - val G = Keys.G - val H = Keys.H - val I = Keys.I - val J = Keys.J - val K = Keys.K - val L = Keys.L - val M = Keys.M - val N = Keys.N - val O = Keys.O - val P = Keys.P - val Q = Keys.Q - val R = Keys.R - val S = Keys.S - val T = Keys.T - val U = Keys.U - val V = Keys.V - val W = Keys.W - val X = Keys.X - val Y = Keys.Y - val Z = Keys.Z - val Space = Keys.Space - val Minus = Keys.Minus - val Equals = Keys.Equals - val LShift = Keys.LShift - } - - init { - // Refers to ferricia::mui::KeyboardKey - for (i in 0u..242u) keys[KeyId(i)] = Key() - } - - @JvmInline - value class KeyId(private val id: UInt) - - class Key { - var down = false - internal set - internal var justChanged = false - - fun isJustDown() = down && justChanged - fun isJustUp() = !down && justChanged - } - - fun condition(predicate: KeysScope.() -> KeyPredicate) = keysScope.predicate().test(KeyPredicate.Helper(keys)) - - sealed class KeyEvent private constructor(internal open val key: KeyId) { - data class Down(override val key: KeyId) : KeyEvent(key) - data class Up(override val key: KeyId) : KeyEvent(key) - } - - internal fun update(events: List) { - keys.forEach { (_, key) -> key.justChanged = false } - events.forEach { - // Note: This may not handle the case where a key is just down less than a tick. - keys[it.key]!!.justChanged = true - when (it) { - is KeyEvent.Down -> keys[it.key]!!.down = true - is KeyEvent.Up -> keys[it.key]!!.down = false - } - } - } -} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/kui/GamepadInputHandler.kt b/src/kernel/client/kotlin/net/terramodulus/mui/kui/GamepadInputHandler.kt new file mode 100644 index 00000000..a3563182 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/kui/GamepadInputHandler.kt @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.kui + +class GamepadInputHandler { + // TODO +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/kui/InputSystem.kt b/src/kernel/client/kotlin/net/terramodulus/mui/kui/InputSystem.kt new file mode 100644 index 00000000..ac6d5fa9 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/kui/InputSystem.kt @@ -0,0 +1,55 @@ +/* + * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.kui + +// TODO Tentative solution, may be rewritten later. +/** + * Simple integrated access interface to various input handlers + */ +class InputSystem internal constructor(private val kuiManager: KuiManager) { + sealed class InputPredicateBase> { + sealed interface Helper + + internal abstract fun test(helper: H): Boolean + + class And>(val x: P, val y: P) : InputPredicateBase() { + override fun test(helper: H) = x.test(helper) && y.test(helper) + } + + class Or>(val x: P, val y: P) : InputPredicateBase() { + override fun test(helper: H) = x.test(helper) || y.test(helper) + } + + class Not>(val x: P) : InputPredicateBase() { + override fun test(helper: H) = !x.test(helper) + } + + @Suppress("UNCHECKED_CAST") + operator fun not() = Not(this as P) + @Suppress("UNCHECKED_CAST") + infix fun and(other: P) = And(this as P, other) + @Suppress("UNCHECKED_CAST") + infix fun or(other: P) = Or(this as P, other) + } + + inner class InputsScope { + fun keyboard(predicate: KeyboardInputHandler.KeysScope.() -> KeyboardInputHandler.KeyPredicate) = + kuiManager.keyboardInputHandler.condition(predicate) + } + + private val inputsScope = InputsScope() + + fun condition(predicate: InputsScope.() -> Boolean) = inputsScope.predicate() + + internal sealed class InputEvent private constructor() { + data class Keyboard(val inner: KeyboardInputHandler.KeyEvent) : InputEvent() + data class Mouse(val inner: MouseInputHandler.Event) : InputEvent() + } + + internal fun update(events: Sequence) { + kuiManager.keyboardInputHandler.update(events.filterIsInstance().map { it.inner }) + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/kui/KeyboardInputHandler.kt b/src/kernel/client/kotlin/net/terramodulus/mui/kui/KeyboardInputHandler.kt new file mode 100644 index 00000000..a446654f --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/kui/KeyboardInputHandler.kt @@ -0,0 +1,180 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.kui + +import net.terramodulus.mui.uid.KeyboardDevice + +class KeyboardInputHandler internal constructor(keyboardDevice: KeyboardDevice) { + typealias KeyId = KeyboardDevice.KeyId + + sealed class KeyPredicate : InputSystem.InputPredicateBase() { + // Automatically asserted helper + class Helper internal constructor(private val keys: Map) : InputSystem.InputPredicateBase.Helper { + operator fun get(key: KeyId) = keys[key]!! + } + + @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE") + abstract override fun test(keys: Helper): Boolean + + data class Down(val x: KeyId) : KeyPredicate() { + override fun test(keys: Helper) = keys[x].down + } + + data class JustDown(val x: KeyId) : KeyPredicate() { + override fun test(keys: Helper) = keys[x].justDown + } + + data class JustUp(val x: KeyId) : KeyPredicate() { + override fun test(keys: Helper) = keys[x].justUp + } + } + + sealed interface InnerKeys { + val down: KeyPredicate.Down + val justDown: KeyPredicate.JustDown + val justUp: KeyPredicate.JustUp + fun matches(other: KeyId): Boolean // Is this useful? + } + + private class KeysImpl(private val id: KeyId) : InnerKeys { + override val down get() = KeyPredicate.Down(id) + override val justDown get() = KeyPredicate.JustDown(id) + override val justUp get() = KeyPredicate.JustUp(id) + override fun matches(other: KeyId) = id == other + } + + // Values refer to ferricia::mui::KeyboardKey + enum class Keys(private val id: KeyId) : InnerKeys by KeysImpl(id) { + A(KeyId(0u)), + B(KeyId(1u)), + C(KeyId(2u)), + D(KeyId(3u)), + E(KeyId(4u)), + F(KeyId(5u)), + G(KeyId(6u)), + H(KeyId(7u)), + I(KeyId(8u)), + J(KeyId(9u)), + K(KeyId(10u)), + L(KeyId(11u)), + M(KeyId(12u)), + N(KeyId(13u)), + O(KeyId(14u)), + P(KeyId(15u)), + Q(KeyId(16u)), + R(KeyId(17u)), + S(KeyId(18u)), + T(KeyId(19u)), + U(KeyId(20u)), + V(KeyId(21u)), + W(KeyId(22u)), + X(KeyId(23u)), + Y(KeyId(24u)), + Z(KeyId(25u)), + One(KeyId(26u)), + Two(KeyId(27u)), + Three(KeyId(28u)), + Four(KeyId(29u)), + Five(KeyId(30u)), + Six(KeyId(31u)), + Seven(KeyId(32u)), + Eight(KeyId(33u)), + Nine(KeyId(34u)), + Zero(KeyId(35u)), + Return(KeyId(36u)), + Escape(KeyId(37u)), + Backspace(KeyId(38u)), + Tab(KeyId(39u)), + Space(KeyId(40u)), + Minus(KeyId(41u)), + Equals(KeyId(42u)), + LShift(KeyId(205u)), + } + + object KeysScope { + val A = Keys.A + val B = Keys.B + val C = Keys.C + val D = Keys.D + val E = Keys.E + val F = Keys.F + val G = Keys.G + val H = Keys.H + val I = Keys.I + val J = Keys.J + val K = Keys.K + val L = Keys.L + val M = Keys.M + val N = Keys.N + val O = Keys.O + val P = Keys.P + val Q = Keys.Q + val R = Keys.R + val S = Keys.S + val T = Keys.T + val U = Keys.U + val V = Keys.V + val W = Keys.W + val X = Keys.X + val Y = Keys.Y + val Z = Keys.Z + val One = Keys.One + val Two = Keys.Two + val Three = Keys.Three + val Four = Keys.Four + val Five = Keys.Five + val Six = Keys.Six + val Seven = Keys.Seven + val Eight = Keys.Eight + val Nine = Keys.Nine + val Zero = Keys.Zero + val Return = Keys.Return + val Escape = Keys.Escape + val Backspace = Keys.Backspace + val Tab = Keys.Tab + val Space = Keys.Space + val Minus = Keys.Minus + val Equals = Keys.Equals + val LShift = Keys.LShift + } + + private val keys = HashMap() + + init { + keyboardDevice.iterKeys().forEach { (k, v) -> + keys[k] = Key(v) + } + } + + // Large difference if specific keyboards can be specifically handled + class Key internal constructor(internal val raw: KeyboardDevice.Key) { + val down: Boolean get() = raw.down + internal var justChanged = false + + val justDown: Boolean get() = down && justChanged + val justUp: Boolean get() = !down && justChanged + } + + fun condition(predicate: KeysScope.() -> KeyPredicate) = KeysScope.predicate().test(KeyPredicate.Helper(keys)) + + sealed class KeyEvent private constructor(internal open val key: KeyId) { + data class Down(override val key: KeyId) : KeyEvent(key) + data class Up(override val key: KeyId) : KeyEvent(key) + } + + internal fun update(events: Sequence) { + keys.values.forEach { it.justChanged = false } + events.forEach { + // Note: This may not handle the case where a key is just down less than a tick. + // This also assumes that keyboard states are consistent across time frames. + keys[it.key]!!.justChanged = true + when (it) { + is KeyEvent.Down -> keys[it.key]!!.raw.down = true + is KeyEvent.Up -> keys[it.key]!!.raw.down = false + } + } + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/kui/KuiManager.kt b/src/kernel/client/kotlin/net/terramodulus/mui/kui/KuiManager.kt new file mode 100644 index 00000000..d5ced3f1 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/kui/KuiManager.kt @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.kui + +import net.terramodulus.mui.uid.UidManager + +class KuiManager internal constructor(uidManager: UidManager) { + val keyboardInputHandler = KeyboardInputHandler(uidManager.keyboardDevice) + val mouseInputHandler = MouseInputHandler(uidManager.mouseDevice) + val inputSystem = InputSystem(this) + +// internal fun update(events: List) { +// inputSystem.update(events) +// } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/kui/MouseInputHandler.kt b/src/kernel/client/kotlin/net/terramodulus/mui/kui/MouseInputHandler.kt new file mode 100644 index 00000000..29d32556 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/kui/MouseInputHandler.kt @@ -0,0 +1,112 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.kui + +import net.terramodulus.mui.uid.MouseDevice +import kotlin.sequences.forEach + +class MouseInputHandler(mouseDevice: MouseDevice) { + typealias ButtonId = MouseDevice.ButtonId + + sealed class ButtonPredicate : InputSystem.InputPredicateBase() { + // Automatically asserted helper + class Helper internal constructor(private val buttons: Map) : InputSystem.InputPredicateBase.Helper { + operator fun get(button: ButtonId) = buttons[button]!! + } + + @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE") + abstract override fun test(buttons: Helper): Boolean + + data class Down(val x: ButtonId) : ButtonPredicate() { + override fun test(buttons: Helper) = buttons[x].down + } + + data class JustDown(val x: ButtonId) : ButtonPredicate() { + override fun test(buttons: Helper) = buttons[x].justDown + } + + data class JustUp(val x: ButtonId) : ButtonPredicate() { + override fun test(buttons: Helper) = buttons[x].justUp + } + } + + sealed interface InnerButtons { + val id: ButtonId + val down: ButtonPredicate.Down + val justDown: ButtonPredicate.JustDown + val justUp: ButtonPredicate.JustUp + fun matches(other: ButtonId): Boolean // Is this useful? + } + + private class ButtonsImpl(override val id: ButtonId) : InnerButtons { + override val down get() = ButtonPredicate.Down(id) + override val justDown get() = ButtonPredicate.JustDown(id) + override val justUp get() = ButtonPredicate.JustUp(id) + override fun matches(other: ButtonId) = id == other + } + + // Values refer to ferricia::mui::KeyboardKey + enum class Buttons(override val id: ButtonId) : InnerButtons by ButtonsImpl(id) { + Left(ButtonId(0u)), + Middle(ButtonId(1u)), + Right(ButtonId(2u)), + X1(ButtonId(3u)), + X2(ButtonId(4u)), + } + + object ButtonsScope { + val Left = Buttons.Left + val Middle = Buttons.Middle + val Right = Buttons.Right + val X1 = Buttons.X1 + val X2 = Buttons.X2 + } + + private val buttons = HashMap() + + init { + mouseDevice.iterButtons().forEach { (k, v) -> + buttons[k] = Button(v) + } + } + + // Large difference if specific keyboards can be specifically handled + class Button internal constructor(internal val raw: MouseDevice.Button) { + val down: Boolean get() = raw.down + internal var justChanged = false + + val justDown: Boolean get() = down && justChanged + val justUp: Boolean get() = !down && justChanged + } + + fun condition(predicate: ButtonsScope.() -> ButtonPredicate) = ButtonsScope.predicate().test(ButtonPredicate.Helper(buttons)) + + internal sealed class Event private constructor() { + sealed class Button private constructor(open val key: ButtonId) : Event() { + data class Down(override val key: ButtonId) : Button(key) + data class Up(override val key: ButtonId) : Button(key) + } + data class Movement(val delX: Float, val delY: Float) : Event() + } + + internal fun update(events: Sequence) { + buttons.values.forEach { it.justChanged = false } + events.forEach { + when (it) { + is Event.Button -> { + // Note: This may not handle the case where a key is just down less than a tick. + // This also assumes that keyboard states are consistent across time frames. + buttons[it.key]!!.justChanged = true + when (it) { + is Event.Button.Down -> buttons[it.key]!!.raw.down = true + is Event.Button.Up -> buttons[it.key]!!.raw.down = false + } + } + is Event.Movement -> {} // TODO + } + } + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/kui/VirtualKeyboard.kt b/src/kernel/client/kotlin/net/terramodulus/mui/kui/VirtualKeyboard.kt new file mode 100644 index 00000000..aac31b62 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/kui/VirtualKeyboard.kt @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.kui + +class VirtualKeyboard { + // TODO +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/uid/Device.kt b/src/kernel/client/kotlin/net/terramodulus/mui/uid/Device.kt new file mode 100644 index 00000000..6cfd2508 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/uid/Device.kt @@ -0,0 +1,13 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.uid + +sealed interface Device { + val id: Id + + @JvmInline + value class Id(private val value: UInt) +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/uid/GamepadDevice.kt b/src/kernel/client/kotlin/net/terramodulus/mui/uid/GamepadDevice.kt new file mode 100644 index 00000000..9f1e0ffa --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/uid/GamepadDevice.kt @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.uid + +class GamepadDevice(override val id: Device.Id) : Device { + // TODO +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/uid/KeyboardDevice.kt b/src/kernel/client/kotlin/net/terramodulus/mui/uid/KeyboardDevice.kt new file mode 100644 index 00000000..10b69670 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/uid/KeyboardDevice.kt @@ -0,0 +1,27 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.uid + +import kotlin.collections.set + +class KeyboardDevice internal constructor(override val id: Device.Id) : Device { + internal class Key internal constructor() { + var down = false + } + + @JvmInline + value class KeyId(private val id: UInt) + + private val keys = HashMap() + + init { + // Refers to ferricia::mui::KeyboardKey + for (i in 0u..242u) keys[KeyId(i)] = Key() + } + + internal fun getKey(key: KeyId) = keys[key] + internal fun iterKeys() = keys.asSequence() +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/uid/MouseDevice.kt b/src/kernel/client/kotlin/net/terramodulus/mui/uid/MouseDevice.kt new file mode 100644 index 00000000..c8fce41c --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/uid/MouseDevice.kt @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.uid + +class MouseDevice(override val id: Device.Id) : Device { + class Button internal constructor() { + var down = false + } + + @JvmInline + value class ButtonId(private val id: UInt) + + private val buttons = HashMap() + + init { + // Refers to ferricia::mui::MouseKey + for (i in 0u..4u) buttons[ButtonId(i)] = Button() + } + + internal fun getKey(key: ButtonId) = buttons[key] + internal fun iterButtons() = buttons.asSequence() +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/uid/UidManager.kt b/src/kernel/client/kotlin/net/terramodulus/mui/uid/UidManager.kt new file mode 100644 index 00000000..d0ae87f4 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/uid/UidManager.kt @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.uid + +/** + * User Interface Device (UID) Manager + */ +internal class UidManager { + class Devices internal constructor() { + private val devices = mutableMapOf() + + fun count() = devices.count() + + fun exists(id: Device.Id) = devices.containsKey(id) + + internal fun add(device: D) { + devices[device.id] = device + } + + internal fun remove(deviceId: Device.Id) { + if (devices.remove(deviceId) == null) + throw IllegalStateException("Device [$javaClass] $deviceId does not exist") + } + } + + typealias KeyboardDevices = Devices + typealias MouseDevices = Devices + + // not yet used +// val keyboardDevices = KeyboardDevices() +// val mouseDevices = MouseDevices() + val keyboardDevice = KeyboardDevice(Device.Id(0u)) + val mouseDevice = MouseDevice(Device.Id(0u)) +} diff --git a/src/kernel/client/resources/gms_txt.fsh b/src/kernel/client/resources/gms_txt.fsh new file mode 100644 index 00000000..a3e62a16 --- /dev/null +++ b/src/kernel/client/resources/gms_txt.fsh @@ -0,0 +1,30 @@ +#version 110 + +varying vec2 texCoord; + +uniform sampler2D msdfTex; // The generated MSDF atlas texture +uniform vec4 textColor; // Desired text color +uniform vec2 texSize; + +float median(float r, float g, float b) { + return max(min(r, g), min(max(r, g), b)); +} + +#define DISTANCE_RANGE 3.0 +#define FRACTION_1_64 0.015625 + +// Reference: https://github.com/Blatko1/awesome-msdf +// Reference: https://medium.com/@sihaolu/performant-crisp-text-rendering-in-metal-with-multi-channel-signed-distance-field-msdf-9acd634d0052 +void main() { + vec3 texel = texture2D(msdfTex, texCoord).rgb; + float dist = median(texel.r, texel.g, texel.b); + + vec2 screenTexSize = 1.0 / fwidth(texCoord); + float screenPxRange = max(0.5 * dot(vec2(DISTANCE_RANGE) / texSize, screenTexSize), 1.0); + float pxDist = screenPxRange * (dist - 0.5); + + float opacity = clamp(pxDist + 0.5, 0.0, 1.0); + + if (opacity < FRACTION_1_64) discard; + gl_FragColor = vec4(textColor.rgb, textColor.a * opacity); +} diff --git a/src/kernel/common/kotlin/net/terramodulus/common/core/AbstractTerraModulus.kt b/src/kernel/common/kotlin/net/terramodulus/common/core/AbstractTerraModulus.kt index b0373bdf..0636a9f2 100644 --- a/src/kernel/common/kotlin/net/terramodulus/common/core/AbstractTerraModulus.kt +++ b/src/kernel/common/kotlin/net/terramodulus/common/core/AbstractTerraModulus.kt @@ -15,8 +15,5 @@ import java.io.Closeable * @constructor Cannot be `internal` because `client` and `server` are other modules. */ abstract class AbstractTerraModulus : Closeable { - abstract var tps: Int - protected set - abstract fun run() } diff --git a/src/kernel/common/kotlin/net/terramodulus/util/Enum.kt b/src/kernel/common/kotlin/net/terramodulus/util/Enum.kt new file mode 100644 index 00000000..630400f1 --- /dev/null +++ b/src/kernel/common/kotlin/net/terramodulus/util/Enum.kt @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.util + +import kotlin.enums.EnumEntries + +fun , L : EnumEntries> L.nextEntry(e: E) = this[(e.ordinal + 1) % size] diff --git a/src/kernel/common/kotlin/net/terramodulus/util/LateInitObservable.kt b/src/kernel/common/kotlin/net/terramodulus/util/LateInitObservable.kt new file mode 100644 index 00000000..9f8dd70d --- /dev/null +++ b/src/kernel/common/kotlin/net/terramodulus/util/LateInitObservable.kt @@ -0,0 +1,27 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.util + +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +class LateInitObservable( + private val onChange: (prop: KProperty<*>, old: T?, new: T) -> Unit +) : ReadWriteProperty { + private var value: T? = null + + override fun getValue(thisRef: Any?, property: KProperty<*>): T { + return value ?: throw IllegalStateException("Property ${property.name} is not initialized.") + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { + val oldValue = this.value + this.value = value + onChange(property, oldValue, value) + } +} + +fun lateInitObservable(onChange: (prop: KProperty<*>, old: T?, new: T) -> Unit) = LateInitObservable(onChange) diff --git a/src/kernel/common/kotlin/net/terramodulus/util/Math.kt b/src/kernel/common/kotlin/net/terramodulus/util/Math.kt new file mode 100644 index 00000000..fb7265b8 --- /dev/null +++ b/src/kernel/common/kotlin/net/terramodulus/util/Math.kt @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.util + +tailrec fun gcd(a: Int, b: Int): Int { + return if (b == 0) a else gcd(b, a % b) +} + +tailrec fun gcd(a: UInt, b: UInt): UInt { + return if (b == 0u) a else gcd(b, a % b) +} + +tailrec fun gcd(a: Long, b: Long): Long { + return if (b == 0L) a else gcd(b, a % b) +} + +tailrec fun gcd(a: ULong, b: ULong): ULong { + return if (b == 0uL) a else gcd(b, a % b) +} diff --git a/src/kernel/common/kotlin/net/terramodulus/util/TypedMap.kt b/src/kernel/common/kotlin/net/terramodulus/util/TypedMap.kt new file mode 100644 index 00000000..28a3e58a --- /dev/null +++ b/src/kernel/common/kotlin/net/terramodulus/util/TypedMap.kt @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.util + +import java.util.function.BiFunction +import java.util.function.Function + +/** + * A map with access by references to unique keys with type of values specified, + * matching the pattern of **Typed Anchor Dynamic Mapping**. + * Localized constrains may be applied by copying this universal structure.\ + * All bulk functions are all unsupported for type safety. + * @param T base class for map values + */ +open class TypedMap> + private constructor(private val map: MutableMap) : MutableMap by map { + constructor() : this(HashMap()) + + open class Simple : TypedMap>() + + /** + * A unique, immutable key that defines and enforces the type for values of a [TypedMap]. + * By hiding public access to the map instance, constrains may be applied by subclassing this. + */ + open class Key(val type: Class) { + companion object { + inline operator fun invoke() = Key(T::class.java) + } + override fun hashCode() = type.hashCode() + override fun equals(other: Any?): Boolean { + if (other === null) return false + if (this === other) return true + if (other !is Key<*>) return false + if (javaClass != other.javaClass) return false + return type == other.type + } + } +} diff --git a/src/kernel/common/kotlin/net/terramodulus/util/exception/Core.kt b/src/kernel/common/kotlin/net/terramodulus/util/exception/Core.kt index d822c0b4..4f574518 100644 --- a/src/kernel/common/kotlin/net/terramodulus/util/exception/Core.kt +++ b/src/kernel/common/kotlin/net/terramodulus/util/exception/Core.kt @@ -14,6 +14,10 @@ import kotlin.reflect.KClass private val logger = logger {} +// TODO add functions that can throw errors in debug and throw warnings in production +// This is useful for like when a resource is closed twice, logic error but may likely run fine in production +// May also optionally add fallback lambda to be run in production to suppress such error + @OptIn(ExperimentalContracts::class) inline fun codeAssert(block: () -> R): R { contract { diff --git a/src/kernel/common/kotlin/net/terramodulus/void/World.kt b/src/kernel/common/kotlin/net/terramodulus/void/World.kt index f8fbea9a..13005b43 100644 --- a/src/kernel/common/kotlin/net/terramodulus/void/World.kt +++ b/src/kernel/common/kotlin/net/terramodulus/void/World.kt @@ -5,11 +5,13 @@ package net.terramodulus.void +import com.cout970.math.vec3.ImmVec3d +import com.cout970.math.vec3.Vec3d +import com.cout970.math.vec3.plus import net.terramodulus.engine.PhyBody import net.terramodulus.engine.PhyEnv import net.terramodulus.engine.PhyGeom import net.terramodulus.engine.PhyGeomBox -import net.terramodulus.engine.Vec3D import net.terramodulus.util.logging.logger import java.io.Closeable import kotlin.properties.Delegates @@ -21,11 +23,13 @@ import kotlin.time.TimeSource private val logger = logger {} -class World(commander: Ymir) : Closeable { +class World(commander: Ymir.Builder, progressBar: ProgressBar) : Closeable { private val env = PhyEnv() private val world = env.createWorld() + var timePerTick = Duration.ZERO + private set - var gravity: Vec3D by world::gravity + var gravity: Vec3d by world::gravity var frictionMode: FrictionMode by Delegates.observable(FrictionMode.Infinite) { _, _, new -> when (new) { FrictionMode.Zero -> world.setFriction(0.0) @@ -46,114 +50,103 @@ class World(commander: Ymir) : Closeable { // Floor at y=-100 val floor = world.createGeomPlane(doubleArrayOf(0.0, 1.0, 0.0, -100.0)) + interface ProgressBar { + fun addReadyListener(listener: () -> Unit) + + fun setProgress(progress: Double) + } + init { - gravity = Vec3D(0.0, -9.81, 0.0) + gravity = ImmVec3d(0.0, -9.81, 0.0) floor.setBits(1u, 1u.inv()) world.omitSpace(mainSpace) - // Spawn point - objects[ObjId.randomUnique(objects)] = commander.wrapCube(createCube(.0, .0, .0), .0, .0, .0) - // Main Character - objects[ObjId.randomUnique(objects)] = commander.wrapChar( - world.newBody(PhyBody.Mass.SphereTotal(1.0, .5)).apply { - addGeom(createGeomSphere(.5)) - pos = Vec3D(0.0, 1.0, 0.0) + val commander = commander.build(object : YmirAgent { + override fun genCube(commander: Ymir, pos: Vec3d) { + objects[ObjId.randomUnique(objects)] = + commander.wrapCube(createCube(pos.x, pos.y, pos.z), pos) } - ) - // Test Objects - randomCubes(commander).forEach { objects[ObjId.randomUnique(objects)] = it } - // Running in parallel + + override fun genChar(commander: Ymir, pos: Vec3d) { + objects[ObjId.randomUnique(objects)] = commander.wrapChar( + world.newBody(PhyBody.Mass.SphereTotal(1.0, .5)).apply { + addGeom(createGeomSphere(.5)) + this.pos = pos + }, + pos, + ) + } + }) Thread { - val timeSource = TimeSource.Monotonic - val interval = 1.seconds / 20 // 20 Hz - var lastMark = timeSource.markNow() - while(true) { - // uncalculated ticks are not accumulated at this stage, *skipped* instead - tick() - val now = timeSource.markNow() - // remaining time after elapsed time used to maintain stable interval - val rem = interval - (now - lastMark) - if (rem > Duration.ZERO) { // sleeps the remaining time only when it is positive - Thread.sleep(rem.inWholeMilliseconds) + var ready = false // intermediate state to prevent cross-thread processing by listener invocation + while (!ready) { + progressBar.addReadyListener { + ready = true } - // makes sure timing does not include slept time - lastMark = timeSource.markNow() + Thread.sleep(1) } + commander.generateWorld(progressBar) + // Running in parallel + Thread { + val timeSource = TimeSource.Monotonic + val interval = 1.seconds / 20 // 20 Hz + var lastMark = timeSource.markNow() + while(true) { + // uncalculated ticks are not accumulated at this stage, *skipped* instead + tick() + val now = timeSource.markNow() + // remaining time after elapsed time used to maintain stable interval + val rem = interval - (now - lastMark) + timePerTick = (now - lastMark) + if (rem > Duration.ZERO) { // sleeps the remaining time only when it is positive + Thread.sleep(rem.inWholeMilliseconds) + } + // makes sure timing does not include slept time + lastMark = timeSource.markNow() + } + }.start() }.start() } interface Ymir { - fun wrapCube(phyGeom: PhyGeom, x: Double, y: Double, z: Double): VoidGeom + fun wrapCube(phyGeom: PhyGeom, pos: Vec3d): VoidGeom + + fun wrapChar(phyBody: PhyBody, pos: Vec3d): VoidGeom + + /** + * Caveat: This is run in parallel, so code involving any graphic context should not be included here. + */ + fun generateWorld(progressBar: ProgressBar) - /** Always at (0, 1, 0) */ - fun wrapChar(phyBody: PhyBody): VoidGeom + interface Builder { + fun build(agent: YmirAgent): Ymir + } + } + + interface YmirAgent { + fun genCube(commander: Ymir, pos: Vec3d) + + fun genChar(commander: Ymir, pos: Vec3d) } /** A wrapper containing rendering context, with a geom of dimensions of 1mx1mx1m */ interface VoidGeom { fun render() - val pos: Vec3D + val pos: Vec3d + + val phyGeoms: Sequence } interface EnvVoidGeom : VoidGeom { val phyGeom: PhyGeom + override val phyGeoms: Sequence get() = sequenceOf(phyGeom) } interface PlayerVoidGeom : VoidGeom { val phyBody: PhyBody + override val phyGeoms: Sequence get() = phyBody.geoms.asSequence() } - // Source: https://en.wikipedia.org/wiki/Maze_generation_algorithm - private fun randomCubes(commander: Ymir): ArrayList { - val list = ArrayList() - var i = 0 - val total = 10 * 10 * 2 * 2 - val interval = 5.0 - val max = 5 * 5 * 5 // 125 for each set - val directions = arrayOf( - Vec3D(1.0, 0.0, 0.0), - Vec3D(-1.0, 0.0, 0.0), - Vec3D(0.0, 1.0, 0.0), - Vec3D(0.0, -1.0, 0.0), - Vec3D(0.0, 0.0, 1.0), - Vec3D(0.0, 0.0, -1.0), - ) - for (x in 1..10) { - for (z in 1..10) { - for (xs in booleanArrayOf(false, true)) { - for (zs in booleanArrayOf(false, true)) { - logger.info { "Generating: ${++i}/$total" } - val xx = (if (xs) x else -x).toDouble() * interval - val zz = (if (zs) z else -z).toDouble() * interval - val origin = Vec3D(xx, Random.nextInt(-3..3).toDouble(), zz) - val visited = mutableSetOf(Vec3D(0.0, 0.0, 0.0)) - val heads = ArrayDeque() - heads.addLast(Vec3D(0.0, 0.0, 0.0)) - while (!heads.isEmpty()) { - val head = heads.removeFirst() - for (d in directions) { - val cur = head + d - if (Random.nextInt(max) > visited.size && cur !in visited) { - visited.add(cur) - if (Random.nextInt(max) > visited.size) { - heads.addLast(cur) - } - } - } - } - for (p in visited) { - val pt = origin + p - list.add(commander.wrapCube(createCube(pt.x, pt.y, pt.z), pt.x, pt.y, pt.z)) - } - } - } - } - } - return list - } - - private operator fun Vec3D.plus(other: Vec3D): Vec3D = Vec3D(x + other.x, y + other.y, z + other.z) - private fun createCube(x: Double, y: Double, z: Double): PhyGeomBox { val cube = createGeomBox(1.0, 1.0, 1.0) cube.setPosition(doubleArrayOf(x, y, z)) diff --git a/vector-math b/vector-math new file mode 160000 index 00000000..d25db430 --- /dev/null +++ b/vector-math @@ -0,0 +1 @@ +Subproject commit d25db430d52bb0ce28d485b1d073db1acc31a99a