Added area mesh loading.

This commit is contained in:
Daan Vanden Bosch 2020-11-07 19:15:37 +01:00
parent 8ec75f8b4a
commit 8de81c9cb4
53 changed files with 820 additions and 233 deletions

View File

@ -0,0 +1,7 @@
package world.phantasmal.core
fun requireNonNegative(value: Int, name: String) {
require(value >= 0) {
"$name should be non-negative but was $value."
}
}

View File

@ -0,0 +1,103 @@
package world.phantasmal.lib.fileFormats.quest
class Area(
val id: Int,
val name: String,
val order: Int,
val areaVariants: List<AreaVariant>,
)
class AreaVariant(
val id: Int,
val area: Area,
)
fun getAreasForEpisode(episode: Episode): List<Area> =
AREAS.getValue(episode)
fun getAreaVariant(episode: Episode, areaId: Int, variantId: Int): AreaVariant? =
AREAS.getValue(episode).find { it.id == areaId }?.areaVariants?.getOrNull(variantId)
private val AREAS by lazy {
var order = 0
@Suppress("UNUSED_CHANGED_VALUE")
val ep1 = listOf(
createArea(0, "Pioneer II", order++, 1),
createArea(1, "Forest 1", order++, 1),
createArea(2, "Forest 2", order++, 1),
createArea(11, "Under the Dome", order++, 1),
createArea(3, "Cave 1", order++, 6),
createArea(4, "Cave 2", order++, 5),
createArea(5, "Cave 3", order++, 6),
createArea(12, "Underground Channel", order++, 1),
createArea(6, "Mine 1", order++, 6),
createArea(7, "Mine 2", order++, 6),
createArea(13, "Monitor Room", order++, 1),
createArea(8, "Ruins 1", order++, 5),
createArea(9, "Ruins 2", order++, 5),
createArea(10, "Ruins 3", order++, 5),
createArea(14, "Dark Falz", order++, 1),
// TODO:
// createArea(15, "BA Ruins", order++, 3),
// createArea(16, "BA Spaceship", order++, 3),
// createArea(17, "Lobby", order++, 15),
)
order = 0
@Suppress("UNUSED_CHANGED_VALUE")
val ep2 = listOf(
createArea(0, "Lab", order++, 1),
createArea(1, "VR Temple Alpha", order++, 3),
createArea(2, "VR Temple Beta", order++, 3),
createArea(14, "VR Temple Final", order++, 1),
createArea(3, "VR Spaceship Alpha", order++, 3),
createArea(4, "VR Spaceship Beta", order++, 3),
createArea(15, "VR Spaceship Final", order++, 1),
createArea(5, "Central Control Area", order++, 1),
createArea(6, "Jungle Area East", order++, 1),
createArea(7, "Jungle Area North", order++, 1),
createArea(8, "Mountain Area", order++, 3),
createArea(9, "Seaside Area", order++, 1),
createArea(12, "Cliffs of Gal Da Val", order++, 1),
createArea(10, "Seabed Upper Levels", order++, 3),
createArea(11, "Seabed Lower Levels", order++, 3),
createArea(13, "Test Subject Disposal Area", order++, 1),
createArea(16, "Seaside Area at Night", order++, 2),
createArea(17, "Control Tower", order++, 5),
)
order = 0
@Suppress("UNUSED_CHANGED_VALUE")
val ep4 = listOf(
createArea(0, "Pioneer II (Ep. IV)", order++, 1),
createArea(1, "Crater Route 1", order++, 1),
createArea(2, "Crater Route 2", order++, 1),
createArea(3, "Crater Route 3", order++, 1),
createArea(4, "Crater Route 4", order++, 1),
createArea(5, "Crater Interior", order++, 1),
createArea(6, "Subterranean Desert 1", order++, 3),
createArea(7, "Subterranean Desert 2", order++, 3),
createArea(8, "Subterranean Desert 3", order++, 3),
createArea(9, "Meteor Impact Site", order++, 1),
)
mapOf(
Episode.I to ep1,
Episode.II to ep2,
Episode.IV to ep4,
)
}
private fun createArea(id: Int, name: String, order: Int, variants: Int): Area {
val avs = mutableListOf<AreaVariant>()
val area = Area(id, name, order, avs)
for (avId in 0 until variants) {
avs.add(AreaVariant(avId, area))
}
return area
}

View File

@ -5,6 +5,8 @@ import world.phantasmal.lib.fileFormats.Vec3
interface QuestEntity<Type : EntityType> { interface QuestEntity<Type : EntityType> {
val type: Type val type: Type
var areaId: Int
/** /**
* Section-relative position. * Section-relative position.
*/ */

View File

@ -6,7 +6,11 @@ import world.phantasmal.lib.fileFormats.ninja.angleToRad
import world.phantasmal.lib.fileFormats.ninja.radToAngle import world.phantasmal.lib.fileFormats.ninja.radToAngle
import kotlin.math.roundToInt import kotlin.math.roundToInt
class QuestNpc(var episode: Episode, var areaId: Int, val data: Buffer) : QuestEntity<NpcType> { class QuestNpc(
var episode: Episode,
override var areaId: Int,
val data: Buffer,
) : QuestEntity<NpcType> {
var typeId: Short var typeId: Short
get() = data.getShort(0) get() = data.getShort(0)
set(value) { set(value) {

View File

@ -6,7 +6,7 @@ import world.phantasmal.lib.fileFormats.ninja.angleToRad
import world.phantasmal.lib.fileFormats.ninja.radToAngle import world.phantasmal.lib.fileFormats.ninja.radToAngle
import kotlin.math.roundToInt import kotlin.math.roundToInt
class QuestObject(var areaId: Int, val data: Buffer) : QuestEntity<ObjectType> { class QuestObject(override var areaId: Int, val data: Buffer) : QuestEntity<ObjectType> {
var typeId: Int var typeId: Int
get() = data.getInt(0) get() = data.getInt(0)
set(value) { set(value) {

View File

@ -11,6 +11,8 @@ interface MutableListVal<E> : ListVal<E>, MutableVal<List<E>> {
fun removeAt(index: Int): E fun removeAt(index: Int): E
fun replaceAll(elements: Iterable<E>)
fun replaceAll(elements: Sequence<E>) fun replaceAll(elements: Sequence<E>)
fun clear() fun clear()

View File

@ -73,6 +73,13 @@ class SimpleListVal<E>(
return removed return removed
} }
override fun replaceAll(elements: Iterable<E>) {
val removed = ArrayList(this.elements)
this.elements.clear()
this.elements.addAll(elements)
finalizeUpdate(ListValChangeEvent.Change(0, removed, this.elements))
}
override fun replaceAll(elements: Sequence<E>) { override fun replaceAll(elements: Sequence<E>) {
val removed = ArrayList(this.elements) val removed = ArrayList(this.elements)
this.elements.clear() this.elements.clear()

View File

@ -12,9 +12,9 @@ import world.phantasmal.web.application.controllers.NavigationController
import world.phantasmal.web.application.widgets.ApplicationWidget import world.phantasmal.web.application.widgets.ApplicationWidget
import world.phantasmal.web.application.widgets.MainContentWidget import world.phantasmal.web.application.widgets.MainContentWidget
import world.phantasmal.web.application.widgets.NavigationWidget import world.phantasmal.web.application.widgets.NavigationWidget
import world.phantasmal.web.core.PwTool
import world.phantasmal.web.core.loading.AssetLoader import world.phantasmal.web.core.loading.AssetLoader
import world.phantasmal.web.core.stores.ApplicationUrl import world.phantasmal.web.core.stores.ApplicationUrl
import world.phantasmal.web.core.stores.PwTool
import world.phantasmal.web.core.stores.UiStore import world.phantasmal.web.core.stores.UiStore
import world.phantasmal.web.externals.babylon.Engine import world.phantasmal.web.externals.babylon.Engine
import world.phantasmal.web.huntOptimizer.HuntOptimizer import world.phantasmal.web.huntOptimizer.HuntOptimizer
@ -47,6 +47,13 @@ class Application(
// Initialize core stores shared by several submodules. // Initialize core stores shared by several submodules.
val uiStore = addDisposable(UiStore(scope, applicationUrl)) val uiStore = addDisposable(UiStore(scope, applicationUrl))
// The various tools Phantasmal World consists of.
val tools: List<PwTool> = listOf(
Viewer(createEngine),
QuestEditor(assetLoader, createEngine),
HuntOptimizer(assetLoader, uiStore),
)
// Controllers. // Controllers.
val navigationController = addDisposable(NavigationController(uiStore)) val navigationController = addDisposable(NavigationController(uiStore))
val mainContentController = addDisposable(MainContentController(uiStore)) val mainContentController = addDisposable(MainContentController(uiStore))
@ -56,28 +63,11 @@ class Application(
ApplicationWidget( ApplicationWidget(
scope, scope,
NavigationWidget(scope, navigationController), NavigationWidget(scope, navigationController),
MainContentWidget(scope, mainContentController, mapOf( MainContentWidget(
PwTool.Viewer to { widgetScope -> scope,
addDisposable(Viewer( mainContentController,
widgetScope, tools.map { it.toolType to it::initialize }.toMap()
createEngine, )
)).createWidget()
},
PwTool.QuestEditor to { widgetScope ->
addDisposable(QuestEditor(
widgetScope,
assetLoader,
createEngine,
)).createWidget()
},
PwTool.HuntOptimizer to { widgetScope ->
addDisposable(HuntOptimizer(
widgetScope,
assetLoader,
uiStore,
)).createWidget()
},
))
) )
) )

View File

@ -1,10 +1,10 @@
package world.phantasmal.web.application.controllers package world.phantasmal.web.application.controllers
import world.phantasmal.observable.value.Val import world.phantasmal.observable.value.Val
import world.phantasmal.web.core.stores.PwTool import world.phantasmal.web.core.PwToolType
import world.phantasmal.web.core.stores.UiStore import world.phantasmal.web.core.stores.UiStore
import world.phantasmal.webui.controllers.Controller import world.phantasmal.webui.controllers.Controller
class MainContentController(uiStore: UiStore) : Controller() { class MainContentController(uiStore: UiStore) : Controller() {
val tools: Map<PwTool, Val<Boolean>> = uiStore.toolToActive val tools: Map<PwToolType, Val<Boolean>> = uiStore.toolToActive
} }

View File

@ -1,14 +1,14 @@
package world.phantasmal.web.application.controllers package world.phantasmal.web.application.controllers
import world.phantasmal.observable.value.Val import world.phantasmal.observable.value.Val
import world.phantasmal.web.core.stores.PwTool import world.phantasmal.web.core.PwToolType
import world.phantasmal.web.core.stores.UiStore import world.phantasmal.web.core.stores.UiStore
import world.phantasmal.webui.controllers.Controller import world.phantasmal.webui.controllers.Controller
class NavigationController(private val uiStore: UiStore) : Controller() { class NavigationController(private val uiStore: UiStore) : Controller() {
val tools: Map<PwTool, Val<Boolean>> = uiStore.toolToActive val tools: Map<PwToolType, Val<Boolean>> = uiStore.toolToActive
fun setCurrentTool(tool: PwTool) { fun setCurrentTool(tool: PwToolType) {
uiStore.setCurrentTool(tool) uiStore.setCurrentTool(tool)
} }
} }

View File

@ -4,7 +4,8 @@ import kotlinx.coroutines.CoroutineScope
import org.w3c.dom.Node import org.w3c.dom.Node
import world.phantasmal.observable.value.not import world.phantasmal.observable.value.not
import world.phantasmal.web.application.controllers.MainContentController import world.phantasmal.web.application.controllers.MainContentController
import world.phantasmal.web.core.stores.PwTool import world.phantasmal.web.core.PwTool
import world.phantasmal.web.core.PwToolType
import world.phantasmal.webui.dom.div import world.phantasmal.webui.dom.div
import world.phantasmal.webui.widgets.LazyLoader import world.phantasmal.webui.widgets.LazyLoader
import world.phantasmal.webui.widgets.Widget import world.phantasmal.webui.widgets.Widget
@ -12,7 +13,7 @@ import world.phantasmal.webui.widgets.Widget
class MainContentWidget( class MainContentWidget(
scope: CoroutineScope, scope: CoroutineScope,
private val ctrl: MainContentController, private val ctrl: MainContentController,
private val toolViews: Map<PwTool, (CoroutineScope) -> Widget>, private val toolViews: Map<PwToolType, (CoroutineScope) -> Widget>,
) : Widget(scope) { ) : Widget(scope) {
override fun Node.createElement() = override fun Node.createElement() =
div { div {

View File

@ -3,7 +3,7 @@ package world.phantasmal.web.application.widgets
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import org.w3c.dom.Node import org.w3c.dom.Node
import world.phantasmal.observable.Observable import world.phantasmal.observable.Observable
import world.phantasmal.web.core.stores.PwTool import world.phantasmal.web.core.PwToolType
import world.phantasmal.webui.dom.input import world.phantasmal.webui.dom.input
import world.phantasmal.webui.dom.label import world.phantasmal.webui.dom.label
import world.phantasmal.webui.dom.span import world.phantasmal.webui.dom.span
@ -11,7 +11,7 @@ import world.phantasmal.webui.widgets.Control
class PwToolButton( class PwToolButton(
scope: CoroutineScope, scope: CoroutineScope,
private val tool: PwTool, private val tool: PwToolType,
private val toggled: Observable<Boolean>, private val toggled: Observable<Boolean>,
private val mouseDown: () -> Unit, private val mouseDown: () -> Unit,
) : Control(scope) { ) : Control(scope) {

View File

@ -0,0 +1,30 @@
package world.phantasmal.web.core
import world.phantasmal.web.externals.babylon.Matrix
import world.phantasmal.web.externals.babylon.Vector3
operator fun Vector3.minus(other: Vector3): Vector3 =
subtract(other)
infix fun Vector3.dot(other: Vector3): Double =
Vector3.Dot(this, other)
infix fun Vector3.cross(other: Vector3): Vector3 =
cross(other)
operator fun Matrix.timesAssign(other: Matrix) {
other.preMultiply(this)
}
fun Matrix.preMultiply(other: Matrix) {
// Multiplies this by other.
multiplyToRef(other, this)
}
fun Matrix.multiply(v: Vector3) {
Vector3.TransformCoordinatesToRef(v, this, v)
}
fun Matrix.multiply3x3(v: Vector3) {
Vector3.TransformNormalToRef(v, this, v)
}

View File

@ -0,0 +1,18 @@
package world.phantasmal.web.core
import kotlinx.coroutines.CoroutineScope
import world.phantasmal.webui.widgets.Widget
/**
* Phantasmal World consists of several tools.
* An instance of PwTool should do as little work as possible in its constructor and defer any
* initialization work until [initialize] is called.
*/
interface PwTool {
val toolType: PwToolType
/**
* The caller of this method takes ownership of the returned widget.
*/
fun initialize(scope: CoroutineScope): Widget
}

View File

@ -0,0 +1,7 @@
package world.phantasmal.web.core
enum class PwToolType(val uiName: String, val slug: String) {
Viewer("Viewer", "viewer"),
QuestEditor("Quest Editor", "quest_editor"),
HuntOptimizer("Hunt Optimizer", "hunt_optimizer"),
}

View File

@ -1,6 +1,6 @@
package world.phantasmal.web.core.controllers package world.phantasmal.web.core.controllers
import world.phantasmal.web.core.stores.PwTool import world.phantasmal.web.core.PwToolType
import world.phantasmal.web.core.stores.UiStore import world.phantasmal.web.core.stores.UiStore
import world.phantasmal.webui.controllers.Tab import world.phantasmal.webui.controllers.Tab
import world.phantasmal.webui.controllers.TabController import world.phantasmal.webui.controllers.TabController
@ -9,7 +9,7 @@ open class PathAwareTab(override val title: String, val path: String) : Tab
open class PathAwareTabController<T : PathAwareTab>( open class PathAwareTabController<T : PathAwareTab>(
private val uiStore: UiStore, private val uiStore: UiStore,
private val tool: PwTool, private val tool: PwToolType,
tabs: List<T>, tabs: List<T>,
) : TabController<T>(tabs) { ) : TabController<T>(tabs) {
init { init {

View File

@ -11,12 +11,17 @@ abstract class Renderer(
protected val canvas: HTMLCanvasElement, protected val canvas: HTMLCanvasElement,
protected val engine: Engine, protected val engine: Engine,
) : DisposableContainer() { ) : DisposableContainer() {
protected val scene = Scene(engine) val scene = Scene(engine)
private val light = HemisphericLight("Light", Vector3(-1.0, 1.0, 0.0), scene)
private val light = HemisphericLight("Light", Vector3(-1.0, 1.0, 1.0), scene)
protected abstract val camera: Camera protected abstract val camera: Camera
init { init {
scene.clearColor = Color4(0.09, 0.09, 0.09, 1.0) with(scene) {
useRightHandedSystem = true
clearColor = Color4(0.09, 0.09, 0.09, 1.0)
}
} }
fun startRendering() { fun startRendering() {
@ -38,7 +43,7 @@ abstract class Renderer(
} }
private fun render() { private fun render() {
val lightDirection = Vector3(-1.0, 1.0, 0.0) val lightDirection = Vector3(-1.0, 1.0, 1.0)
lightDirection.rotateByQuaternionToRef(camera.absoluteRotation, lightDirection) lightDirection.rotateByQuaternionToRef(camera.absoluteRotation, lightDirection)
light.direction = lightDirection light.direction = lightDirection
scene.render() scene.render()

View File

@ -6,6 +6,7 @@ import world.phantasmal.lib.fileFormats.ninja.NinjaModel
import world.phantasmal.lib.fileFormats.ninja.NinjaObject import world.phantasmal.lib.fileFormats.ninja.NinjaObject
import world.phantasmal.lib.fileFormats.ninja.NjModel import world.phantasmal.lib.fileFormats.ninja.NjModel
import world.phantasmal.lib.fileFormats.ninja.XjModel import world.phantasmal.lib.fileFormats.ninja.XjModel
import world.phantasmal.web.core.*
import world.phantasmal.web.externals.babylon.* import world.phantasmal.web.externals.babylon.*
import kotlin.math.cos import kotlin.math.cos
import kotlin.math.sin import kotlin.math.sin
@ -39,8 +40,7 @@ private class NinjaToVertexDataConverter(private val builder: VertexDataBuilder)
if (ef.noRotate) NO_ROTATION else eulerToQuat(obj.rotation, ef.zxyRotationOrder), if (ef.noRotate) NO_ROTATION else eulerToQuat(obj.rotation, ef.zxyRotationOrder),
if (ef.noTranslate) NO_TRANSLATION else vec3ToBabylon(obj.position), if (ef.noTranslate) NO_TRANSLATION else vec3ToBabylon(obj.position),
) )
matrix.preMultiply(parentMatrix)
matrix.multiplyToRef(parentMatrix, matrix)
if (!ef.hidden) { if (!ef.hidden) {
obj.model?.let { model -> obj.model?.let { model ->
@ -72,8 +72,8 @@ private class NinjaToVertexDataConverter(private val builder: VertexDataBuilder)
val position = vec3ToBabylon(vertex.position) val position = vec3ToBabylon(vertex.position)
val normal = vertex.normal?.let(::vec3ToBabylon) ?: Vector3.Up() val normal = vertex.normal?.let(::vec3ToBabylon) ?: Vector3.Up()
Vector3.TransformCoordinatesToRef(position, matrix, position) matrix.multiply(position)
Vector3.TransformNormalToRef(normal, normalMatrix, normal) normalMatrix.multiply3x3(normal)
Vertex( Vertex(
boneIndex, boneIndex,
@ -158,10 +158,10 @@ private class NinjaToVertexDataConverter(private val builder: VertexDataBuilder)
for (vertex in model.vertices) { for (vertex in model.vertices) {
val p = vec3ToBabylon(vertex.position) val p = vec3ToBabylon(vertex.position)
Vector3.TransformCoordinatesToRef(p, matrix, p) matrix.multiply(p)
val n = vertex.normal?.let(::vec3ToBabylon) ?: Vector3.Up() val n = vertex.normal?.let(::vec3ToBabylon) ?: Vector3.Up()
Vector3.TransformCoordinatesToRef(n, normalMatrix, n) normalMatrix.multiply3x3(n)
val uv = vertex.uv?.let(::vec2ToBabylon) ?: DEFAULT_UV val uv = vertex.uv?.let(::vec2ToBabylon) ?: DEFAULT_UV
@ -190,16 +190,16 @@ private class NinjaToVertexDataConverter(private val builder: VertexDataBuilder)
// Calculate a surface normal and reverse the vertex winding if at least 2 of the // Calculate a surface normal and reverse the vertex winding if at least 2 of the
// vertex normals point in the opposite direction. This hack fixes the winding for // vertex normals point in the opposite direction. This hack fixes the winding for
// most models. // most models.
val normal = pb.subtract(pa).cross(pc.subtract(pa)) val normal = (pb - pa) cross (pc - pa)
if (!clockwise) { if (!clockwise) {
normal.negateInPlace() normal.negateInPlace()
} }
val oppositeCount = val oppositeCount =
(if (Vector3.Dot(normal, na) < 0) 1 else 0) + (if (normal dot na < 0) 1 else 0) +
(if (Vector3.Dot(normal, nb) < 0) 1 else 0) + (if (normal dot nb < 0) 1 else 0) +
(if (Vector3.Dot(normal, nc) < 0) 1 else 0) (if (normal dot nc < 0) 1 else 0)
if (oppositeCount >= 2) { if (oppositeCount >= 2) {
clockwise = !clockwise clockwise = !clockwise

View File

@ -27,10 +27,10 @@ class VertexDataBuilder {
fun getNormal(index: Int): Vector3 = fun getNormal(index: Int): Vector3 =
normals[index] normals[index]
fun addVertex(position: Vector3, normal: Vector3, uv: Vector2) { fun addVertex(position: Vector3, normal: Vector3, uv: Vector2? = null) {
positions.add(position) positions.add(position)
normals.add(normal) normals.add(normal)
uvs.add(uv) uv?.let { uvs.add(uv) }
} }
fun addIndex(index: Int) { fun addIndex(index: Int) {
@ -55,11 +55,11 @@ class VertexDataBuilder {
fun build(): VertexData { fun build(): VertexData {
check(this.positions.size == this.normals.size) check(this.positions.size == this.normals.size)
check(this.positions.size == this.uvs.size) check(this.uvs.isEmpty() || this.positions.size == this.uvs.size)
val positions = Float32Array(3 * positions.size) val positions = Float32Array(3 * positions.size)
val normals = Float32Array(3 * normals.size) val normals = Float32Array(3 * normals.size)
val uvs = Float32Array(2 * uvs.size) val uvs = if (uvs.isEmpty()) null else Float32Array(2 * uvs.size)
for (i in this.positions.indices) { for (i in this.positions.indices) {
val pos = this.positions[i] val pos = this.positions[i]
@ -72,9 +72,11 @@ class VertexDataBuilder {
normals[3 * i + 1] = normal.y.toFloat() normals[3 * i + 1] = normal.y.toFloat()
normals[3 * i + 2] = normal.z.toFloat() normals[3 * i + 2] = normal.z.toFloat()
val uv = this.uvs[i] uvs?.let {
uvs[2 * i] = uv.x.toFloat() val uv = this.uvs[i]
uvs[2 * i + 1] = uv.y.toFloat() uvs[2 * i] = uv.x.toFloat()
uvs[2 * i + 1] = uv.y.toFloat()
}
} }
val data = VertexData() val data = VertexData()

View File

@ -6,19 +6,11 @@ import org.w3c.dom.events.KeyboardEvent
import world.phantasmal.observable.value.MutableVal import world.phantasmal.observable.value.MutableVal
import world.phantasmal.observable.value.Val import world.phantasmal.observable.value.Val
import world.phantasmal.observable.value.mutableVal import world.phantasmal.observable.value.mutableVal
import world.phantasmal.web.core.PwToolType
import world.phantasmal.web.core.models.Server import world.phantasmal.web.core.models.Server
import world.phantasmal.webui.dom.disposableListener import world.phantasmal.webui.dom.disposableListener
import world.phantasmal.webui.stores.Store import world.phantasmal.webui.stores.Store
/**
* Phantasmal World consists of several tools.
*/
enum class PwTool(val uiName: String, val slug: String) {
Viewer("Viewer", "viewer"),
QuestEditor("Quest Editor", "quest_editor"),
HuntOptimizer("Hunt Optimizer", "hunt_optimizer"),
}
interface ApplicationUrl { interface ApplicationUrl {
val url: Val<String> val url: Val<String>
@ -27,8 +19,11 @@ interface ApplicationUrl {
fun replaceUrl(url: String) fun replaceUrl(url: String)
} }
class UiStore(scope: CoroutineScope, private val applicationUrl: ApplicationUrl) : Store(scope) { class UiStore(
private val _currentTool: MutableVal<PwTool> scope: CoroutineScope,
private val applicationUrl: ApplicationUrl,
) : Store(scope) {
private val _currentTool: MutableVal<PwToolType>
private val _path = mutableVal("") private val _path = mutableVal("")
private val _server = mutableVal(Server.Ephinea) private val _server = mutableVal(Server.Ephinea)
@ -48,22 +43,22 @@ class UiStore(scope: CoroutineScope, private val applicationUrl: ApplicationUrl)
*/ */
private val features: MutableSet<String> = mutableSetOf() private val features: MutableSet<String> = mutableSetOf()
val tools: List<PwTool> = PwTool.values().toList() val tools: List<PwToolType> = PwToolType.values().toList()
/** /**
* The default tool that is loaded. * The default tool that is loaded.
*/ */
val defaultTool: PwTool = PwTool.Viewer val defaultTool: PwToolType = PwToolType.Viewer
/** /**
* The tool that is current visible. * The tool that is currently visible.
*/ */
val currentTool: Val<PwTool> val currentTool: Val<PwToolType>
/** /**
* Map of tools to a boolean Val that says whether they are the current tool or not. * Map of tools to a boolean Val that says whether they are the current tool or not.
*/ */
val toolToActive: Map<PwTool, Val<Boolean>> val toolToActive: Map<PwToolType, Val<Boolean>>
/** /**
* Application URL without the tool path prefix. * Application URL without the tool path prefix.
@ -92,7 +87,7 @@ class UiStore(scope: CoroutineScope, private val applicationUrl: ApplicationUrl)
observe(applicationUrl.url) { setDataFromUrl(it) } observe(applicationUrl.url) { setDataFromUrl(it) }
} }
fun setCurrentTool(tool: PwTool) { fun setCurrentTool(tool: PwToolType) {
if (tool != currentTool.value) { if (tool != currentTool.value) {
updateApplicationUrl(tool, path = "", replace = false) updateApplicationUrl(tool, path = "", replace = false)
setCurrentTool(tool, path = "") setCurrentTool(tool, path = "")
@ -150,12 +145,12 @@ class UiStore(scope: CoroutineScope, private val applicationUrl: ApplicationUrl)
} }
} }
private fun setCurrentTool(tool: PwTool, path: String) { private fun setCurrentTool(tool: PwToolType, path: String) {
_path.value = path _path.value = path
_currentTool.value = tool _currentTool.value = tool
} }
private fun updateApplicationUrl(tool: PwTool, path: String, replace: Boolean) { private fun updateApplicationUrl(tool: PwToolType, path: String, replace: Boolean) {
val fullPath = "/${tool.slug}${path}" val fullPath = "/${tool.slug}${path}"
val params: MutableMap<String, String> = val params: MutableMap<String, String> =
parameters[fullPath]?.let { HashMap(it) } ?: mutableMapOf() parameters[fullPath]?.let { HashMap(it) } ?: mutableMapOf()
@ -195,12 +190,12 @@ class UiStore(scope: CoroutineScope, private val applicationUrl: ApplicationUrl)
} }
} }
private fun handlerKey(tool: PwTool, binding: String): String { private fun handlerKey(tool: PwToolType, binding: String): String {
return "$tool -> $binding" return "$tool -> $binding"
} }
companion object { companion object {
private val SLUG_TO_PW_TOOL: Map<String, PwTool> = private val SLUG_TO_PW_TOOL: Map<String, PwToolType> =
PwTool.values().map { it.slug to it }.toMap() PwToolType.values().map { it.slug to it }.toMap()
} }
} }

View File

@ -4,35 +4,35 @@ import kotlinx.coroutines.CoroutineScope
import org.w3c.dom.HTMLCanvasElement import org.w3c.dom.HTMLCanvasElement
import org.w3c.dom.Node import org.w3c.dom.Node
import world.phantasmal.web.core.rendering.Renderer import world.phantasmal.web.core.rendering.Renderer
import world.phantasmal.webui.dom.canvas import world.phantasmal.webui.dom.div
import world.phantasmal.webui.widgets.Widget import world.phantasmal.webui.widgets.Widget
import kotlin.math.floor import kotlin.math.floor
class RendererWidget( class RendererWidget(
scope: CoroutineScope, scope: CoroutineScope,
private val createRenderer: (HTMLCanvasElement) -> Renderer, private val canvas: HTMLCanvasElement,
private val renderer: Renderer,
) : Widget(scope) { ) : Widget(scope) {
private var renderer: Renderer? = null
override fun Node.createElement() = override fun Node.createElement() =
canvas { div {
className = "pw-core-renderer" className = "pw-core-renderer"
tabIndex = -1 tabIndex = -1
observeResize() observeResize()
renderer = addDisposable(createRenderer(this))
observe(selfOrAncestorHidden) { hidden -> observe(selfOrAncestorHidden) { hidden ->
if (hidden) { if (hidden) {
renderer?.stopRendering() renderer.stopRendering()
} else { } else {
renderer?.startRendering() renderer.startRendering()
} }
} }
append(canvas)
} }
override fun resized(width: Double, height: Double) { override fun resized(width: Double, height: Double) {
val canvas = (element as HTMLCanvasElement)
canvas.width = floor(width).toInt() canvas.width = floor(width).toInt()
canvas.height = floor(height).toInt() canvas.height = floor(height).toInt()
} }

View File

@ -149,6 +149,7 @@ external class Engine(
) : ThinEngine ) : ThinEngine
external class Scene(engine: Engine) { external class Scene(engine: Engine) {
var useRightHandedSystem: Boolean
var clearColor: Color4 var clearColor: Color4
fun render() fun render()

View File

@ -1,6 +1,8 @@
package world.phantasmal.web.huntOptimizer package world.phantasmal.web.huntOptimizer
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import world.phantasmal.web.core.PwTool
import world.phantasmal.web.core.PwToolType
import world.phantasmal.web.core.loading.AssetLoader import world.phantasmal.web.core.loading.AssetLoader
import world.phantasmal.web.core.stores.UiStore import world.phantasmal.web.core.stores.UiStore
import world.phantasmal.web.huntOptimizer.controllers.HuntOptimizerController import world.phantasmal.web.huntOptimizer.controllers.HuntOptimizerController
@ -12,20 +14,24 @@ import world.phantasmal.webui.DisposableContainer
import world.phantasmal.webui.widgets.Widget import world.phantasmal.webui.widgets.Widget
class HuntOptimizer( class HuntOptimizer(
private val scope: CoroutineScope, private val assetLoader: AssetLoader,
assetLoader: AssetLoader, private val uiStore: UiStore,
uiStore: UiStore, ) : DisposableContainer(), PwTool {
) : DisposableContainer() { override val toolType = PwToolType.HuntOptimizer
private val huntMethodStore = addDisposable(HuntMethodStore(scope, uiStore, assetLoader))
private val huntOptimizerController = addDisposable(HuntOptimizerController(uiStore)) override fun initialize(scope: CoroutineScope): Widget {
private val methodsController = // Stores
addDisposable(MethodsController(uiStore, huntMethodStore)) val huntMethodStore = addDisposable(HuntMethodStore(scope, uiStore, assetLoader))
fun createWidget(): Widget = // Controllers
HuntOptimizerWidget( val huntOptimizerController = addDisposable(HuntOptimizerController(uiStore))
val methodsController = addDisposable(MethodsController(uiStore, huntMethodStore))
// Main Widget
return HuntOptimizerWidget(
scope, scope,
ctrl = huntOptimizerController, ctrl = huntOptimizerController,
createMethodsWidget = { scope -> MethodsWidget(scope, methodsController) } createMethodsWidget = { s -> MethodsWidget(s, methodsController) }
) )
}
} }

View File

@ -1,15 +1,15 @@
package world.phantasmal.web.huntOptimizer.controllers package world.phantasmal.web.huntOptimizer.controllers
import world.phantasmal.web.core.PwToolType
import world.phantasmal.web.core.controllers.PathAwareTab import world.phantasmal.web.core.controllers.PathAwareTab
import world.phantasmal.web.core.controllers.PathAwareTabController import world.phantasmal.web.core.controllers.PathAwareTabController
import world.phantasmal.web.core.stores.PwTool
import world.phantasmal.web.core.stores.UiStore import world.phantasmal.web.core.stores.UiStore
import world.phantasmal.web.huntOptimizer.HuntOptimizerUrls import world.phantasmal.web.huntOptimizer.HuntOptimizerUrls
class HuntOptimizerController(uiStore: UiStore) : class HuntOptimizerController(uiStore: UiStore) :
PathAwareTabController<PathAwareTab>( PathAwareTabController<PathAwareTab>(
uiStore, uiStore,
PwTool.HuntOptimizer, PwToolType.HuntOptimizer,
listOf( listOf(
PathAwareTab("Optimize", HuntOptimizerUrls.optimize), PathAwareTab("Optimize", HuntOptimizerUrls.optimize),
PathAwareTab("Methods", HuntOptimizerUrls.methods), PathAwareTab("Methods", HuntOptimizerUrls.methods),

View File

@ -4,9 +4,9 @@ import world.phantasmal.lib.fileFormats.quest.Episode
import world.phantasmal.observable.value.list.ListVal import world.phantasmal.observable.value.list.ListVal
import world.phantasmal.observable.value.list.MutableListVal import world.phantasmal.observable.value.list.MutableListVal
import world.phantasmal.observable.value.list.mutableListVal import world.phantasmal.observable.value.list.mutableListVal
import world.phantasmal.web.core.PwToolType
import world.phantasmal.web.core.controllers.PathAwareTab import world.phantasmal.web.core.controllers.PathAwareTab
import world.phantasmal.web.core.controllers.PathAwareTabController import world.phantasmal.web.core.controllers.PathAwareTabController
import world.phantasmal.web.core.stores.PwTool
import world.phantasmal.web.core.stores.UiStore import world.phantasmal.web.core.stores.UiStore
import world.phantasmal.web.huntOptimizer.HuntOptimizerUrls import world.phantasmal.web.huntOptimizer.HuntOptimizerUrls
import world.phantasmal.web.huntOptimizer.models.HuntMethodModel import world.phantasmal.web.huntOptimizer.models.HuntMethodModel
@ -19,7 +19,7 @@ class MethodsController(
huntMethodStore: HuntMethodStore, huntMethodStore: HuntMethodStore,
) : PathAwareTabController<MethodsTab>( ) : PathAwareTabController<MethodsTab>(
uiStore, uiStore,
PwTool.HuntOptimizer, PwToolType.HuntOptimizer,
listOf( listOf(
MethodsTab("Episode I", HuntOptimizerUrls.methodsEpisodeI, Episode.I), MethodsTab("Episode I", HuntOptimizerUrls.methodsEpisodeI, Episode.I),
MethodsTab("Episode II", HuntOptimizerUrls.methodsEpisodeII, Episode.II), MethodsTab("Episode II", HuntOptimizerUrls.methodsEpisodeII, Episode.II),

View File

@ -1,56 +1,68 @@
package world.phantasmal.web.questEditor package world.phantasmal.web.questEditor
import kotlinx.browser.document
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import org.w3c.dom.HTMLCanvasElement import org.w3c.dom.HTMLCanvasElement
import world.phantasmal.web.core.PwTool
import world.phantasmal.web.core.PwToolType
import world.phantasmal.web.core.loading.AssetLoader import world.phantasmal.web.core.loading.AssetLoader
import world.phantasmal.web.externals.babylon.Engine import world.phantasmal.web.externals.babylon.Engine
import world.phantasmal.web.questEditor.controllers.NpcCountsController import world.phantasmal.web.questEditor.controllers.NpcCountsController
import world.phantasmal.web.questEditor.controllers.QuestEditorToolbarController import world.phantasmal.web.questEditor.controllers.QuestEditorToolbarController
import world.phantasmal.web.questEditor.controllers.QuestInfoController import world.phantasmal.web.questEditor.controllers.QuestInfoController
import world.phantasmal.web.questEditor.loading.AreaAssetLoader
import world.phantasmal.web.questEditor.loading.EntityAssetLoader import world.phantasmal.web.questEditor.loading.EntityAssetLoader
import world.phantasmal.web.questEditor.loading.QuestLoader import world.phantasmal.web.questEditor.loading.QuestLoader
import world.phantasmal.web.questEditor.rendering.QuestEditorMeshManager import world.phantasmal.web.questEditor.rendering.QuestEditorMeshManager
import world.phantasmal.web.questEditor.rendering.QuestRenderer import world.phantasmal.web.questEditor.rendering.QuestRenderer
import world.phantasmal.web.questEditor.stores.AreaStore
import world.phantasmal.web.questEditor.stores.QuestEditorStore import world.phantasmal.web.questEditor.stores.QuestEditorStore
import world.phantasmal.web.questEditor.widgets.* import world.phantasmal.web.questEditor.widgets.*
import world.phantasmal.webui.DisposableContainer import world.phantasmal.webui.DisposableContainer
import world.phantasmal.webui.widgets.Widget import world.phantasmal.webui.widgets.Widget
class QuestEditor( class QuestEditor(
private val scope: CoroutineScope,
private val assetLoader: AssetLoader, private val assetLoader: AssetLoader,
private val createEngine: (HTMLCanvasElement) -> Engine, private val createEngine: (HTMLCanvasElement) -> Engine,
) : DisposableContainer() { ) : DisposableContainer(), PwTool {
// Asset Loaders override val toolType = PwToolType.QuestEditor
private val questLoader = addDisposable(QuestLoader(scope, assetLoader))
// Stores override fun initialize(scope: CoroutineScope): Widget {
private val questEditorStore = addDisposable(QuestEditorStore(scope)) // Renderer
val canvas = document.createElement("CANVAS") as HTMLCanvasElement
val renderer = addDisposable(QuestRenderer(canvas, createEngine(canvas)))
// Controllers // Asset Loaders
private val toolbarController = val questLoader = addDisposable(QuestLoader(scope, assetLoader))
addDisposable(QuestEditorToolbarController(questLoader, questEditorStore)) val areaAssetLoader = addDisposable(AreaAssetLoader(scope, assetLoader, renderer.scene))
private val questInfoController = addDisposable(QuestInfoController(questEditorStore)) val entityAssetLoader = addDisposable(EntityAssetLoader(scope, assetLoader, renderer.scene))
private val npcCountsController = addDisposable(NpcCountsController(questEditorStore))
fun createWidget(): Widget = // Stores
QuestEditorWidget( val areaStore = addDisposable(AreaStore(scope, areaAssetLoader))
val questEditorStore = addDisposable(QuestEditorStore(scope, areaStore))
// Controllers
val toolbarController =
addDisposable(QuestEditorToolbarController(questLoader, areaStore, questEditorStore))
val questInfoController = addDisposable(QuestInfoController(questEditorStore))
val npcCountsController = addDisposable(NpcCountsController(questEditorStore))
// Rendering
addDisposable(QuestEditorMeshManager(
scope,
questEditorStore,
renderer,
areaAssetLoader,
entityAssetLoader
))
// Main Widget
return QuestEditorWidget(
scope, scope,
QuestEditorToolbar(scope, toolbarController), QuestEditorToolbar(scope, toolbarController),
{ scope -> QuestInfoWidget(scope, questInfoController) }, { s -> QuestInfoWidget(s, questInfoController) },
{ scope -> NpcCountsWidget(scope, npcCountsController) }, { s -> NpcCountsWidget(s, npcCountsController) },
{ scope -> QuestEditorRendererWidget(scope, ::createQuestEditorRenderer) } { s -> QuestEditorRendererWidget(s, canvas, renderer) }
) )
}
private fun createQuestEditorRenderer(canvas: HTMLCanvasElement): QuestRenderer =
QuestRenderer(canvas, createEngine(canvas)) { renderer, scene ->
QuestEditorMeshManager(
scope,
questEditorStore.currentQuest,
questEditorStore.currentArea,
questEditorStore.selectedWave,
renderer,
EntityAssetLoader(scope, assetLoader, scene)
)
}
} }

View File

@ -12,6 +12,7 @@ import world.phantasmal.lib.fileFormats.quest.parseQstToQuest
import world.phantasmal.observable.value.Val import world.phantasmal.observable.value.Val
import world.phantasmal.observable.value.mutableVal import world.phantasmal.observable.value.mutableVal
import world.phantasmal.web.questEditor.loading.QuestLoader import world.phantasmal.web.questEditor.loading.QuestLoader
import world.phantasmal.web.questEditor.stores.AreaStore
import world.phantasmal.web.questEditor.stores.QuestEditorStore import world.phantasmal.web.questEditor.stores.QuestEditorStore
import world.phantasmal.web.questEditor.stores.convertQuestToModel import world.phantasmal.web.questEditor.stores.convertQuestToModel
import world.phantasmal.webui.controllers.Controller import world.phantasmal.webui.controllers.Controller
@ -21,6 +22,7 @@ private val logger = KotlinLogging.logger {}
class QuestEditorToolbarController( class QuestEditorToolbarController(
private val questLoader: QuestLoader, private val questLoader: QuestLoader,
private val areaStore: AreaStore,
private val questEditorStore: QuestEditorStore, private val questEditorStore: QuestEditorStore,
) : Controller() { ) : Controller() {
private val _resultDialogVisible = mutableVal(false) private val _resultDialogVisible = mutableVal(false)
@ -31,7 +33,7 @@ class QuestEditorToolbarController(
suspend fun createNewQuest(episode: Episode) { suspend fun createNewQuest(episode: Episode) {
questEditorStore.setCurrentQuest( questEditorStore.setCurrentQuest(
convertQuestToModel(questLoader.loadDefaultQuest(episode)) convertQuestToModel(questLoader.loadDefaultQuest(episode), areaStore::getVariant)
) )
} }
@ -80,7 +82,7 @@ class QuestEditorToolbarController(
} }
private fun setCurrentQuest(quest: Quest) { private fun setCurrentQuest(quest: Quest) {
questEditorStore.setCurrentQuest(convertQuestToModel(quest)) questEditorStore.setCurrentQuest(convertQuestToModel(quest, areaStore::getVariant))
} }
private fun setResult(result: PwResult<*>) { private fun setResult(result: PwResult<*>) {

View File

@ -0,0 +1,137 @@
package world.phantasmal.web.questEditor.loading
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async
import org.khronos.webgl.ArrayBuffer
import world.phantasmal.lib.Endianness
import world.phantasmal.lib.cursor.cursor
import world.phantasmal.lib.fileFormats.parseAreaCollisionGeometry
import world.phantasmal.lib.fileFormats.quest.Episode
import world.phantasmal.web.core.loading.AssetLoader
import world.phantasmal.web.externals.babylon.Scene
import world.phantasmal.web.externals.babylon.TransformNode
import world.phantasmal.web.questEditor.models.AreaVariantModel
import world.phantasmal.web.questEditor.rendering.conversion.areaCollisionGeometryToTransformNode
import world.phantasmal.webui.DisposableContainer
class AreaAssetLoader(
private val scope: CoroutineScope,
private val assetLoader: AssetLoader,
private val scene: Scene,
) : DisposableContainer() {
private val collisionObjectCache =
addDisposable(LoadingCache<Triple<Episode, Int, Int>, TransformNode> { it.dispose() })
suspend fun loadCollisionGeometry(
episode: Episode,
areaVariant: AreaVariantModel,
): TransformNode =
collisionObjectCache.getOrPut(Triple(episode, areaVariant.area.id, areaVariant.id)) {
scope.async {
val buffer = getAreaAsset(episode, areaVariant, AssetType.Collision)
val obj = parseAreaCollisionGeometry(buffer.cursor(Endianness.Little))
areaCollisionGeometryToTransformNode(scene, obj)
}
}.await()
private suspend fun getAreaAsset(
episode: Episode,
areaVariant: AreaVariantModel,
type: AssetType,
): ArrayBuffer {
val baseUrl = areaVersionToBaseUrl(episode, areaVariant)
val suffix = when (type) {
AssetType.Render -> "n.rel"
AssetType.Collision -> "c.rel"
}
return assetLoader.loadArrayBuffer(baseUrl + suffix)
}
enum class AssetType {
Render, Collision
}
}
private val AREA_BASE_NAMES: Map<Episode, List<Pair<String, Int>>> = mapOf(
Episode.I to listOf(
Pair("city00_00", 1),
Pair("forest01", 1),
Pair("forest02", 1),
Pair("cave01_", 6),
Pair("cave02_", 5),
Pair("cave03_", 6),
Pair("machine01_", 6),
Pair("machine02_", 6),
Pair("ancient01_", 5),
Pair("ancient02_", 5),
Pair("ancient03_", 5),
Pair("boss01", 1),
Pair("boss02", 1),
Pair("boss03", 1),
Pair("darkfalz00", 1),
),
Episode.II to listOf(
Pair("labo00_00", 1),
Pair("ruins01_", 3),
Pair("ruins02_", 3),
Pair("space01_", 3),
Pair("space02_", 3),
Pair("jungle01_00", 1),
Pair("jungle02_00", 1),
Pair("jungle03_00", 1),
Pair("jungle04_", 3),
Pair("jungle05_00", 1),
Pair("seabed01_", 3),
Pair("seabed02_", 3),
Pair("boss05", 1),
Pair("boss06", 1),
Pair("boss07", 1),
Pair("boss08", 1),
Pair("jungle06_00", 1),
Pair("jungle07_", 5),
),
Episode.IV to listOf(
Pair("city02_00", 1),
Pair("wilds01_00", 1),
Pair("wilds01_01", 1),
Pair("wilds01_02", 1),
Pair("wilds01_03", 1),
Pair("crater01_00", 1),
Pair("desert01_", 3),
Pair("desert02_", 3),
Pair("desert03_", 3),
Pair("boss09_00", 1),
)
)
private fun areaVersionToBaseUrl(episode: Episode, areaVariant: AreaVariantModel): String {
var areaId = areaVariant.area.id
var areaVariantId = areaVariant.id
// Exception for Seaside area at night, variant 1.
// Phantasmal World 4 and Lost heart breaker use this to have two tower maps.
if (areaId == 16 && areaVariantId == 1) {
areaId = 17
areaVariantId = 1
}
val episodeBaseNames = AREA_BASE_NAMES.getValue(episode)
require(areaId in episodeBaseNames.indices) {
"Unknown episode $episode area $areaId."
}
val (base_name, variants) = episodeBaseNames[areaId]
require(areaVariantId in 0 until variants) {
"Unknown variant $areaVariantId of area $areaId in episode $episode."
}
val variant = if (variants == 1) {
""
} else {
areaVariantId.toString().padStart(2, '0')
}
return "/maps/map_${base_name}${variant}"
}

View File

@ -29,18 +29,24 @@ class EntityAssetLoader(
private val assetLoader: AssetLoader, private val assetLoader: AssetLoader,
private val scene: Scene, private val scene: Scene,
) : DisposableContainer() { ) : DisposableContainer() {
private val defaultMesh = MeshBuilder.CreateCylinder("Entity", obj { private val defaultMesh =
diameter = 6.0 MeshBuilder.CreateCylinder(
height = 20.0 "Entity",
}, scene).apply { obj {
setEnabled(false) diameter = 6.0
position = Vector3(0.0, 10.0, 0.0) height = 20.0
} },
scene
).apply {
setEnabled(false)
position = Vector3(0.0, 10.0, 0.0)
}
private val meshCache = addDisposable(LoadingCache<Pair<EntityType, Int?>, Mesh>()) private val meshCache =
addDisposable(LoadingCache<Pair<EntityType, Int?>, Mesh> { it.dispose() })
suspend fun loadMesh(type: EntityType, model: Int?): Mesh { suspend fun loadMesh(type: EntityType, model: Int?): Mesh =
return meshCache.getOrPut(Pair(type, model)) { meshCache.getOrPut(Pair(type, model)) {
scope.async { scope.async {
try { try {
loadGeometry(type, model)?.let { vertexData -> loadGeometry(type, model)?.let { vertexData ->
@ -60,7 +66,6 @@ class EntityAssetLoader(
} }
} }
}.await() }.await()
}
private suspend fun loadGeometry(type: EntityType, model: Int?): VertexData? { private suspend fun loadGeometry(type: EntityType, model: Int?): VertexData? {
val geomFormat = entityTypeToGeometryFormat(type) val geomFormat = entityTypeToGeometryFormat(type)

View File

@ -1,20 +1,30 @@
package world.phantasmal.web.questEditor.loading package world.phantasmal.web.questEditor.loading
import kotlinx.coroutines.Deferred import kotlinx.coroutines.Deferred
import kotlinx.coroutines.ExperimentalCoroutinesApi
import world.phantasmal.core.disposable.TrackedDisposable import world.phantasmal.core.disposable.TrackedDisposable
class LoadingCache<K, V> : TrackedDisposable() { class LoadingCache<K, V>(private val disposeValue: (V) -> Unit) : TrackedDisposable() {
private val map = mutableMapOf<K, Deferred<V>>() private val map = mutableMapOf<K, Deferred<V>>()
operator fun set(key: K, value: Deferred<V>) { operator fun set(key: K, value: Deferred<V>) {
map[key] = value map[key] = value
} }
@Suppress("DeferredIsResult")
fun getOrPut(key: K, defaultValue: () -> Deferred<V>): Deferred<V> = fun getOrPut(key: K, defaultValue: () -> Deferred<V>): Deferred<V> =
map.getOrPut(key, defaultValue) map.getOrPut(key, defaultValue)
@OptIn(ExperimentalCoroutinesApi::class)
override fun internalDispose() { override fun internalDispose() {
map.values.forEach { it.cancel() } map.values.forEach {
if (it.isActive) {
it.cancel()
} else if (it.isCompleted) {
disposeValue(it.getCompleted())
}
}
super.internalDispose() super.internalDispose()
} }
} }

View File

@ -15,7 +15,7 @@ class QuestLoader(
private val scope: CoroutineScope, private val scope: CoroutineScope,
private val assetLoader: AssetLoader, private val assetLoader: AssetLoader,
) : TrackedDisposable() { ) : TrackedDisposable() {
private val cache = LoadingCache<String, ArrayBuffer>() private val cache = LoadingCache<String, ArrayBuffer> {}
override fun internalDispose() { override fun internalDispose() {
cache.dispose() cache.dispose()

View File

@ -1,3 +1,17 @@
package world.phantasmal.web.questEditor.models package world.phantasmal.web.questEditor.models
class AreaModel import world.phantasmal.core.requireNonNegative
class AreaModel(
/**
* Matches the PSO ID.
*/
val id: Int,
val name: String,
val order: Int,
val areaVariants: List<AreaVariantModel>,
) {
init {
requireNonNegative(id, "id")
}
}

View File

@ -1,3 +1,19 @@
package world.phantasmal.web.questEditor.models package world.phantasmal.web.questEditor.models
class AreaVariantModel import world.phantasmal.core.requireNonNegative
import world.phantasmal.observable.value.list.ListVal
import world.phantasmal.observable.value.list.mutableListVal
class AreaVariantModel(val id: Int, val area: AreaModel) {
private val _sections = mutableListVal<SectionModel>()
val sections: ListVal<SectionModel> = _sections
init {
requireNonNegative(id, "id")
}
fun setSections(sections: List<SectionModel>) {
_sections.replaceAll(sections)
}
}

View File

@ -15,6 +15,8 @@ abstract class QuestEntityModel<Type : EntityType, Entity : QuestEntity<Type>>(
val type: Type get() = entity.type val type: Type get() = entity.type
val areaId: Int get() = entity.areaId
/** /**
* Section-relative position * Section-relative position
*/ */

View File

@ -13,14 +13,17 @@ class QuestModel(
shortDescription: String, shortDescription: String,
longDescription: String, longDescription: String,
val episode: Episode, val episode: Episode,
mapDesignations: Map<Int, Int>,
npcs: MutableList<QuestNpcModel>, npcs: MutableList<QuestNpcModel>,
objects: MutableList<QuestObjectModel>, objects: MutableList<QuestObjectModel>,
getVariant: (Episode, areaId: Int, variantId: Int) -> AreaVariantModel?,
) { ) {
private val _id = mutableVal(0) private val _id = mutableVal(0)
private val _language = mutableVal(0) private val _language = mutableVal(0)
private val _name = mutableVal("") private val _name = mutableVal("")
private val _shortDescription = mutableVal("") private val _shortDescription = mutableVal("")
private val _longDescription = mutableVal("") private val _longDescription = mutableVal("")
private val _mapDesignations = mutableVal(mapDesignations)
private val _npcs = mutableListVal(npcs) private val _npcs = mutableListVal(npcs)
private val _objects = mutableListVal(objects) private val _objects = mutableListVal(objects)
@ -29,6 +32,18 @@ class QuestModel(
val name: Val<String> = _name val name: Val<String> = _name
val shortDescription: Val<String> = _shortDescription val shortDescription: Val<String> = _shortDescription
val longDescription: Val<String> = _longDescription val longDescription: Val<String> = _longDescription
val mapDesignations: Val<Map<Int, Int>> = _mapDesignations
/**
* Map of area IDs to entity counts.
*/
val entitiesPerArea: Val<Map<Int, Int>>
/**
* One variant per area.
*/
val areaVariants: Val<List<AreaVariantModel>>
val npcs: ListVal<QuestNpcModel> = _npcs val npcs: ListVal<QuestNpcModel> = _npcs
val objects: ListVal<QuestObjectModel> = _objects val objects: ListVal<QuestObjectModel> = _objects
@ -38,6 +53,39 @@ class QuestModel(
setName(name) setName(name)
setShortDescription(shortDescription) setShortDescription(shortDescription)
setLongDescription(longDescription) setLongDescription(longDescription)
entitiesPerArea = this.npcs.map(this.objects) { ns, os ->
val map = mutableMapOf<Int, Int>()
for (npc in ns) {
map[npc.areaId] = (map[npc.areaId] ?: 0) + 1
}
for (obj in os) {
map[obj.areaId] = (map[obj.areaId] ?: 0) + 1
}
map
}
areaVariants =
entitiesPerArea.map(this.mapDesignations) { entitiesPerArea, mds ->
val variants = mutableMapOf<Int, AreaVariantModel>()
for (areaId in entitiesPerArea.values) {
getVariant(episode, areaId, 0)?.let {
variants[areaId] = it
}
}
for ((areaId, variantId) in mds) {
getVariant(episode, areaId, variantId)?.let {
variants[areaId] = it
}
}
variants.values.toList()
}
} }
fun setId(id: Int): QuestModel { fun setId(id: Int): QuestModel {

View File

@ -0,0 +1,16 @@
package world.phantasmal.web.questEditor.models
import world.phantasmal.web.externals.babylon.Vector3
class SectionModel(
val id: Int,
val position: Vector3,
val rotation: Vector3,
val areaVariant: AreaVariantModel,
) {
init {
require(id >= -1) {
"id should be greater than or equal to -1 but was $id."
}
}
}

View File

@ -0,0 +1,31 @@
package world.phantasmal.web.questEditor.rendering
import mu.KotlinLogging
import world.phantasmal.lib.fileFormats.quest.Episode
import world.phantasmal.web.externals.babylon.TransformNode
import world.phantasmal.web.questEditor.loading.AreaAssetLoader
import world.phantasmal.web.questEditor.models.AreaVariantModel
private val logger = KotlinLogging.logger {}
class AreaMeshManager(private val areaAssetLoader: AreaAssetLoader) {
private var currentGeometry: TransformNode? = null
suspend fun load(episode: Episode?, areaVariant: AreaVariantModel?) {
currentGeometry?.setEnabled(false)
if (episode == null || areaVariant == null) {
return
}
try {
val geom = areaAssetLoader.loadCollisionGeometry(episode, areaVariant)
geom.setEnabled(true)
currentGeometry = geom
} catch (e: Exception) {
logger.error(e) {
"Couldn't load models for area ${areaVariant.area.id}, variant ${areaVariant.id}."
}
}
}
}

View File

@ -1,46 +1,50 @@
package world.phantasmal.web.questEditor.rendering package world.phantasmal.web.questEditor.rendering
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import world.phantasmal.observable.value.Val import world.phantasmal.lib.fileFormats.quest.Episode
import world.phantasmal.observable.value.list.ListVal import world.phantasmal.observable.value.list.ListVal
import world.phantasmal.observable.value.list.listVal import world.phantasmal.observable.value.list.listVal
import world.phantasmal.web.questEditor.loading.AreaAssetLoader
import world.phantasmal.web.questEditor.loading.EntityAssetLoader import world.phantasmal.web.questEditor.loading.EntityAssetLoader
import world.phantasmal.web.questEditor.models.* import world.phantasmal.web.questEditor.models.*
import world.phantasmal.web.questEditor.stores.QuestEditorStore
class QuestEditorMeshManager( class QuestEditorMeshManager(
scope: CoroutineScope, scope: CoroutineScope,
private val currentQuest: Val<QuestModel?>, questEditorStore: QuestEditorStore,
private val currentArea: Val<AreaModel?>,
selectedWave: Val<WaveModel?>,
renderer: QuestRenderer, renderer: QuestRenderer,
areaAssetLoader: AreaAssetLoader,
entityAssetLoader: EntityAssetLoader, entityAssetLoader: EntityAssetLoader,
) : QuestMeshManager(scope, selectedWave, renderer, entityAssetLoader) { ) : QuestMeshManager(scope, questEditorStore, renderer, areaAssetLoader, entityAssetLoader) {
init { init {
disposer.addAll( disposer.addAll(
currentQuest.observe { areaVariantChanged() }, questEditorStore.currentQuest.map(questEditorStore.currentArea, ::getAreaVariantDetails)
currentArea.observe { areaVariantChanged() }, .observe { (details) ->
loadMeshes(details.episode, details.areaVariant, details.npcs, details.objects)
},
) )
} }
override fun getAreaVariantDetails(): AreaVariantDetails { private fun getAreaVariantDetails(quest: QuestModel?, area: AreaModel?): AreaVariantDetails {
val quest = currentQuest.value quest?.let {
val area = currentArea.value val areaVariant = area?.let {
quest.areaVariants.value.find { it.area.id == area.id }
}
val areaVariant: AreaVariantModel? areaVariant?.let {
val npcs: ListVal<QuestNpcModel> val npcs = quest.npcs // TODO: Filter NPCs.
val objects: ListVal<QuestObjectModel> val objects = quest.objects // TODO: Filter objects.
return AreaVariantDetails(quest.episode, areaVariant, npcs, objects)
if (quest != null /*&& area != null*/) { }
// TODO: Set areaVariant.
areaVariant = null
npcs = quest.npcs // TODO: Filter NPCs.
objects = listVal() // TODO: Filter objects.
} else {
areaVariant = null
npcs = listVal()
objects = listVal()
} }
return AreaVariantDetails(quest?.episode, areaVariant, npcs, objects) return AreaVariantDetails(null, null, listVal(), listVal())
} }
private class AreaVariantDetails(
val episode: Episode?,
val areaVariant: AreaVariantModel?,
val npcs: ListVal<QuestNpcModel>,
val objects: ListVal<QuestObjectModel>,
)
} }

View File

@ -1,53 +1,64 @@
package world.phantasmal.web.questEditor.rendering package world.phantasmal.web.questEditor.rendering
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import world.phantasmal.core.disposable.Disposer import world.phantasmal.core.disposable.Disposer
import world.phantasmal.core.disposable.TrackedDisposable import world.phantasmal.core.disposable.TrackedDisposable
import world.phantasmal.lib.fileFormats.quest.Episode import world.phantasmal.lib.fileFormats.quest.Episode
import world.phantasmal.observable.value.Val
import world.phantasmal.observable.value.list.ListVal import world.phantasmal.observable.value.list.ListVal
import world.phantasmal.observable.value.list.ListValChangeEvent import world.phantasmal.observable.value.list.ListValChangeEvent
import world.phantasmal.web.questEditor.loading.AreaAssetLoader
import world.phantasmal.web.questEditor.loading.EntityAssetLoader import world.phantasmal.web.questEditor.loading.EntityAssetLoader
import world.phantasmal.web.questEditor.models.AreaVariantModel import world.phantasmal.web.questEditor.models.AreaVariantModel
import world.phantasmal.web.questEditor.models.QuestNpcModel import world.phantasmal.web.questEditor.models.QuestNpcModel
import world.phantasmal.web.questEditor.models.QuestObjectModel import world.phantasmal.web.questEditor.models.QuestObjectModel
import world.phantasmal.web.questEditor.models.WaveModel import world.phantasmal.web.questEditor.stores.QuestEditorStore
/** /**
* Loads the necessary area and entity 3D models into [QuestRenderer]. * Loads the necessary area and entity 3D models into [QuestRenderer].
*/ */
abstract class QuestMeshManager protected constructor( abstract class QuestMeshManager protected constructor(
scope: CoroutineScope, private val scope: CoroutineScope,
selectedWave: Val<WaveModel?>, questEditorStore: QuestEditorStore,
private val renderer: QuestRenderer, private val renderer: QuestRenderer,
areaAssetLoader: AreaAssetLoader,
entityAssetLoader: EntityAssetLoader, entityAssetLoader: EntityAssetLoader,
) : TrackedDisposable() { ) : TrackedDisposable() {
protected val disposer = Disposer() protected val disposer = Disposer()
private val areaDisposer = disposer.add(Disposer()) private val areaDisposer = disposer.add(Disposer())
private val areaMeshManager = AreaMeshManager(areaAssetLoader)
private val npcMeshManager = disposer.add( private val npcMeshManager = disposer.add(
EntityMeshManager(scope, selectedWave, renderer, entityAssetLoader) EntityMeshManager(scope, questEditorStore.selectedWave, renderer, entityAssetLoader)
) )
private val objectMeshManager = disposer.add( private val objectMeshManager = disposer.add(
EntityMeshManager(scope, selectedWave, renderer, entityAssetLoader) EntityMeshManager(scope, questEditorStore.selectedWave, renderer, entityAssetLoader)
) )
protected abstract fun getAreaVariantDetails(): AreaVariantDetails private var loadJob: Job? = null
protected fun areaVariantChanged() { protected fun loadMeshes(
val details = getAreaVariantDetails() episode: Episode?,
areaVariant: AreaVariantModel?,
npcs: ListVal<QuestNpcModel>,
objects: ListVal<QuestObjectModel>,
) {
loadJob?.cancel()
loadJob = scope.launch {
areaMeshManager.load(episode, areaVariant)
// TODO: Load area mesh. areaDisposer.disposeAll()
npcMeshManager.removeAll()
objectMeshManager.removeAll()
renderer.resetEntityMeshes()
areaDisposer.disposeAll() // Load entity meshes.
npcMeshManager.removeAll() areaDisposer.addAll(
renderer.resetEntityMeshes() npcs.observeList(callNow = true, ::npcsChanged),
objects.observeList(callNow = true, ::objectsChanged),
// Load entity meshes. )
areaDisposer.addAll( }
details.npcs.observeList(callNow = true, ::npcsChanged),
details.objects.observeList(callNow = true, ::objectsChanged),
)
} }
override fun internalDispose() { override fun internalDispose() {
@ -69,10 +80,3 @@ abstract class QuestMeshManager protected constructor(
} }
} }
} }
class AreaVariantDetails(
val episode: Episode?,
val areaVariant: AreaVariantModel?,
val npcs: ListVal<QuestNpcModel>,
val objects: ListVal<QuestObjectModel>,
)

View File

@ -7,16 +7,11 @@ import world.phantasmal.web.questEditor.models.QuestEntityModel
import world.phantasmal.web.questEditor.rendering.conversion.EntityMetadata import world.phantasmal.web.questEditor.rendering.conversion.EntityMetadata
import kotlin.math.PI import kotlin.math.PI
class QuestRenderer( class QuestRenderer(canvas: HTMLCanvasElement, engine: Engine) : Renderer(canvas, engine) {
canvas: HTMLCanvasElement,
engine: Engine,
createMeshManager: (QuestRenderer, Scene) -> QuestMeshManager,
) : Renderer(canvas, engine) {
private val meshManager = createMeshManager(this, scene)
private var entityMeshes = TransformNode("Entities", scene) private var entityMeshes = TransformNode("Entities", scene)
private val entityToMesh = mutableMapOf<QuestEntityModel<*, *>, AbstractMesh>() private val entityToMesh = mutableMapOf<QuestEntityModel<*, *>, AbstractMesh>()
override val camera = ArcRotateCamera("Camera", 0.0, PI / 6, 500.0, Vector3.Zero(), scene) override val camera = ArcRotateCamera("Camera", PI / 2, PI / 6, 500.0, Vector3.Zero(), scene)
init { init {
with(camera) { with(camera) {
@ -31,14 +26,13 @@ class QuestRenderer(
angularSensibilityY = 200.0 angularSensibilityY = 200.0
panningInertia = 0.0 panningInertia = 0.0
panningSensibility = 3.0 panningSensibility = 3.0
panningAxis = Vector3(1.0, 0.0, 1.0) panningAxis = Vector3(1.0, 0.0, -1.0)
pinchDeltaPercentage = 0.1 pinchDeltaPercentage = 0.1
wheelDeltaPercentage = 0.1 wheelDeltaPercentage = 0.1
} }
} }
override fun internalDispose() { override fun internalDispose() {
meshManager.dispose()
entityMeshes.dispose() entityMeshes.dispose()
entityToMesh.clear() entityToMesh.clear()
super.internalDispose() super.internalDispose()

View File

@ -0,0 +1,50 @@
package world.phantasmal.web.questEditor.rendering.conversion
import world.phantasmal.lib.fileFormats.CollisionObject
import world.phantasmal.web.core.rendering.conversion.VertexDataBuilder
import world.phantasmal.web.core.rendering.conversion.vec3ToBabylon
import world.phantasmal.web.externals.babylon.Mesh
import world.phantasmal.web.externals.babylon.Scene
import world.phantasmal.web.externals.babylon.TransformNode
fun areaCollisionGeometryToTransformNode(scene: Scene, obj: CollisionObject): TransformNode {
val node = TransformNode("", scene)
for (collisionMesh in obj.meshes) {
val builder = VertexDataBuilder()
for (triangle in collisionMesh.triangles) {
val isSectionTransition = (triangle.flags and 0b1000000) != 0
val isVegetation = (triangle.flags and 0b10000) != 0
val isGround = (triangle.flags and 0b1) != 0
val colorIndex = when {
isSectionTransition -> 3
isVegetation -> 2
isGround -> 1
else -> 0
}
// Filter out walls.
if (colorIndex != 0) {
val p1 = vec3ToBabylon(collisionMesh.vertices[triangle.index1])
val p2 = vec3ToBabylon(collisionMesh.vertices[triangle.index2])
val p3 = vec3ToBabylon(collisionMesh.vertices[triangle.index3])
val n = vec3ToBabylon(triangle.normal)
builder.addIndex(builder.vertexCount)
builder.addVertex(p1, n)
builder.addIndex(builder.vertexCount)
builder.addVertex(p3, n)
builder.addIndex(builder.vertexCount)
builder.addVertex(p2, n)
}
}
if (builder.vertexCount > 0) {
val mesh = Mesh("Collision Geometry", scene, parent = node)
builder.build().applyToMesh(mesh)
}
}
return node
}

View File

@ -0,0 +1,36 @@
package world.phantasmal.web.questEditor.stores
import kotlinx.coroutines.CoroutineScope
import world.phantasmal.lib.fileFormats.quest.Episode
import world.phantasmal.lib.fileFormats.quest.getAreasForEpisode
import world.phantasmal.web.questEditor.loading.AreaAssetLoader
import world.phantasmal.web.questEditor.models.AreaModel
import world.phantasmal.web.questEditor.models.AreaVariantModel
import world.phantasmal.webui.stores.Store
class AreaStore(scope: CoroutineScope, areaAssetLoader: AreaAssetLoader) : Store(scope) {
private val areas: Map<Episode, List<AreaModel>>
init {
areas = Episode.values()
.map { episode ->
episode to getAreasForEpisode(episode).map { area ->
val variants = mutableListOf<AreaVariantModel>()
val areaModel = AreaModel(area.id, area.name, area.order, variants)
area.areaVariants.forEach { variant ->
variants.add(AreaVariantModel(variant.id, areaModel))
}
areaModel
}
}
.toMap()
}
fun getArea(episode: Episode, areaId: Int): AreaModel? =
areas.getValue(episode).find { it.id == areaId }
fun getVariant(episode: Episode, areaId: Int, variantId: Int): AreaVariantModel? =
getArea(episode, areaId)?.areaVariants?.getOrNull(variantId)
}

View File

@ -1,11 +1,16 @@
package world.phantasmal.web.questEditor.stores package world.phantasmal.web.questEditor.stores
import world.phantasmal.lib.fileFormats.quest.Episode
import world.phantasmal.lib.fileFormats.quest.Quest import world.phantasmal.lib.fileFormats.quest.Quest
import world.phantasmal.web.questEditor.models.AreaVariantModel
import world.phantasmal.web.questEditor.models.QuestModel import world.phantasmal.web.questEditor.models.QuestModel
import world.phantasmal.web.questEditor.models.QuestNpcModel import world.phantasmal.web.questEditor.models.QuestNpcModel
import world.phantasmal.web.questEditor.models.QuestObjectModel import world.phantasmal.web.questEditor.models.QuestObjectModel
fun convertQuestToModel(quest: Quest): QuestModel { fun convertQuestToModel(
quest: Quest,
getVariant: (Episode, areaId: Int, variantId: Int) -> AreaVariantModel?,
): QuestModel {
return QuestModel( return QuestModel(
quest.id, quest.id,
quest.language, quest.language,
@ -13,8 +18,10 @@ fun convertQuestToModel(quest: Quest): QuestModel {
quest.shortDescription, quest.shortDescription,
quest.longDescription, quest.longDescription,
quest.episode, quest.episode,
quest.mapDesignations,
// TODO: Add WaveModel to QuestNpcModel // TODO: Add WaveModel to QuestNpcModel
quest.npcs.mapTo(mutableListOf()) { QuestNpcModel(it, null) }, quest.npcs.mapTo(mutableListOf()) { QuestNpcModel(it, null) },
quest.objects.mapTo(mutableListOf()) { QuestObjectModel(it) } quest.objects.mapTo(mutableListOf()) { QuestObjectModel(it) },
getVariant
) )
} }

View File

@ -8,7 +8,7 @@ import world.phantasmal.web.questEditor.models.QuestModel
import world.phantasmal.web.questEditor.models.WaveModel import world.phantasmal.web.questEditor.models.WaveModel
import world.phantasmal.webui.stores.Store import world.phantasmal.webui.stores.Store
class QuestEditorStore(scope: CoroutineScope) : Store(scope) { class QuestEditorStore(scope: CoroutineScope, private val areaStore: AreaStore) : Store(scope) {
private val _currentQuest = mutableVal<QuestModel?>(null) private val _currentQuest = mutableVal<QuestModel?>(null)
private val _currentArea = mutableVal<AreaModel?>(null) private val _currentArea = mutableVal<AreaModel?>(null)
private val _selectedWave = mutableVal<WaveModel?>(null) private val _selectedWave = mutableVal<WaveModel?>(null)
@ -21,6 +21,11 @@ class QuestEditorStore(scope: CoroutineScope) : Store(scope) {
val questEditingDisabled: Val<Boolean> = currentQuest.map { it == null } val questEditingDisabled: Val<Boolean> = currentQuest.map { it == null }
fun setCurrentQuest(quest: QuestModel?) { fun setCurrentQuest(quest: QuestModel?) {
_currentArea.value = null
_currentQuest.value = quest _currentQuest.value = quest
quest?.let {
_currentArea.value = areaStore.getArea(quest.episode, 0)
}
} }
} }

View File

@ -6,5 +6,6 @@ import world.phantasmal.web.questEditor.rendering.QuestRenderer
class QuestEditorRendererWidget( class QuestEditorRendererWidget(
scope: CoroutineScope, scope: CoroutineScope,
createRenderer: (HTMLCanvasElement) -> QuestRenderer, canvas: HTMLCanvasElement,
) : QuestRendererWidget(scope, createRenderer) renderer: QuestRenderer,
) : QuestRendererWidget(scope, canvas, renderer)

View File

@ -10,14 +10,15 @@ import world.phantasmal.webui.widgets.Widget
abstract class QuestRendererWidget( abstract class QuestRendererWidget(
scope: CoroutineScope, scope: CoroutineScope,
private val createRenderer: (HTMLCanvasElement) -> QuestRenderer, private val canvas: HTMLCanvasElement,
private val renderer: QuestRenderer,
) : Widget(scope) { ) : Widget(scope) {
override fun Node.createElement() = override fun Node.createElement() =
div { div {
className = "pw-quest-editor-quest-renderer" className = "pw-quest-editor-quest-renderer"
tabIndex = -1 tabIndex = -1
addChild(RendererWidget(scope, createRenderer)) addChild(RendererWidget(scope, canvas, renderer))
} }
companion object { companion object {

View File

@ -1,7 +1,10 @@
package world.phantasmal.web.viewer package world.phantasmal.web.viewer
import kotlinx.browser.document
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import org.w3c.dom.HTMLCanvasElement import org.w3c.dom.HTMLCanvasElement
import world.phantasmal.web.core.PwTool
import world.phantasmal.web.core.PwToolType
import world.phantasmal.web.externals.babylon.Engine import world.phantasmal.web.externals.babylon.Engine
import world.phantasmal.web.viewer.controller.ViewerToolbarController import world.phantasmal.web.viewer.controller.ViewerToolbarController
import world.phantasmal.web.viewer.rendering.MeshRenderer import world.phantasmal.web.viewer.rendering.MeshRenderer
@ -12,18 +15,22 @@ import world.phantasmal.webui.DisposableContainer
import world.phantasmal.webui.widgets.Widget import world.phantasmal.webui.widgets.Widget
class Viewer( class Viewer(
private val scope: CoroutineScope,
private val createEngine: (HTMLCanvasElement) -> Engine, private val createEngine: (HTMLCanvasElement) -> Engine,
) : DisposableContainer() { ) : DisposableContainer(), PwTool {
// Stores override val toolType = PwToolType.Viewer
private val viewerStore = addDisposable(ViewerStore(scope))
// Controllers override fun initialize(scope: CoroutineScope): Widget {
private val viewerToolbarController = addDisposable(ViewerToolbarController(viewerStore)) // Stores
val viewerStore = addDisposable(ViewerStore(scope))
fun createWidget(): Widget = // Controllers
ViewerWidget(scope, ViewerToolbar(scope, viewerToolbarController), ::createViewerRenderer) val viewerToolbarController = addDisposable(ViewerToolbarController(viewerStore))
private fun createViewerRenderer(canvas: HTMLCanvasElement): MeshRenderer = // Rendering
MeshRenderer(viewerStore, canvas, createEngine(canvas)) val canvas = document.createElement("CANVAS") as HTMLCanvasElement
val renderer = addDisposable(MeshRenderer(viewerStore, canvas, createEngine(canvas)))
// Main Widget
return ViewerWidget(scope, ViewerToolbar(scope, viewerToolbarController), canvas, renderer)
}
} }

View File

@ -4,7 +4,10 @@ import org.w3c.dom.HTMLCanvasElement
import world.phantasmal.lib.fileFormats.ninja.NinjaObject import world.phantasmal.lib.fileFormats.ninja.NinjaObject
import world.phantasmal.web.core.rendering.Renderer import world.phantasmal.web.core.rendering.Renderer
import world.phantasmal.web.core.rendering.conversion.ninjaObjectToVertexData import world.phantasmal.web.core.rendering.conversion.ninjaObjectToVertexData
import world.phantasmal.web.externals.babylon.* import world.phantasmal.web.externals.babylon.ArcRotateCamera
import world.phantasmal.web.externals.babylon.Engine
import world.phantasmal.web.externals.babylon.Mesh
import world.phantasmal.web.externals.babylon.Vector3
import world.phantasmal.web.viewer.store.ViewerStore import world.phantasmal.web.viewer.store.ViewerStore
import kotlin.math.PI import kotlin.math.PI
@ -15,7 +18,7 @@ class MeshRenderer(
) : Renderer(canvas, engine) { ) : Renderer(canvas, engine) {
private var mesh: Mesh? = null private var mesh: Mesh? = null
override val camera = ArcRotateCamera("Camera", 0.0, PI / 3, 70.0, Vector3.Zero(), scene) override val camera = ArcRotateCamera("Camera", PI / 2, PI / 3, 70.0, Vector3.Zero(), scene)
init { init {
with(camera) { with(camera) {
@ -29,7 +32,7 @@ class MeshRenderer(
angularSensibilityX = 200.0 angularSensibilityX = 200.0
angularSensibilityY = 200.0 angularSensibilityY = 200.0
panningInertia = 0.0 panningInertia = 0.0
panningSensibility = 20.0 panningSensibility = 10.0
panningAxis = Vector3(1.0, 1.0, 0.0) panningAxis = Vector3(1.0, 1.0, 0.0)
pinchDeltaPercentage = 0.1 pinchDeltaPercentage = 0.1
wheelDeltaPercentage = 0.1 wheelDeltaPercentage = 0.1

View File

@ -11,7 +11,8 @@ import world.phantasmal.webui.widgets.Widget
class ViewerWidget( class ViewerWidget(
scope: CoroutineScope, scope: CoroutineScope,
private val toolbar: Widget, private val toolbar: Widget,
private val createRenderer: (HTMLCanvasElement) -> Renderer, private val canvas: HTMLCanvasElement,
private val renderer: Renderer,
) : Widget(scope) { ) : Widget(scope) {
override fun Node.createElement() = override fun Node.createElement() =
div { div {
@ -21,7 +22,7 @@ class ViewerWidget(
div { div {
className = "pw-viewer-viewer-container" className = "pw-viewer-viewer-container"
addChild(RendererWidget(scope, createRenderer)) addChild(RendererWidget(scope, canvas, renderer))
} }
} }
@ -38,6 +39,7 @@ class ViewerWidget(
flex-grow: 1; flex-grow: 1;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
overflow: hidden;
} }
""".trimIndent()) """.trimIndent())
} }

View File

@ -10,7 +10,7 @@ import world.phantasmal.core.disposable.disposable
import world.phantasmal.core.disposable.use import world.phantasmal.core.disposable.use
import world.phantasmal.testUtils.TestSuite import world.phantasmal.testUtils.TestSuite
import world.phantasmal.web.core.loading.AssetLoader import world.phantasmal.web.core.loading.AssetLoader
import world.phantasmal.web.core.stores.PwTool import world.phantasmal.web.core.PwTool
import world.phantasmal.web.externals.babylon.Engine import world.phantasmal.web.externals.babylon.Engine
import world.phantasmal.web.test.TestApplicationUrl import world.phantasmal.web.test.TestApplicationUrl
import kotlin.test.Test import kotlin.test.Test

View File

@ -1,7 +1,7 @@
package world.phantasmal.web.core.controllers package world.phantasmal.web.core.controllers
import world.phantasmal.testUtils.TestSuite import world.phantasmal.testUtils.TestSuite
import world.phantasmal.web.core.stores.PwTool import world.phantasmal.web.core.PwTool
import world.phantasmal.web.core.stores.UiStore import world.phantasmal.web.core.stores.UiStore
import world.phantasmal.web.test.TestApplicationUrl import world.phantasmal.web.test.TestApplicationUrl
import kotlin.test.Test import kotlin.test.Test

View File

@ -1,7 +1,7 @@
package world.phantasmal.web.core.store package world.phantasmal.web.core.store
import world.phantasmal.testUtils.TestSuite import world.phantasmal.testUtils.TestSuite
import world.phantasmal.web.core.stores.PwTool import world.phantasmal.web.core.PwTool
import world.phantasmal.web.core.stores.UiStore import world.phantasmal.web.core.stores.UiStore
import world.phantasmal.web.test.TestApplicationUrl import world.phantasmal.web.test.TestApplicationUrl
import kotlin.test.Test import kotlin.test.Test

View File

@ -7,7 +7,7 @@ import kotlinx.coroutines.cancel
import world.phantasmal.core.disposable.disposable import world.phantasmal.core.disposable.disposable
import world.phantasmal.testUtils.TestSuite import world.phantasmal.testUtils.TestSuite
import world.phantasmal.web.core.loading.AssetLoader import world.phantasmal.web.core.loading.AssetLoader
import world.phantasmal.web.core.stores.PwTool import world.phantasmal.web.core.PwTool
import world.phantasmal.web.core.stores.UiStore import world.phantasmal.web.core.stores.UiStore
import world.phantasmal.web.test.TestApplicationUrl import world.phantasmal.web.test.TestApplicationUrl
import kotlin.test.Test import kotlin.test.Test