mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-04 22:58:29 +08:00
Improved camera handling.
This commit is contained in:
parent
132cdccd0a
commit
990a8c144f
@ -9,11 +9,11 @@ private val logger = KotlinLogging.logger {}
|
|||||||
|
|
||||||
abstract class Renderer(
|
abstract class Renderer(
|
||||||
val canvas: HTMLCanvasElement,
|
val canvas: HTMLCanvasElement,
|
||||||
protected val engine: Engine,
|
val engine: Engine,
|
||||||
) : DisposableContainer() {
|
) : DisposableContainer() {
|
||||||
private val light: HemisphericLight
|
private val light: HemisphericLight
|
||||||
|
|
||||||
protected abstract val camera: Camera
|
abstract val camera: Camera
|
||||||
|
|
||||||
val scene = Scene(engine)
|
val scene = Scene(engine)
|
||||||
|
|
||||||
@ -44,7 +44,7 @@ abstract class Renderer(
|
|||||||
engine.stopRenderLoop()
|
engine.stopRenderLoop()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun render() {
|
protected open fun render() {
|
||||||
val lightDirection = Vector3(-1.0, 1.0, 1.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
|
||||||
|
@ -50,6 +50,7 @@ external class Vector3(x: Double, y: Double, z: Double) {
|
|||||||
companion object {
|
companion object {
|
||||||
fun One(): Vector3
|
fun One(): Vector3
|
||||||
fun Up(): Vector3
|
fun Up(): Vector3
|
||||||
|
fun Down(): Vector3
|
||||||
fun Zero(): Vector3
|
fun Zero(): Vector3
|
||||||
fun Dot(left: Vector3, right: Vector3): Double
|
fun Dot(left: Vector3, right: Vector3): Double
|
||||||
fun TransformCoordinates(vector: Vector3, transformation: Matrix): Vector3
|
fun TransformCoordinates(vector: Vector3, transformation: Matrix): Vector3
|
||||||
@ -145,6 +146,8 @@ open external class ThinEngine {
|
|||||||
* be removed.
|
* be removed.
|
||||||
*/
|
*/
|
||||||
fun stopRenderLoop(renderFunction: () -> Unit = definedExternally)
|
fun stopRenderLoop(renderFunction: () -> Unit = definedExternally)
|
||||||
|
fun getRenderWidth(useScreen: Boolean = definedExternally): Double
|
||||||
|
fun getRenderHeight(useScreen: Boolean = definedExternally): Double
|
||||||
|
|
||||||
fun dispose()
|
fun dispose()
|
||||||
}
|
}
|
||||||
@ -154,7 +157,7 @@ external class Engine(
|
|||||||
antialias: Boolean = definedExternally,
|
antialias: Boolean = definedExternally,
|
||||||
) : ThinEngine
|
) : ThinEngine
|
||||||
|
|
||||||
external class Ray
|
external class Ray(origin: Vector3, direction: Vector3, length: Double = definedExternally)
|
||||||
|
|
||||||
external class PickingInfo {
|
external class PickingInfo {
|
||||||
val bu: Double
|
val bu: Double
|
||||||
@ -166,6 +169,13 @@ external class PickingInfo {
|
|||||||
val pickedMesh: AbstractMesh?
|
val pickedMesh: AbstractMesh?
|
||||||
val pickedPoint: Vector3?
|
val pickedPoint: Vector3?
|
||||||
val ray: Ray?
|
val ray: Ray?
|
||||||
|
|
||||||
|
fun getNormal(
|
||||||
|
useWorldCoordinates: Boolean = definedExternally,
|
||||||
|
useVerticesNormals: Boolean = definedExternally,
|
||||||
|
): Vector3?
|
||||||
|
|
||||||
|
fun getTextureCoordinates(): Vector2?
|
||||||
}
|
}
|
||||||
|
|
||||||
external class Scene(engine: Engine) {
|
external class Scene(engine: Engine) {
|
||||||
@ -190,6 +200,32 @@ external class Scene(engine: Engine) {
|
|||||||
trianglePredicate: (p0: Vector3, p1: Vector3, p2: Vector3, ray: Ray) -> Boolean = definedExternally,
|
trianglePredicate: (p0: Vector3, p1: Vector3, p2: Vector3, ray: Ray) -> Boolean = definedExternally,
|
||||||
): PickingInfo?
|
): PickingInfo?
|
||||||
|
|
||||||
|
fun pickWithRay(
|
||||||
|
ray: Ray,
|
||||||
|
predicate: (AbstractMesh) -> Boolean = definedExternally,
|
||||||
|
fastCheck: Boolean = definedExternally,
|
||||||
|
trianglePredicate: (p0: Vector3, p1: Vector3, p2: Vector3, ray: Ray) -> Boolean = definedExternally,
|
||||||
|
): PickingInfo?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param x X position on screen
|
||||||
|
* @param y Y position on screen
|
||||||
|
* @param predicate Predicate function used to determine eligible meshes. Can be set to null. In this case, a mesh must be enabled, visible and with isPickable set to true
|
||||||
|
*/
|
||||||
|
fun multiPick(
|
||||||
|
x: Double,
|
||||||
|
y: Double,
|
||||||
|
predicate: (AbstractMesh) -> Boolean = definedExternally,
|
||||||
|
camera: Camera = definedExternally,
|
||||||
|
trianglePredicate: (p0: Vector3, p1: Vector3, p2: Vector3, ray: Ray) -> Boolean = definedExternally,
|
||||||
|
): Array<PickingInfo>?
|
||||||
|
|
||||||
|
fun multiPickWithRay(
|
||||||
|
ray: Ray,
|
||||||
|
predicate: (AbstractMesh) -> Boolean = definedExternally,
|
||||||
|
trianglePredicate: (p0: Vector3, p1: Vector3, p2: Vector3, ray: Ray) -> Boolean = definedExternally,
|
||||||
|
): Array<PickingInfo>?
|
||||||
|
|
||||||
fun dispose()
|
fun dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -214,15 +250,21 @@ open external class Node {
|
|||||||
}
|
}
|
||||||
|
|
||||||
open external class Camera : Node {
|
open external class Camera : Node {
|
||||||
|
var minZ: Double
|
||||||
|
var maxZ: Double
|
||||||
val absoluteRotation: Quaternion
|
val absoluteRotation: Quaternion
|
||||||
val onProjectionMatrixChangedObservable: Observable<Camera>
|
val onProjectionMatrixChangedObservable: Observable<Camera>
|
||||||
val onViewMatrixChangedObservable: Observable<Camera>
|
val onViewMatrixChangedObservable: Observable<Camera>
|
||||||
val onAfterCheckInputsObservable: Observable<Camera>
|
val onAfterCheckInputsObservable: Observable<Camera>
|
||||||
|
|
||||||
fun attachControl(noPreventDefault: Boolean = definedExternally)
|
fun attachControl(noPreventDefault: Boolean = definedExternally)
|
||||||
|
fun storeState(): Camera
|
||||||
|
fun restoreState(): Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
open external class TargetCamera : Camera
|
open external class TargetCamera : Camera {
|
||||||
|
var target: Vector3
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param setActiveOnSceneIfNoneActive default true
|
* @param setActiveOnSceneIfNoneActive default true
|
||||||
@ -236,6 +278,9 @@ external class ArcRotateCamera(
|
|||||||
scene: Scene,
|
scene: Scene,
|
||||||
setActiveOnSceneIfNoneActive: Boolean = definedExternally,
|
setActiveOnSceneIfNoneActive: Boolean = definedExternally,
|
||||||
) : TargetCamera {
|
) : TargetCamera {
|
||||||
|
var alpha: Double
|
||||||
|
var beta: Double
|
||||||
|
var radius: Double
|
||||||
var inertia: Double
|
var inertia: Double
|
||||||
var angularSensibilityX: Double
|
var angularSensibilityX: Double
|
||||||
var angularSensibilityY: Double
|
var angularSensibilityY: Double
|
||||||
@ -244,6 +289,7 @@ external class ArcRotateCamera(
|
|||||||
var panningAxis: Vector3
|
var panningAxis: Vector3
|
||||||
var pinchDeltaPercentage: Double
|
var pinchDeltaPercentage: Double
|
||||||
var wheelDeltaPercentage: Double
|
var wheelDeltaPercentage: Double
|
||||||
|
var lowerBetaLimit: Double
|
||||||
|
|
||||||
fun attachControl(
|
fun attachControl(
|
||||||
element: HTMLCanvasElement,
|
element: HTMLCanvasElement,
|
||||||
|
@ -13,7 +13,7 @@ import world.phantasmal.web.questEditor.controllers.QuestInfoController
|
|||||||
import world.phantasmal.web.questEditor.loading.AreaAssetLoader
|
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.EntityManipulator
|
import world.phantasmal.web.questEditor.rendering.UserInputManager
|
||||||
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.AreaStore
|
||||||
@ -57,7 +57,7 @@ class QuestEditor(
|
|||||||
areaAssetLoader,
|
areaAssetLoader,
|
||||||
entityAssetLoader
|
entityAssetLoader
|
||||||
),
|
),
|
||||||
EntityManipulator(questEditorStore, renderer)
|
UserInputManager(questEditorStore, renderer)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Main Widget
|
// Main Widget
|
||||||
|
@ -19,6 +19,7 @@ import world.phantasmal.web.externals.babylon.Scene
|
|||||||
import world.phantasmal.web.externals.babylon.TransformNode
|
import world.phantasmal.web.externals.babylon.TransformNode
|
||||||
import world.phantasmal.web.questEditor.models.AreaVariantModel
|
import world.phantasmal.web.questEditor.models.AreaVariantModel
|
||||||
import world.phantasmal.web.questEditor.models.SectionModel
|
import world.phantasmal.web.questEditor.models.SectionModel
|
||||||
|
import world.phantasmal.web.questEditor.rendering.CollisionMetadata
|
||||||
import world.phantasmal.webui.DisposableContainer
|
import world.phantasmal.webui.DisposableContainer
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -261,6 +262,7 @@ private fun areaCollisionGeometryToTransformNode(
|
|||||||
if (builder.vertexCount > 0) {
|
if (builder.vertexCount > 0) {
|
||||||
val mesh = Mesh("Collision Geometry", scene, parent = node)
|
val mesh = Mesh("Collision Geometry", scene, parent = node)
|
||||||
builder.build().applyToMesh(mesh)
|
builder.build().applyToMesh(mesh)
|
||||||
|
mesh.metadata = CollisionMetadata()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,17 +2,17 @@ package world.phantasmal.web.questEditor.rendering
|
|||||||
|
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
import world.phantasmal.lib.fileFormats.quest.Episode
|
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.loading.AreaAssetLoader
|
||||||
import world.phantasmal.web.questEditor.models.AreaVariantModel
|
import world.phantasmal.web.questEditor.models.AreaVariantModel
|
||||||
|
|
||||||
private val logger = KotlinLogging.logger {}
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
class AreaMeshManager(private val areaAssetLoader: AreaAssetLoader) {
|
class AreaMeshManager(
|
||||||
private var currentGeometry: TransformNode? = null
|
private val renderer: QuestRenderer,
|
||||||
|
private val areaAssetLoader: AreaAssetLoader,
|
||||||
|
) {
|
||||||
suspend fun load(episode: Episode?, areaVariant: AreaVariantModel?) {
|
suspend fun load(episode: Episode?, areaVariant: AreaVariantModel?) {
|
||||||
currentGeometry?.setEnabled(false)
|
renderer.collisionGeometry?.setEnabled(false)
|
||||||
|
|
||||||
if (episode == null || areaVariant == null) {
|
if (episode == null || areaVariant == null) {
|
||||||
return
|
return
|
||||||
@ -21,7 +21,8 @@ class AreaMeshManager(private val areaAssetLoader: AreaAssetLoader) {
|
|||||||
try {
|
try {
|
||||||
val geom = areaAssetLoader.loadCollisionGeometry(episode, areaVariant)
|
val geom = areaAssetLoader.loadCollisionGeometry(episode, areaVariant)
|
||||||
geom.setEnabled(true)
|
geom.setEnabled(true)
|
||||||
currentGeometry = geom
|
renderer.collisionGeometry?.setEnabled(false)
|
||||||
|
renderer.collisionGeometry = geom
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logger.error(e) {
|
logger.error(e) {
|
||||||
"Couldn't load models for area ${areaVariant.area.id}, variant ${areaVariant.id}."
|
"Couldn't load models for area ${areaVariant.area.id}, variant ${areaVariant.id}."
|
||||||
|
@ -11,7 +11,6 @@ import world.phantasmal.web.questEditor.models.QuestEntityModel
|
|||||||
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.models.WaveModel
|
||||||
import world.phantasmal.web.questEditor.rendering.conversion.EntityMetadata
|
|
||||||
import world.phantasmal.web.questEditor.stores.QuestEditorStore
|
import world.phantasmal.web.questEditor.stores.QuestEditorStore
|
||||||
import world.phantasmal.webui.DisposableContainer
|
import world.phantasmal.webui.DisposableContainer
|
||||||
|
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package world.phantasmal.web.questEditor.rendering.conversion
|
package world.phantasmal.web.questEditor.rendering
|
||||||
|
|
||||||
import world.phantasmal.web.questEditor.models.QuestEntityModel
|
import world.phantasmal.web.questEditor.models.QuestEntityModel
|
||||||
|
|
||||||
class EntityMetadata(val entity: QuestEntityModel<*, *>)
|
class EntityMetadata(val entity: QuestEntityModel<*, *>)
|
||||||
|
|
||||||
|
class CollisionMetadata
|
@ -21,14 +21,14 @@ import world.phantasmal.web.questEditor.stores.QuestEditorStore
|
|||||||
abstract class QuestMeshManager protected constructor(
|
abstract class QuestMeshManager protected constructor(
|
||||||
private val scope: CoroutineScope,
|
private val scope: CoroutineScope,
|
||||||
questEditorStore: QuestEditorStore,
|
questEditorStore: QuestEditorStore,
|
||||||
renderer: QuestRenderer,
|
private val renderer: QuestRenderer,
|
||||||
areaAssetLoader: AreaAssetLoader,
|
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 areaMeshManager = AreaMeshManager(renderer, areaAssetLoader)
|
||||||
private val npcMeshManager = disposer.add(
|
private val npcMeshManager = disposer.add(
|
||||||
EntityMeshManager(scope, questEditorStore, renderer, entityAssetLoader)
|
EntityMeshManager(scope, questEditorStore, renderer, entityAssetLoader)
|
||||||
)
|
)
|
||||||
@ -51,6 +51,8 @@ abstract class QuestMeshManager protected constructor(
|
|||||||
npcMeshManager.removeAll()
|
npcMeshManager.removeAll()
|
||||||
objectMeshManager.removeAll()
|
objectMeshManager.removeAll()
|
||||||
|
|
||||||
|
renderer.resetCamera()
|
||||||
|
|
||||||
// Load area model.
|
// Load area model.
|
||||||
areaMeshManager.load(episode, areaVariant)
|
areaMeshManager.load(episode, areaVariant)
|
||||||
|
|
||||||
|
@ -4,12 +4,16 @@ import org.w3c.dom.HTMLCanvasElement
|
|||||||
import world.phantasmal.web.core.rendering.Renderer
|
import world.phantasmal.web.core.rendering.Renderer
|
||||||
import world.phantasmal.web.externals.babylon.ArcRotateCamera
|
import world.phantasmal.web.externals.babylon.ArcRotateCamera
|
||||||
import world.phantasmal.web.externals.babylon.Engine
|
import world.phantasmal.web.externals.babylon.Engine
|
||||||
|
import world.phantasmal.web.externals.babylon.TransformNode
|
||||||
import world.phantasmal.web.externals.babylon.Vector3
|
import world.phantasmal.web.externals.babylon.Vector3
|
||||||
import kotlin.math.PI
|
import kotlin.math.PI
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
class QuestRenderer(canvas: HTMLCanvasElement, engine: Engine) : Renderer(canvas, engine) {
|
class QuestRenderer(canvas: HTMLCanvasElement, engine: Engine) : Renderer(canvas, engine) {
|
||||||
override val camera = ArcRotateCamera("Camera", PI / 2, PI / 6, 500.0, Vector3.Zero(), scene)
|
override val camera = ArcRotateCamera("Camera", PI / 2, PI / 6, 500.0, Vector3.Zero(), scene)
|
||||||
|
|
||||||
|
var collisionGeometry: TransformNode? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
with(camera) {
|
with(camera) {
|
||||||
attachControl(
|
attachControl(
|
||||||
@ -21,11 +25,38 @@ class QuestRenderer(canvas: HTMLCanvasElement, engine: Engine) : Renderer(canvas
|
|||||||
inertia = 0.0
|
inertia = 0.0
|
||||||
angularSensibilityX = 200.0
|
angularSensibilityX = 200.0
|
||||||
angularSensibilityY = 200.0
|
angularSensibilityY = 200.0
|
||||||
|
// Set lowerBetaLimit to avoid shitty camera implementation from breaking completely
|
||||||
|
// when looking directly down.
|
||||||
|
lowerBetaLimit = 0.4
|
||||||
panningInertia = 0.0
|
panningInertia = 0.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
|
||||||
|
|
||||||
|
updatePanningSensibility()
|
||||||
|
onViewMatrixChangedObservable.add({ _, _ ->
|
||||||
|
updatePanningSensibility()
|
||||||
|
})
|
||||||
|
|
||||||
|
camera.storeState()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun resetCamera() {
|
||||||
|
camera.restoreState()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun render() {
|
||||||
|
camera.minZ = max(0.01, camera.radius / 100)
|
||||||
|
camera.maxZ = max(2_000.0, 10 * camera.radius)
|
||||||
|
super.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make "panningSensibility" an inverse function of radius to make panning work "sensibly"
|
||||||
|
* at all distances.
|
||||||
|
*/
|
||||||
|
private fun updatePanningSensibility() {
|
||||||
|
camera.panningSensibility = 1_000 / camera.radius
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,21 +2,20 @@ package world.phantasmal.web.questEditor.rendering
|
|||||||
|
|
||||||
import kotlinx.browser.document
|
import kotlinx.browser.document
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
import org.w3c.dom.events.Event
|
|
||||||
import org.w3c.dom.pointerevents.PointerEvent
|
import org.w3c.dom.pointerevents.PointerEvent
|
||||||
import world.phantasmal.web.core.minusAssign
|
import world.phantasmal.core.disposable.Disposable
|
||||||
import world.phantasmal.web.externals.babylon.AbstractMesh
|
import world.phantasmal.web.core.minus
|
||||||
import world.phantasmal.web.externals.babylon.Vector2
|
import world.phantasmal.web.externals.babylon.*
|
||||||
import world.phantasmal.web.externals.babylon.Vector3
|
|
||||||
import world.phantasmal.web.questEditor.models.QuestEntityModel
|
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.web.questEditor.stores.QuestEditorStore
|
||||||
import world.phantasmal.webui.DisposableContainer
|
import world.phantasmal.webui.DisposableContainer
|
||||||
import world.phantasmal.webui.dom.disposableListener
|
import world.phantasmal.webui.dom.disposableListener
|
||||||
|
|
||||||
private val logger = KotlinLogging.logger {}
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
class EntityManipulator(
|
private val DOWN_VECTOR = Vector3.Down()
|
||||||
|
|
||||||
|
class UserInputManager(
|
||||||
private val questEditorStore: QuestEditorStore,
|
private val questEditorStore: QuestEditorStore,
|
||||||
private val renderer: QuestRenderer,
|
private val renderer: QuestRenderer,
|
||||||
) : DisposableContainer() {
|
) : DisposableContainer() {
|
||||||
@ -24,12 +23,13 @@ class EntityManipulator(
|
|||||||
private val lastPointerPosition = Vector2.Zero()
|
private val lastPointerPosition = Vector2.Zero()
|
||||||
private var movedSinceLastPointerDown = false
|
private var movedSinceLastPointerDown = false
|
||||||
private var state: State
|
private var state: State
|
||||||
|
private var onPointerUpListener: Disposable? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether entity transformations, deletions, etc. are enabled or not.
|
* Whether entity transformations, deletions, etc. are enabled or not.
|
||||||
* Hover over and selection still work when this is set to false.
|
* Hover over and selection still work when this is set to false.
|
||||||
*/
|
*/
|
||||||
var enabled: Boolean = true
|
var entityManipulationEnabled: Boolean = true
|
||||||
set(enabled) {
|
set(enabled) {
|
||||||
field = enabled
|
field = enabled
|
||||||
state.cancel()
|
state.cancel()
|
||||||
@ -37,7 +37,7 @@ class EntityManipulator(
|
|||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
state = IdleState(questEditorStore, renderer, enabled)
|
state = IdleState(questEditorStore, renderer, entityManipulationEnabled)
|
||||||
|
|
||||||
observe(questEditorStore.selectedEntity) { state.cancel() }
|
observe(questEditorStore.selectedEntity) { state.cancel() }
|
||||||
|
|
||||||
@ -54,12 +54,11 @@ class EntityManipulator(
|
|||||||
movedSinceLastPointerDown
|
movedSinceLastPointerDown
|
||||||
))
|
))
|
||||||
|
|
||||||
document.addEventListener("pointerup", ::onPointerUp)
|
onPointerUpListener = disposableListener(document, "pointerup", ::onPointerUp)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onPointerUp(e: Event) {
|
private fun onPointerUp(e: PointerEvent) {
|
||||||
try {
|
try {
|
||||||
e as PointerEvent
|
|
||||||
processPointerEvent(e)
|
processPointerEvent(e)
|
||||||
|
|
||||||
state = state.processEvent(PointerUpEvt(
|
state = state.processEvent(PointerUpEvt(
|
||||||
@ -67,7 +66,8 @@ class EntityManipulator(
|
|||||||
movedSinceLastPointerDown
|
movedSinceLastPointerDown
|
||||||
))
|
))
|
||||||
} finally {
|
} finally {
|
||||||
document.removeEventListener("pointerup", ::onPointerUp)
|
onPointerUpListener?.dispose()
|
||||||
|
onPointerUpListener = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,16 +112,16 @@ private class Pick(
|
|||||||
val mesh: AbstractMesh,
|
val mesh: AbstractMesh,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vector that points from the grabbing point (somewhere on the model's surface) to the model's
|
* Vector that points from the grabbing point (somewhere on the model's surface) to the entity's
|
||||||
* origin.
|
* position.
|
||||||
*/
|
*/
|
||||||
val grabOffset: Vector3,
|
val grabOffset: Vector3,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vector that points from the grabbing point to the terrain point directly under the model's
|
* Vector that points from the grabbing point to the terrain point directly under the entity's
|
||||||
* origin.
|
* position.
|
||||||
*/
|
*/
|
||||||
// val dragAdjust: Vector3,
|
val dragAdjust: Vector3,
|
||||||
)
|
)
|
||||||
|
|
||||||
private abstract class State {
|
private abstract class State {
|
||||||
@ -141,7 +141,7 @@ private abstract class State {
|
|||||||
private class IdleState(
|
private class IdleState(
|
||||||
private val questEditorStore: QuestEditorStore,
|
private val questEditorStore: QuestEditorStore,
|
||||||
private val renderer: QuestRenderer,
|
private val renderer: QuestRenderer,
|
||||||
private val enabled: Boolean,
|
private val entityManipulationEnabled: Boolean,
|
||||||
) : State() {
|
) : State() {
|
||||||
override fun processEvent(event: Evt): State =
|
override fun processEvent(event: Evt): State =
|
||||||
when (event) {
|
when (event) {
|
||||||
@ -151,14 +151,14 @@ private class IdleState(
|
|||||||
1 -> {
|
1 -> {
|
||||||
questEditorStore.setSelectedEntity(pick.entity)
|
questEditorStore.setSelectedEntity(pick.entity)
|
||||||
|
|
||||||
if (enabled) {
|
if (entityManipulationEnabled) {
|
||||||
// TODO: Enter TranslationState.
|
// TODO: Enter TranslationState.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
2 -> {
|
2 -> {
|
||||||
questEditorStore.setSelectedEntity(pick.entity)
|
questEditorStore.setSelectedEntity(pick.entity)
|
||||||
|
|
||||||
if (enabled) {
|
if (entityManipulationEnabled) {
|
||||||
// TODO: Enter RotationState.
|
// TODO: Enter RotationState.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -169,6 +169,8 @@ private class IdleState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
is PointerUpEvt -> {
|
is PointerUpEvt -> {
|
||||||
|
updateCameraTarget()
|
||||||
|
|
||||||
// If the user clicks on nothing, deselect the currently selected entity.
|
// If the user clicks on nothing, deselect the currently selected entity.
|
||||||
if (!event.movedSinceLastPointerDown && pickEntity() == null) {
|
if (!event.movedSinceLastPointerDown && pickEntity() == null) {
|
||||||
questEditorStore.setSelectedEntity(null)
|
questEditorStore.setSelectedEntity(null)
|
||||||
@ -182,6 +184,14 @@ private class IdleState(
|
|||||||
// Do nothing.
|
// Do nothing.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateCameraTarget() {
|
||||||
|
// If the user moved the camera, try setting the camera
|
||||||
|
// target to a better point.
|
||||||
|
pickGround()?.pickedPoint?.let { newTarget ->
|
||||||
|
renderer.camera.target = newTarget
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun pickEntity(): Pick? {
|
private fun pickEntity(): Pick? {
|
||||||
// Find the nearest object and NPC under the pointer.
|
// Find the nearest object and NPC under the pointer.
|
||||||
val pickInfo = renderer.scene.pick(renderer.scene.pointerX, renderer.scene.pointerY)
|
val pickInfo = renderer.scene.pick(renderer.scene.pointerX, renderer.scene.pointerY)
|
||||||
@ -189,27 +199,49 @@ private class IdleState(
|
|||||||
|
|
||||||
val entity = (pickInfo.pickedMesh.metadata as? EntityMetadata)?.entity
|
val entity = (pickInfo.pickedMesh.metadata as? EntityMetadata)?.entity
|
||||||
?: return null
|
?: return null
|
||||||
val grabOffset = pickInfo.pickedMesh.position.clone()
|
|
||||||
grabOffset -= pickInfo.pickedPoint!!
|
|
||||||
|
|
||||||
// TODO: dragAdjust.
|
// Vector from the point where we grab the entity to its position.
|
||||||
// val dragAdjust = grabOffset.clone()
|
val grabOffset = pickInfo.pickedMesh.position - pickInfo.pickedPoint!!
|
||||||
//
|
|
||||||
// // Find vertical distance to the ground.
|
// Vector from the point where we grab the entity to the point on the ground right beneath
|
||||||
// raycaster.set(intersection.object.position, DOWN_VECTOR)
|
// its position. The same as grabOffset when an entity is standing on the ground.
|
||||||
// val [collision_geom_intersection] = raycaster.intersectObjects(
|
val dragAdjust = grabOffset.clone()
|
||||||
// this.renderer.collision_geometry.children,
|
|
||||||
// true,
|
// Find vertical distance to the ground.
|
||||||
// )
|
renderer.scene.pickWithRay(
|
||||||
//
|
Ray(pickInfo.pickedMesh.position, DOWN_VECTOR),
|
||||||
// if (collision_geom_intersection) {
|
{ it.metadata is CollisionMetadata },
|
||||||
// dragAdjust.y -= collision_geom_intersection.distance
|
)?.let { groundPick ->
|
||||||
// }
|
dragAdjust.y -= groundPick.distance
|
||||||
|
}
|
||||||
|
|
||||||
return Pick(
|
return Pick(
|
||||||
entity,
|
entity,
|
||||||
pickInfo.pickedMesh,
|
pickInfo.pickedMesh,
|
||||||
grabOffset,
|
grabOffset,
|
||||||
|
dragAdjust,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun pickGround(): PickingInfo? {
|
||||||
|
renderer.scene.multiPick(
|
||||||
|
renderer.engine.getRenderWidth() / 2,
|
||||||
|
renderer.engine.getRenderHeight() / 2,
|
||||||
|
{ it.metadata is CollisionMetadata },
|
||||||
|
renderer.camera,
|
||||||
|
)?.let { pickingInfoArray ->
|
||||||
|
// Don't allow entities to be placed on very steep terrain.
|
||||||
|
// E.g. walls.
|
||||||
|
// TODO: make use of the flags field in the collision data.
|
||||||
|
for (pickingInfo in pickingInfoArray) {
|
||||||
|
pickingInfo.getNormal()?.let { n ->
|
||||||
|
if (n.y > 0.75) {
|
||||||
|
return pickingInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
@ -39,7 +39,7 @@ class QuestEditorToolbarWidget(
|
|||||||
disabled = ctrl.areaSelectDisabled,
|
disabled = ctrl.areaSelectDisabled,
|
||||||
itemsVal = ctrl.areas,
|
itemsVal = ctrl.areas,
|
||||||
itemToString = { it.label },
|
itemToString = { it.label },
|
||||||
selectedVal = ctrl.currentArea,
|
selectedVal = ctrl.currentArea,
|
||||||
onSelect = ctrl::setCurrentArea
|
onSelect = ctrl::setCurrentArea
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -6,6 +6,7 @@ import org.w3c.dom.*
|
|||||||
import org.w3c.dom.events.Event
|
import org.w3c.dom.events.Event
|
||||||
import org.w3c.dom.events.KeyboardEvent
|
import org.w3c.dom.events.KeyboardEvent
|
||||||
import org.w3c.dom.events.MouseEvent
|
import org.w3c.dom.events.MouseEvent
|
||||||
|
import world.phantasmal.core.disposable.Disposable
|
||||||
import world.phantasmal.observable.value.Val
|
import world.phantasmal.observable.value.Val
|
||||||
import world.phantasmal.observable.value.falseVal
|
import world.phantasmal.observable.value.falseVal
|
||||||
import world.phantasmal.observable.value.value
|
import world.phantasmal.observable.value.value
|
||||||
@ -34,6 +35,7 @@ class Menu<T : Any>(
|
|||||||
private var highlightedIndex: Int? = null
|
private var highlightedIndex: Int? = null
|
||||||
private var highlightedElement: Element? = null
|
private var highlightedElement: Element? = null
|
||||||
private var previouslyFocusedElement: Element? = null
|
private var previouslyFocusedElement: Element? = null
|
||||||
|
private var onDocumentMouseDownListener: Disposable? = null
|
||||||
|
|
||||||
override fun Node.createElement() =
|
override fun Node.createElement() =
|
||||||
div {
|
div {
|
||||||
@ -57,12 +59,14 @@ class Menu<T : Any>(
|
|||||||
|
|
||||||
observe(this@Menu.hidden) {
|
observe(this@Menu.hidden) {
|
||||||
if (it) {
|
if (it) {
|
||||||
document.removeEventListener("mousedown", ::onDocumentMouseDown)
|
onDocumentMouseDownListener?.dispose()
|
||||||
|
onDocumentMouseDownListener = null
|
||||||
clearHighlightItem()
|
clearHighlightItem()
|
||||||
|
|
||||||
(previouslyFocusedElement as HTMLElement?)?.focus()
|
(previouslyFocusedElement as HTMLElement?)?.focus()
|
||||||
} else {
|
} else {
|
||||||
document.addEventListener("mousedown", ::onDocumentMouseDown)
|
onDocumentMouseDownListener =
|
||||||
|
disposableListener(document, "mousedown", ::onDocumentMouseDown)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,7 +80,7 @@ class Menu<T : Any>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun internalDispose() {
|
override fun internalDispose() {
|
||||||
document.removeEventListener("mousedown", ::onDocumentMouseDown)
|
onDocumentMouseDownListener?.dispose()
|
||||||
super.internalDispose()
|
super.internalDispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,15 +52,15 @@ class Toolbar(
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-bottom: var(--pw-border);
|
border-bottom: var(--pw-border);
|
||||||
padding: 0 2px;
|
padding: 3px 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pw-toolbar > * {
|
.pw-toolbar > * {
|
||||||
margin: 2px 1px;
|
margin: 0 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pw-toolbar > .pw-toolbar-group {
|
.pw-toolbar > .pw-toolbar-group {
|
||||||
margin: 2px 3px;
|
margin: 0 3px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
Loading…
Reference in New Issue
Block a user