mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-04 22:58:29 +08:00
Added minimal entity picking.
This commit is contained in:
parent
8de81c9cb4
commit
25f015dfbb
@ -6,6 +6,10 @@ import world.phantasmal.web.externals.babylon.Vector3
|
||||
operator fun Vector3.minus(other: Vector3): Vector3 =
|
||||
subtract(other)
|
||||
|
||||
operator fun Vector3.minusAssign(other: Vector3) {
|
||||
subtractInPlace(other)
|
||||
}
|
||||
|
||||
infix fun Vector3.dot(other: Vector3): Double =
|
||||
Vector3.Dot(this, other)
|
||||
|
||||
|
@ -8,20 +8,30 @@ import world.phantasmal.webui.DisposableContainer
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
abstract class Renderer(
|
||||
protected val canvas: HTMLCanvasElement,
|
||||
val canvas: HTMLCanvasElement,
|
||||
protected val engine: Engine,
|
||||
) : DisposableContainer() {
|
||||
val scene = Scene(engine)
|
||||
|
||||
private val light = HemisphericLight("Light", Vector3(-1.0, 1.0, 1.0), scene)
|
||||
private val light: HemisphericLight
|
||||
|
||||
protected abstract val camera: Camera
|
||||
|
||||
val scene = Scene(engine)
|
||||
|
||||
init {
|
||||
with(scene) {
|
||||
useRightHandedSystem = true
|
||||
clearColor = Color4(0.09, 0.09, 0.09, 1.0)
|
||||
}
|
||||
|
||||
light = HemisphericLight("Light", Vector3(-1.0, 1.0, 1.0), scene)
|
||||
}
|
||||
|
||||
override fun internalDispose() {
|
||||
camera.dispose()
|
||||
light.dispose()
|
||||
scene.dispose()
|
||||
engine.dispose()
|
||||
super.internalDispose()
|
||||
}
|
||||
|
||||
fun startRendering() {
|
||||
@ -34,14 +44,6 @@ abstract class Renderer(
|
||||
engine.stopRenderLoop()
|
||||
}
|
||||
|
||||
override fun internalDispose() {
|
||||
camera.dispose()
|
||||
light.dispose()
|
||||
scene.dispose()
|
||||
engine.dispose()
|
||||
super.internalDispose()
|
||||
}
|
||||
|
||||
private fun render() {
|
||||
val lightDirection = Vector3(-1.0, 1.0, 1.0)
|
||||
lightDirection.rotateByQuaternionToRef(camera.absoluteRotation, lightDirection)
|
||||
|
@ -12,6 +12,7 @@ external class Vector2(x: Double, y: Double) {
|
||||
var x: Double
|
||||
var y: Double
|
||||
|
||||
fun set(x: Double, y: Double): Vector2
|
||||
fun addInPlace(otherVector: Vector2): Vector2
|
||||
fun addInPlaceFromFloats(x: Double, y: Double): Vector2
|
||||
fun subtract(otherVector: Vector2): Vector2
|
||||
@ -32,10 +33,12 @@ external class Vector3(x: Double, y: Double, z: Double) {
|
||||
var y: Double
|
||||
var z: Double
|
||||
|
||||
fun set(x: Double, y: Double, z: Double): Vector2
|
||||
fun toQuaternion(): Quaternion
|
||||
fun addInPlace(otherVector: Vector3): Vector3
|
||||
fun addInPlaceFromFloats(x: Double, y: Double, z: Double): Vector3
|
||||
fun subtract(otherVector: Vector3): Vector3
|
||||
fun subtractInPlace(otherVector: Vector3): Vector3
|
||||
fun negate(): Vector3
|
||||
fun negateInPlace(): Vector3
|
||||
fun cross(other: Vector3): Vector3
|
||||
@ -148,9 +151,25 @@ external class Engine(
|
||||
antialias: Boolean = definedExternally,
|
||||
) : ThinEngine
|
||||
|
||||
external class Ray
|
||||
|
||||
external class PickingInfo {
|
||||
val bu: Double
|
||||
val bv: Double
|
||||
val distance: Double
|
||||
val faceId: Int
|
||||
val hit: Boolean
|
||||
val originMesh: AbstractMesh?
|
||||
val pickedMesh: AbstractMesh?
|
||||
val pickedPoint: Vector3?
|
||||
val ray: Ray?
|
||||
}
|
||||
|
||||
external class Scene(engine: Engine) {
|
||||
var useRightHandedSystem: Boolean
|
||||
var clearColor: Color4
|
||||
var pointerX: Double
|
||||
var pointerY: Double
|
||||
|
||||
fun render()
|
||||
fun addLight(light: Light)
|
||||
@ -159,6 +178,15 @@ external class Scene(engine: Engine) {
|
||||
fun removeLight(toRemove: Light)
|
||||
fun removeMesh(toRemove: TransformNode, recursive: Boolean? = definedExternally)
|
||||
fun removeTransformNode(toRemove: TransformNode)
|
||||
fun pick(
|
||||
x: Double,
|
||||
y: Double,
|
||||
predicate: (AbstractMesh) -> Boolean = definedExternally,
|
||||
fastCheck: Boolean = definedExternally,
|
||||
camera: Camera? = definedExternally,
|
||||
trianglePredicate: (p0: Vector3, p1: Vector3, p2: Vector3, ray: Ray) -> Boolean = definedExternally,
|
||||
): PickingInfo?
|
||||
|
||||
fun dispose()
|
||||
}
|
||||
|
||||
@ -238,9 +266,13 @@ open external class TransformNode(
|
||||
var rotationQuaternion: Quaternion?
|
||||
val absoluteRotation: Quaternion
|
||||
var scaling: Vector3
|
||||
|
||||
fun locallyTranslate(vector3: Vector3): TransformNode
|
||||
}
|
||||
|
||||
abstract external class AbstractMesh : TransformNode {
|
||||
var showBoundingBox: Boolean
|
||||
|
||||
fun getBoundingInfo(): BoundingInfo
|
||||
}
|
||||
|
||||
@ -253,6 +285,9 @@ external class Mesh(
|
||||
clonePhysicsImpostor: Boolean = definedExternally,
|
||||
) : AbstractMesh {
|
||||
fun createInstance(name: String): InstancedMesh
|
||||
fun bakeCurrentTransformIntoVertices(
|
||||
bakeIndependenlyOfChildren: Boolean = definedExternally,
|
||||
): Mesh
|
||||
}
|
||||
|
||||
external class InstancedMesh : AbstractMesh
|
||||
|
@ -14,6 +14,7 @@ import world.phantasmal.web.questEditor.loading.AreaAssetLoader
|
||||
import world.phantasmal.web.questEditor.loading.EntityAssetLoader
|
||||
import world.phantasmal.web.questEditor.loading.QuestLoader
|
||||
import world.phantasmal.web.questEditor.rendering.QuestEditorMeshManager
|
||||
import world.phantasmal.web.questEditor.rendering.EntityManipulator
|
||||
import world.phantasmal.web.questEditor.rendering.QuestRenderer
|
||||
import world.phantasmal.web.questEditor.stores.AreaStore
|
||||
import world.phantasmal.web.questEditor.stores.QuestEditorStore
|
||||
@ -48,13 +49,16 @@ class QuestEditor(
|
||||
val npcCountsController = addDisposable(NpcCountsController(questEditorStore))
|
||||
|
||||
// Rendering
|
||||
addDisposable(QuestEditorMeshManager(
|
||||
addDisposables(
|
||||
QuestEditorMeshManager(
|
||||
scope,
|
||||
questEditorStore,
|
||||
renderer,
|
||||
areaAssetLoader,
|
||||
entityAssetLoader
|
||||
))
|
||||
),
|
||||
EntityManipulator(questEditorStore, renderer)
|
||||
)
|
||||
|
||||
// Main Widget
|
||||
return QuestEditorWidget(
|
||||
|
@ -33,32 +33,33 @@ class EntityAssetLoader(
|
||||
MeshBuilder.CreateCylinder(
|
||||
"Entity",
|
||||
obj {
|
||||
diameter = 6.0
|
||||
height = 20.0
|
||||
diameter = 5.0
|
||||
height = 18.0
|
||||
},
|
||||
scene
|
||||
).apply {
|
||||
setEnabled(false)
|
||||
position = Vector3(0.0, 10.0, 0.0)
|
||||
locallyTranslate(Vector3(0.0, 10.0, 0.0))
|
||||
bakeCurrentTransformIntoVertices()
|
||||
}
|
||||
|
||||
private val meshCache =
|
||||
addDisposable(LoadingCache<Pair<EntityType, Int?>, Mesh> { it.dispose() })
|
||||
|
||||
override fun internalDispose() {
|
||||
defaultMesh.dispose()
|
||||
super.internalDispose()
|
||||
}
|
||||
|
||||
suspend fun loadMesh(type: EntityType, model: Int?): Mesh =
|
||||
meshCache.getOrPut(Pair(type, model)) {
|
||||
scope.async {
|
||||
try {
|
||||
loadGeometry(type, model)?.let { vertexData ->
|
||||
// TODO: Remove this check when XJ models are parsed.
|
||||
if (vertexData.indices == null || vertexData.indices!!.length == 0) {
|
||||
defaultMesh
|
||||
} else {
|
||||
val mesh = Mesh("${type.uniqueName}${model?.let { "-$it" }}", scene)
|
||||
mesh.setEnabled(false)
|
||||
vertexData.applyToMesh(mesh)
|
||||
mesh
|
||||
}
|
||||
} ?: defaultMesh
|
||||
} catch (e: Exception) {
|
||||
logger.error(e) { "Couldn't load mesh for $type (model: $model)." }
|
||||
|
@ -0,0 +1,219 @@
|
||||
package world.phantasmal.web.questEditor.rendering
|
||||
|
||||
import kotlinx.browser.document
|
||||
import mu.KotlinLogging
|
||||
import org.w3c.dom.events.Event
|
||||
import org.w3c.dom.pointerevents.PointerEvent
|
||||
import world.phantasmal.web.core.minusAssign
|
||||
import world.phantasmal.web.externals.babylon.AbstractMesh
|
||||
import world.phantasmal.web.externals.babylon.Vector2
|
||||
import world.phantasmal.web.externals.babylon.Vector3
|
||||
import world.phantasmal.web.questEditor.models.QuestEntityModel
|
||||
import world.phantasmal.web.questEditor.rendering.conversion.EntityMetadata
|
||||
import world.phantasmal.web.questEditor.stores.QuestEditorStore
|
||||
import world.phantasmal.webui.DisposableContainer
|
||||
import world.phantasmal.webui.dom.disposableListener
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
class EntityManipulator(
|
||||
private val questEditorStore: QuestEditorStore,
|
||||
private val renderer: QuestRenderer,
|
||||
) : DisposableContainer() {
|
||||
private val pointerPosition = Vector2.Zero()
|
||||
private val lastPointerPosition = Vector2.Zero()
|
||||
private var movedSinceLastPointerDown = false
|
||||
private var state: State
|
||||
|
||||
/**
|
||||
* Whether entity transformations, deletions, etc. are enabled or not.
|
||||
* Hover over and selection still work when this is set to false.
|
||||
*/
|
||||
var enabled: Boolean = true
|
||||
set(enabled) {
|
||||
field = enabled
|
||||
state.cancel()
|
||||
state = IdleState(questEditorStore, renderer, enabled)
|
||||
}
|
||||
|
||||
init {
|
||||
state = IdleState(questEditorStore, renderer, enabled)
|
||||
|
||||
observe(questEditorStore.selectedEntity, ::selectedEntityChanged)
|
||||
|
||||
addDisposables(
|
||||
disposableListener(renderer.canvas, "pointerdown", ::onPointerDown)
|
||||
)
|
||||
}
|
||||
|
||||
private fun selectedEntityChanged(entity: QuestEntityModel<*, *>?) {
|
||||
state.cancel()
|
||||
}
|
||||
|
||||
private fun onPointerDown(e: PointerEvent) {
|
||||
processPointerEvent(e)
|
||||
|
||||
state = state.processEvent(PointerDownEvt(
|
||||
e.buttons.toInt(),
|
||||
movedSinceLastPointerDown
|
||||
))
|
||||
|
||||
document.addEventListener("pointerup", ::onPointerUp)
|
||||
}
|
||||
|
||||
private fun onPointerUp(e: Event) {
|
||||
try {
|
||||
e as PointerEvent
|
||||
processPointerEvent(e)
|
||||
|
||||
state = state.processEvent(PointerUpEvt(
|
||||
e.buttons.toInt(),
|
||||
movedSinceLastPointerDown
|
||||
))
|
||||
} finally {
|
||||
document.removeEventListener("pointerup", ::onPointerUp)
|
||||
}
|
||||
}
|
||||
|
||||
private fun processPointerEvent(e: PointerEvent) {
|
||||
val rect = renderer.canvas.getBoundingClientRect()
|
||||
pointerPosition.set(e.clientX - rect.left, e.clientY - rect.top)
|
||||
|
||||
when (e.type) {
|
||||
"pointerdown" -> {
|
||||
movedSinceLastPointerDown = false
|
||||
}
|
||||
"pointermove", "pointerup" -> {
|
||||
if (!pointerPosition.equals(lastPointerPosition)) {
|
||||
movedSinceLastPointerDown = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lastPointerPosition.copyFrom(pointerPosition)
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class Evt
|
||||
|
||||
private sealed class PointerEvt : Evt() {
|
||||
abstract val buttons: Int
|
||||
abstract val movedSinceLastPointerDown: Boolean
|
||||
}
|
||||
|
||||
private class PointerDownEvt(
|
||||
override val buttons: Int,
|
||||
override val movedSinceLastPointerDown: Boolean,
|
||||
) : PointerEvt()
|
||||
|
||||
private class PointerUpEvt(
|
||||
override val buttons: Int,
|
||||
override val movedSinceLastPointerDown: Boolean,
|
||||
) : PointerEvt()
|
||||
|
||||
private class Pick(
|
||||
val entity: QuestEntityModel<*, *>,
|
||||
val mesh: AbstractMesh,
|
||||
|
||||
/**
|
||||
* Vector that points from the grabbing point (somewhere on the model's surface) to the model's
|
||||
* origin.
|
||||
*/
|
||||
val grabOffset: Vector3,
|
||||
|
||||
/**
|
||||
* Vector that points from the grabbing point to the terrain point directly under the model's
|
||||
* origin.
|
||||
*/
|
||||
// val dragAdjust: Vector3,
|
||||
)
|
||||
|
||||
private abstract class State {
|
||||
init {
|
||||
logger.trace { "Transitioning to ${this::class.simpleName}." }
|
||||
}
|
||||
|
||||
abstract fun processEvent(event: Evt): State
|
||||
|
||||
/**
|
||||
* The state object should stop doing what it's doing and revert to the idle state as soon as
|
||||
* possible.
|
||||
*/
|
||||
abstract fun cancel()
|
||||
}
|
||||
|
||||
private class IdleState(
|
||||
private val questEditorStore: QuestEditorStore,
|
||||
private val renderer: QuestRenderer,
|
||||
private val enabled: Boolean,
|
||||
) : State() {
|
||||
override fun processEvent(event: Evt): State =
|
||||
when (event) {
|
||||
is PointerDownEvt -> {
|
||||
pickEntity()?.let { pick ->
|
||||
when (event.buttons) {
|
||||
1 -> {
|
||||
questEditorStore.setSelectedEntity(pick.entity)
|
||||
|
||||
if (enabled) {
|
||||
// TODO: Enter TranslationState.
|
||||
}
|
||||
}
|
||||
2 -> {
|
||||
questEditorStore.setSelectedEntity(pick.entity)
|
||||
|
||||
if (enabled) {
|
||||
// TODO: Enter RotationState.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
is PointerUpEvt -> {
|
||||
// If the user clicks on nothing, deselect the currently selected entity.
|
||||
if (!event.movedSinceLastPointerDown && pickEntity() == null) {
|
||||
questEditorStore.setSelectedEntity(null)
|
||||
}
|
||||
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
override fun cancel() {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
private fun pickEntity(): Pick? {
|
||||
// Find the nearest object and NPC under the pointer.
|
||||
val pickInfo = renderer.scene.pick(renderer.scene.pointerX, renderer.scene.pointerY)
|
||||
if (pickInfo?.pickedMesh == null) return null
|
||||
|
||||
val entity = (pickInfo.pickedMesh.metadata as? EntityMetadata)?.entity
|
||||
?: return null
|
||||
val grabOffset = pickInfo.pickedMesh.position.clone()
|
||||
grabOffset -= pickInfo.pickedPoint!!
|
||||
|
||||
// TODO: dragAdjust.
|
||||
// val dragAdjust = grabOffset.clone()
|
||||
//
|
||||
// // Find vertical distance to the ground.
|
||||
// raycaster.set(intersection.object.position, DOWN_VECTOR)
|
||||
// val [collision_geom_intersection] = raycaster.intersectObjects(
|
||||
// this.renderer.collision_geometry.children,
|
||||
// true,
|
||||
// )
|
||||
//
|
||||
// if (collision_geom_intersection) {
|
||||
// dragAdjust.y -= collision_geom_intersection.distance
|
||||
// }
|
||||
|
||||
return Pick(
|
||||
entity,
|
||||
pickInfo.pickedMesh,
|
||||
grabOffset,
|
||||
)
|
||||
}
|
||||
}
|
@ -3,31 +3,52 @@ package world.phantasmal.web.questEditor.rendering
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import mu.KotlinLogging
|
||||
import world.phantasmal.core.disposable.Disposer
|
||||
import world.phantasmal.core.disposable.TrackedDisposable
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.web.externals.babylon.AbstractMesh
|
||||
import world.phantasmal.web.externals.babylon.TransformNode
|
||||
import world.phantasmal.web.questEditor.loading.EntityAssetLoader
|
||||
import world.phantasmal.web.questEditor.models.QuestEntityModel
|
||||
import world.phantasmal.web.questEditor.models.QuestNpcModel
|
||||
import world.phantasmal.web.questEditor.models.WaveModel
|
||||
import world.phantasmal.web.questEditor.rendering.conversion.EntityMetadata
|
||||
import world.phantasmal.web.questEditor.stores.QuestEditorStore
|
||||
import world.phantasmal.webui.DisposableContainer
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
private class LoadedEntity(val entity: QuestEntityModel<*, *>, val disposer: Disposer)
|
||||
|
||||
class EntityMeshManager(
|
||||
private val scope: CoroutineScope,
|
||||
private val selectedWave: Val<WaveModel?>,
|
||||
private val renderer: QuestRenderer,
|
||||
private val questEditorStore: QuestEditorStore,
|
||||
renderer: QuestRenderer,
|
||||
private val entityAssetLoader: EntityAssetLoader,
|
||||
) : TrackedDisposable() {
|
||||
) : DisposableContainer() {
|
||||
private val queue: MutableList<QuestEntityModel<*, *>> = mutableListOf()
|
||||
private val loadedEntities: MutableList<LoadedEntity> = mutableListOf()
|
||||
private val loadedEntities: MutableMap<QuestEntityModel<*, *>, LoadedEntity> = mutableMapOf()
|
||||
private var loading = false
|
||||
|
||||
private var entityMeshes = TransformNode("Entities", renderer.scene)
|
||||
private var hoveredMesh: AbstractMesh? = null
|
||||
private var selectedMesh: AbstractMesh? = null
|
||||
|
||||
init {
|
||||
observe(questEditorStore.selectedEntity) { entity ->
|
||||
if (entity == null) {
|
||||
unmarkSelected()
|
||||
} else {
|
||||
val loaded = loadedEntities[entity]
|
||||
|
||||
// Mesh might not be loaded yet.
|
||||
if (loaded == null) {
|
||||
unmarkSelected()
|
||||
} else {
|
||||
markSelected(loaded.mesh)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun internalDispose() {
|
||||
entityMeshes.dispose()
|
||||
removeAll()
|
||||
super.internalDispose()
|
||||
}
|
||||
@ -63,26 +84,38 @@ class EntityMeshManager(
|
||||
for (entity in entities) {
|
||||
queue.remove(entity)
|
||||
|
||||
val loadedIndex = loadedEntities.indexOfFirst { it.entity == entity }
|
||||
|
||||
if (loadedIndex != -1) {
|
||||
val loaded = loadedEntities.removeAt(loadedIndex)
|
||||
|
||||
renderer.removeEntityMesh(loaded.entity)
|
||||
loaded.disposer.dispose()
|
||||
}
|
||||
loadedEntities.remove(entity)?.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
fun removeAll() {
|
||||
for (loaded in loadedEntities) {
|
||||
loaded.disposer.dispose()
|
||||
for (loaded in loadedEntities.values) {
|
||||
loaded.dispose()
|
||||
}
|
||||
|
||||
loadedEntities.clear()
|
||||
queue.clear()
|
||||
}
|
||||
|
||||
private fun markSelected(entityMesh: AbstractMesh) {
|
||||
if (entityMesh == hoveredMesh) {
|
||||
hoveredMesh = null
|
||||
}
|
||||
|
||||
if (entityMesh != selectedMesh) {
|
||||
selectedMesh?.let { it.showBoundingBox = false }
|
||||
|
||||
entityMesh.showBoundingBox = true
|
||||
}
|
||||
|
||||
selectedMesh = entityMesh
|
||||
}
|
||||
|
||||
private fun unmarkSelected() {
|
||||
selectedMesh?.let { it.showBoundingBox = false }
|
||||
selectedMesh = null
|
||||
}
|
||||
|
||||
private suspend fun load(entity: QuestEntityModel<*, *>) {
|
||||
// TODO
|
||||
val mesh = entityAssetLoader.loadMesh(entity.type, model = null)
|
||||
@ -90,20 +123,30 @@ class EntityMeshManager(
|
||||
// Only add an instance of this mesh if the entity is still in the queue at this point.
|
||||
if (queue.remove(entity)) {
|
||||
val instance = mesh.createInstance(entity.type.uniqueName)
|
||||
instance.metadata = EntityMetadata(entity)
|
||||
instance.position = entity.worldPosition.value
|
||||
updateEntityMesh(entity, instance)
|
||||
instance.parent = entityMeshes
|
||||
|
||||
if (entity == questEditorStore.selectedEntity.value) {
|
||||
markSelected(instance)
|
||||
}
|
||||
|
||||
loadedEntities[entity] = LoadedEntity(entity, instance, questEditorStore.selectedWave)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateEntityMesh(entity: QuestEntityModel<*, *>, mesh: AbstractMesh) {
|
||||
renderer.addEntityMesh(mesh)
|
||||
private class LoadedEntity(
|
||||
entity: QuestEntityModel<*, *>,
|
||||
val mesh: AbstractMesh,
|
||||
selectedWave: Val<WaveModel?>,
|
||||
) : DisposableContainer() {
|
||||
init {
|
||||
mesh.metadata = EntityMetadata(entity)
|
||||
|
||||
val disposer = Disposer(
|
||||
entity.worldPosition.observe { (pos) ->
|
||||
observe(entity.worldPosition) { pos ->
|
||||
mesh.position = pos
|
||||
},
|
||||
}
|
||||
|
||||
addDisposables(
|
||||
// TODO: Rotation.
|
||||
// entity.worldRotation.observe { (value) ->
|
||||
// mesh.rotation.copy(value)
|
||||
@ -118,17 +161,21 @@ class EntityMeshManager(
|
||||
)
|
||||
|
||||
if (entity is QuestNpcModel) {
|
||||
disposer.add(
|
||||
addDisposable(
|
||||
selectedWave
|
||||
.map(entity.wave) { selectedWave, entityWave ->
|
||||
selectedWave == null || selectedWave == entityWave
|
||||
.map(entity.wave) { sWave, entityWave ->
|
||||
sWave == null || sWave == entityWave
|
||||
}
|
||||
.observe(callNow = true) { (visible) ->
|
||||
mesh.setEnabled(visible)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
loadedEntities.add(LoadedEntity(entity, disposer))
|
||||
override fun internalDispose() {
|
||||
mesh.parent = null
|
||||
mesh.dispose()
|
||||
super.internalDispose()
|
||||
}
|
||||
}
|
||||
|
@ -30,10 +30,10 @@ abstract class QuestMeshManager protected constructor(
|
||||
private val areaDisposer = disposer.add(Disposer())
|
||||
private val areaMeshManager = AreaMeshManager(areaAssetLoader)
|
||||
private val npcMeshManager = disposer.add(
|
||||
EntityMeshManager(scope, questEditorStore.selectedWave, renderer, entityAssetLoader)
|
||||
EntityMeshManager(scope, questEditorStore, renderer, entityAssetLoader)
|
||||
)
|
||||
private val objectMeshManager = disposer.add(
|
||||
EntityMeshManager(scope, questEditorStore.selectedWave, renderer, entityAssetLoader)
|
||||
EntityMeshManager(scope, questEditorStore, renderer, entityAssetLoader)
|
||||
)
|
||||
|
||||
private var loadJob: Job? = null
|
||||
@ -51,7 +51,6 @@ abstract class QuestMeshManager protected constructor(
|
||||
areaDisposer.disposeAll()
|
||||
npcMeshManager.removeAll()
|
||||
objectMeshManager.removeAll()
|
||||
renderer.resetEntityMeshes()
|
||||
|
||||
// Load entity meshes.
|
||||
areaDisposer.addAll(
|
||||
|
@ -2,15 +2,12 @@ package world.phantasmal.web.questEditor.rendering
|
||||
|
||||
import org.w3c.dom.HTMLCanvasElement
|
||||
import world.phantasmal.web.core.rendering.Renderer
|
||||
import world.phantasmal.web.externals.babylon.*
|
||||
import world.phantasmal.web.questEditor.models.QuestEntityModel
|
||||
import world.phantasmal.web.questEditor.rendering.conversion.EntityMetadata
|
||||
import world.phantasmal.web.externals.babylon.ArcRotateCamera
|
||||
import world.phantasmal.web.externals.babylon.Engine
|
||||
import world.phantasmal.web.externals.babylon.Vector3
|
||||
import kotlin.math.PI
|
||||
|
||||
class QuestRenderer(canvas: HTMLCanvasElement, engine: Engine) : Renderer(canvas, engine) {
|
||||
private var entityMeshes = TransformNode("Entities", scene)
|
||||
private val entityToMesh = mutableMapOf<QuestEntityModel<*, *>, AbstractMesh>()
|
||||
|
||||
override val camera = ArcRotateCamera("Camera", PI / 2, PI / 6, 500.0, Vector3.Zero(), scene)
|
||||
|
||||
init {
|
||||
@ -31,41 +28,4 @@ class QuestRenderer(canvas: HTMLCanvasElement, engine: Engine) : Renderer(canvas
|
||||
wheelDeltaPercentage = 0.1
|
||||
}
|
||||
}
|
||||
|
||||
override fun internalDispose() {
|
||||
entityMeshes.dispose()
|
||||
entityToMesh.clear()
|
||||
super.internalDispose()
|
||||
}
|
||||
|
||||
fun resetEntityMeshes() {
|
||||
entityMeshes.dispose(false)
|
||||
entityToMesh.clear()
|
||||
|
||||
entityMeshes = TransformNode("Entities", scene)
|
||||
}
|
||||
|
||||
fun addEntityMesh(mesh: AbstractMesh) {
|
||||
val entity = (mesh.metadata as EntityMetadata).entity
|
||||
mesh.parent = entityMeshes
|
||||
|
||||
entityToMesh[entity]?.let { prevMesh ->
|
||||
prevMesh.parent = null
|
||||
prevMesh.dispose()
|
||||
}
|
||||
|
||||
entityToMesh[entity] = mesh
|
||||
|
||||
// TODO: Mark selected entity.
|
||||
// if (entity === this.selected_entity) {
|
||||
// this.mark_selected(model)
|
||||
// }
|
||||
}
|
||||
|
||||
fun removeEntityMesh(entity: QuestEntityModel<*, *>) {
|
||||
entityToMesh.remove(entity)?.let { mesh ->
|
||||
mesh.parent = null
|
||||
mesh.dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.mutableVal
|
||||
import world.phantasmal.web.questEditor.models.AreaModel
|
||||
import world.phantasmal.web.questEditor.models.QuestEntityModel
|
||||
import world.phantasmal.web.questEditor.models.QuestModel
|
||||
import world.phantasmal.web.questEditor.models.WaveModel
|
||||
import world.phantasmal.webui.stores.Store
|
||||
@ -12,10 +13,12 @@ class QuestEditorStore(scope: CoroutineScope, private val areaStore: AreaStore)
|
||||
private val _currentQuest = mutableVal<QuestModel?>(null)
|
||||
private val _currentArea = mutableVal<AreaModel?>(null)
|
||||
private val _selectedWave = mutableVal<WaveModel?>(null)
|
||||
private val _selectedEntity = mutableVal<QuestEntityModel<*, *>?>(null)
|
||||
|
||||
val currentQuest: Val<QuestModel?> = _currentQuest
|
||||
val currentArea: Val<AreaModel?> = _currentArea
|
||||
val selectedWave: Val<WaveModel?> = _selectedWave
|
||||
val selectedEntity: Val<QuestEntityModel<*, *>?> = _selectedEntity
|
||||
|
||||
// TODO: Take into account whether we're debugging or not.
|
||||
val questEditingDisabled: Val<Boolean> = currentQuest.map { it == null }
|
||||
@ -28,4 +31,14 @@ class QuestEditorStore(scope: CoroutineScope, private val areaStore: AreaStore)
|
||||
_currentArea.value = areaStore.getArea(quest.episode, 0)
|
||||
}
|
||||
}
|
||||
|
||||
fun setSelectedEntity(entity: QuestEntityModel<*, *>?) {
|
||||
entity?.let {
|
||||
currentQuest.value?.let { quest ->
|
||||
_currentArea.value = areaStore.getArea(quest.episode, entity.areaId)
|
||||
}
|
||||
}
|
||||
|
||||
_selectedEntity.value = entity
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user