diff --git a/web/src/main/kotlin/world/phantasmal/web/core/rendering/Renderer.kt b/web/src/main/kotlin/world/phantasmal/web/core/rendering/Renderer.kt index c1b7c455..c55ac29f 100644 --- a/web/src/main/kotlin/world/phantasmal/web/core/rendering/Renderer.kt +++ b/web/src/main/kotlin/world/phantasmal/web/core/rendering/Renderer.kt @@ -9,11 +9,11 @@ private val logger = KotlinLogging.logger {} abstract class Renderer( val canvas: HTMLCanvasElement, - protected val engine: Engine, + val engine: Engine, ) : DisposableContainer() { private val light: HemisphericLight - protected abstract val camera: Camera + abstract val camera: Camera val scene = Scene(engine) @@ -44,7 +44,7 @@ abstract class Renderer( engine.stopRenderLoop() } - private fun render() { + protected open fun render() { val lightDirection = Vector3(-1.0, 1.0, 1.0) lightDirection.rotateByQuaternionToRef(camera.absoluteRotation, lightDirection) light.direction = lightDirection diff --git a/web/src/main/kotlin/world/phantasmal/web/externals/babylon/babylon.kt b/web/src/main/kotlin/world/phantasmal/web/externals/babylon/babylon.kt index 21d30e10..fa041de3 100644 --- a/web/src/main/kotlin/world/phantasmal/web/externals/babylon/babylon.kt +++ b/web/src/main/kotlin/world/phantasmal/web/externals/babylon/babylon.kt @@ -50,6 +50,7 @@ external class Vector3(x: Double, y: Double, z: Double) { companion object { fun One(): Vector3 fun Up(): Vector3 + fun Down(): Vector3 fun Zero(): Vector3 fun Dot(left: Vector3, right: Vector3): Double fun TransformCoordinates(vector: Vector3, transformation: Matrix): Vector3 @@ -145,6 +146,8 @@ open external class ThinEngine { * be removed. */ fun stopRenderLoop(renderFunction: () -> Unit = definedExternally) + fun getRenderWidth(useScreen: Boolean = definedExternally): Double + fun getRenderHeight(useScreen: Boolean = definedExternally): Double fun dispose() } @@ -154,7 +157,7 @@ external class Engine( antialias: Boolean = definedExternally, ) : ThinEngine -external class Ray +external class Ray(origin: Vector3, direction: Vector3, length: Double = definedExternally) external class PickingInfo { val bu: Double @@ -166,6 +169,13 @@ external class PickingInfo { val pickedMesh: AbstractMesh? val pickedPoint: Vector3? val ray: Ray? + + fun getNormal( + useWorldCoordinates: Boolean = definedExternally, + useVerticesNormals: Boolean = definedExternally, + ): Vector3? + + fun getTextureCoordinates(): Vector2? } 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, ): 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? + + fun multiPickWithRay( + ray: Ray, + predicate: (AbstractMesh) -> Boolean = definedExternally, + trianglePredicate: (p0: Vector3, p1: Vector3, p2: Vector3, ray: Ray) -> Boolean = definedExternally, + ): Array? + fun dispose() } @@ -214,15 +250,21 @@ open external class Node { } open external class Camera : Node { + var minZ: Double + var maxZ: Double val absoluteRotation: Quaternion val onProjectionMatrixChangedObservable: Observable val onViewMatrixChangedObservable: Observable val onAfterCheckInputsObservable: Observable 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 @@ -236,6 +278,9 @@ external class ArcRotateCamera( scene: Scene, setActiveOnSceneIfNoneActive: Boolean = definedExternally, ) : TargetCamera { + var alpha: Double + var beta: Double + var radius: Double var inertia: Double var angularSensibilityX: Double var angularSensibilityY: Double @@ -244,6 +289,7 @@ external class ArcRotateCamera( var panningAxis: Vector3 var pinchDeltaPercentage: Double var wheelDeltaPercentage: Double + var lowerBetaLimit: Double fun attachControl( element: HTMLCanvasElement, diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/QuestEditor.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/QuestEditor.kt index 50d1f68b..da0fae62 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/QuestEditor.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/QuestEditor.kt @@ -13,7 +13,7 @@ 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.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.QuestRenderer import world.phantasmal.web.questEditor.stores.AreaStore @@ -57,7 +57,7 @@ class QuestEditor( areaAssetLoader, entityAssetLoader ), - EntityManipulator(questEditorStore, renderer) + UserInputManager(questEditorStore, renderer) ) // Main Widget diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/loading/AreaAssetLoader.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/loading/AreaAssetLoader.kt index 436b33ff..9470ee4e 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/loading/AreaAssetLoader.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/loading/AreaAssetLoader.kt @@ -19,6 +19,7 @@ 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.models.SectionModel +import world.phantasmal.web.questEditor.rendering.CollisionMetadata import world.phantasmal.webui.DisposableContainer /** @@ -261,6 +262,7 @@ private fun areaCollisionGeometryToTransformNode( if (builder.vertexCount > 0) { val mesh = Mesh("Collision Geometry", scene, parent = node) builder.build().applyToMesh(mesh) + mesh.metadata = CollisionMetadata() } } diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/AreaMeshManager.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/AreaMeshManager.kt index 7fed7cf9..442ef3d3 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/AreaMeshManager.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/AreaMeshManager.kt @@ -2,17 +2,17 @@ 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 - +class AreaMeshManager( + private val renderer: QuestRenderer, + private val areaAssetLoader: AreaAssetLoader, +) { suspend fun load(episode: Episode?, areaVariant: AreaVariantModel?) { - currentGeometry?.setEnabled(false) + renderer.collisionGeometry?.setEnabled(false) if (episode == null || areaVariant == null) { return @@ -21,7 +21,8 @@ class AreaMeshManager(private val areaAssetLoader: AreaAssetLoader) { try { val geom = areaAssetLoader.loadCollisionGeometry(episode, areaVariant) geom.setEnabled(true) - currentGeometry = geom + renderer.collisionGeometry?.setEnabled(false) + renderer.collisionGeometry = geom } catch (e: Exception) { logger.error(e) { "Couldn't load models for area ${areaVariant.area.id}, variant ${areaVariant.id}." diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/EntityMeshManager.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/EntityMeshManager.kt index f1a2dbfc..2447fdad 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/EntityMeshManager.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/EntityMeshManager.kt @@ -11,7 +11,6 @@ import world.phantasmal.web.questEditor.models.QuestEntityModel import world.phantasmal.web.questEditor.models.QuestNpcModel import world.phantasmal.web.questEditor.models.QuestObjectModel 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 diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/conversion/Entities.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/MeshMetadata.kt similarity index 61% rename from web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/conversion/Entities.kt rename to web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/MeshMetadata.kt index d813fe2f..38ff19b6 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/conversion/Entities.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/MeshMetadata.kt @@ -1,5 +1,7 @@ -package world.phantasmal.web.questEditor.rendering.conversion +package world.phantasmal.web.questEditor.rendering import world.phantasmal.web.questEditor.models.QuestEntityModel class EntityMetadata(val entity: QuestEntityModel<*, *>) + +class CollisionMetadata diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/QuestMeshManager.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/QuestMeshManager.kt index b24b27d6..38479e19 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/QuestMeshManager.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/QuestMeshManager.kt @@ -21,14 +21,14 @@ import world.phantasmal.web.questEditor.stores.QuestEditorStore abstract class QuestMeshManager protected constructor( private val scope: CoroutineScope, questEditorStore: QuestEditorStore, - renderer: QuestRenderer, + private val renderer: QuestRenderer, areaAssetLoader: AreaAssetLoader, entityAssetLoader: EntityAssetLoader, ) : TrackedDisposable() { protected val disposer = Disposer() private val areaDisposer = disposer.add(Disposer()) - private val areaMeshManager = AreaMeshManager(areaAssetLoader) + private val areaMeshManager = AreaMeshManager(renderer, areaAssetLoader) private val npcMeshManager = disposer.add( EntityMeshManager(scope, questEditorStore, renderer, entityAssetLoader) ) @@ -51,6 +51,8 @@ abstract class QuestMeshManager protected constructor( npcMeshManager.removeAll() objectMeshManager.removeAll() + renderer.resetCamera() + // Load area model. areaMeshManager.load(episode, areaVariant) diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/QuestRenderer.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/QuestRenderer.kt index 69843b6b..b17ffbd7 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/QuestRenderer.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/QuestRenderer.kt @@ -4,12 +4,16 @@ import org.w3c.dom.HTMLCanvasElement import world.phantasmal.web.core.rendering.Renderer import world.phantasmal.web.externals.babylon.ArcRotateCamera import world.phantasmal.web.externals.babylon.Engine +import world.phantasmal.web.externals.babylon.TransformNode import world.phantasmal.web.externals.babylon.Vector3 import kotlin.math.PI +import kotlin.math.max class QuestRenderer(canvas: HTMLCanvasElement, engine: Engine) : Renderer(canvas, engine) { override val camera = ArcRotateCamera("Camera", PI / 2, PI / 6, 500.0, Vector3.Zero(), scene) + var collisionGeometry: TransformNode? = null + init { with(camera) { attachControl( @@ -21,11 +25,38 @@ class QuestRenderer(canvas: HTMLCanvasElement, engine: Engine) : Renderer(canvas inertia = 0.0 angularSensibilityX = 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 - panningSensibility = 3.0 panningAxis = Vector3(1.0, 0.0, -1.0) pinchDeltaPercentage = 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 + } } diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/EntityManipulator.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/UserInputManager.kt similarity index 65% rename from web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/EntityManipulator.kt rename to web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/UserInputManager.kt index c7ca3c2d..347a53b3 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/EntityManipulator.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/UserInputManager.kt @@ -2,21 +2,20 @@ 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.core.disposable.Disposable +import world.phantasmal.web.core.minus +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.questEditor.stores.QuestEditorStore import world.phantasmal.webui.DisposableContainer import world.phantasmal.webui.dom.disposableListener private val logger = KotlinLogging.logger {} -class EntityManipulator( +private val DOWN_VECTOR = Vector3.Down() + +class UserInputManager( private val questEditorStore: QuestEditorStore, private val renderer: QuestRenderer, ) : DisposableContainer() { @@ -24,12 +23,13 @@ class EntityManipulator( private val lastPointerPosition = Vector2.Zero() private var movedSinceLastPointerDown = false private var state: State + private var onPointerUpListener: Disposable? = null /** * 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 + var entityManipulationEnabled: Boolean = true set(enabled) { field = enabled state.cancel() @@ -37,7 +37,7 @@ class EntityManipulator( } init { - state = IdleState(questEditorStore, renderer, enabled) + state = IdleState(questEditorStore, renderer, entityManipulationEnabled) observe(questEditorStore.selectedEntity) { state.cancel() } @@ -54,12 +54,11 @@ class EntityManipulator( movedSinceLastPointerDown )) - document.addEventListener("pointerup", ::onPointerUp) + onPointerUpListener = disposableListener(document, "pointerup", ::onPointerUp) } - private fun onPointerUp(e: Event) { + private fun onPointerUp(e: PointerEvent) { try { - e as PointerEvent processPointerEvent(e) state = state.processEvent(PointerUpEvt( @@ -67,7 +66,8 @@ class EntityManipulator( movedSinceLastPointerDown )) } finally { - document.removeEventListener("pointerup", ::onPointerUp) + onPointerUpListener?.dispose() + onPointerUpListener = null } } @@ -112,16 +112,16 @@ private class Pick( val mesh: AbstractMesh, /** - * Vector that points from the grabbing point (somewhere on the model's surface) to the model's - * origin. + * Vector that points from the grabbing point (somewhere on the model's surface) to the entity's + * position. */ val grabOffset: Vector3, /** - * Vector that points from the grabbing point to the terrain point directly under the model's - * origin. + * Vector that points from the grabbing point to the terrain point directly under the entity's + * position. */ -// val dragAdjust: Vector3, + val dragAdjust: Vector3, ) private abstract class State { @@ -141,7 +141,7 @@ private abstract class State { private class IdleState( private val questEditorStore: QuestEditorStore, private val renderer: QuestRenderer, - private val enabled: Boolean, + private val entityManipulationEnabled: Boolean, ) : State() { override fun processEvent(event: Evt): State = when (event) { @@ -151,14 +151,14 @@ private class IdleState( 1 -> { questEditorStore.setSelectedEntity(pick.entity) - if (enabled) { + if (entityManipulationEnabled) { // TODO: Enter TranslationState. } } 2 -> { questEditorStore.setSelectedEntity(pick.entity) - if (enabled) { + if (entityManipulationEnabled) { // TODO: Enter RotationState. } } @@ -169,6 +169,8 @@ private class IdleState( } is PointerUpEvt -> { + updateCameraTarget() + // If the user clicks on nothing, deselect the currently selected entity. if (!event.movedSinceLastPointerDown && pickEntity() == null) { questEditorStore.setSelectedEntity(null) @@ -182,6 +184,14 @@ private class IdleState( // 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? { // Find the nearest object and NPC under the pointer. 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 ?: 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 -// } + // Vector from the point where we grab the entity to its position. + val grabOffset = pickInfo.pickedMesh.position - pickInfo.pickedPoint!! + + // Vector from the point where we grab the entity to the point on the ground right beneath + // its position. The same as grabOffset when an entity is standing on the ground. + val dragAdjust = grabOffset.clone() + + // Find vertical distance to the ground. + renderer.scene.pickWithRay( + Ray(pickInfo.pickedMesh.position, DOWN_VECTOR), + { it.metadata is CollisionMetadata }, + )?.let { groundPick -> + dragAdjust.y -= groundPick.distance + } return Pick( entity, pickInfo.pickedMesh, 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 + } } diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/widgets/QuestEditorToolbarWidget.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/widgets/QuestEditorToolbarWidget.kt index ac554b3f..5628e101 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/widgets/QuestEditorToolbarWidget.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/widgets/QuestEditorToolbarWidget.kt @@ -39,7 +39,7 @@ class QuestEditorToolbarWidget( disabled = ctrl.areaSelectDisabled, itemsVal = ctrl.areas, itemToString = { it.label }, - selectedVal = ctrl.currentArea, + selectedVal = ctrl.currentArea, onSelect = ctrl::setCurrentArea ) ) diff --git a/webui/src/main/kotlin/world/phantasmal/webui/widgets/Menu.kt b/webui/src/main/kotlin/world/phantasmal/webui/widgets/Menu.kt index afdf3662..c1ebf1c9 100644 --- a/webui/src/main/kotlin/world/phantasmal/webui/widgets/Menu.kt +++ b/webui/src/main/kotlin/world/phantasmal/webui/widgets/Menu.kt @@ -6,6 +6,7 @@ import org.w3c.dom.* import org.w3c.dom.events.Event import org.w3c.dom.events.KeyboardEvent import org.w3c.dom.events.MouseEvent +import world.phantasmal.core.disposable.Disposable import world.phantasmal.observable.value.Val import world.phantasmal.observable.value.falseVal import world.phantasmal.observable.value.value @@ -34,6 +35,7 @@ class Menu( private var highlightedIndex: Int? = null private var highlightedElement: Element? = null private var previouslyFocusedElement: Element? = null + private var onDocumentMouseDownListener: Disposable? = null override fun Node.createElement() = div { @@ -57,12 +59,14 @@ class Menu( observe(this@Menu.hidden) { if (it) { - document.removeEventListener("mousedown", ::onDocumentMouseDown) + onDocumentMouseDownListener?.dispose() + onDocumentMouseDownListener = null clearHighlightItem() (previouslyFocusedElement as HTMLElement?)?.focus() } else { - document.addEventListener("mousedown", ::onDocumentMouseDown) + onDocumentMouseDownListener = + disposableListener(document, "mousedown", ::onDocumentMouseDown) } } @@ -76,7 +80,7 @@ class Menu( } override fun internalDispose() { - document.removeEventListener("mousedown", ::onDocumentMouseDown) + onDocumentMouseDownListener?.dispose() super.internalDispose() } diff --git a/webui/src/main/kotlin/world/phantasmal/webui/widgets/Toolbar.kt b/webui/src/main/kotlin/world/phantasmal/webui/widgets/Toolbar.kt index b0249ebe..874efa9a 100644 --- a/webui/src/main/kotlin/world/phantasmal/webui/widgets/Toolbar.kt +++ b/webui/src/main/kotlin/world/phantasmal/webui/widgets/Toolbar.kt @@ -52,15 +52,15 @@ class Toolbar( flex-direction: row; align-items: center; border-bottom: var(--pw-border); - padding: 0 2px; + padding: 3px 2px; } .pw-toolbar > * { - margin: 2px 1px; + margin: 0 1px; } .pw-toolbar > .pw-toolbar-group { - margin: 2px 3px; + margin: 0 3px; display: flex; flex-direction: row; align-items: center;