mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-04 22:58:29 +08:00
Added viewer, xj parsing and fixed several bugs.
This commit is contained in:
parent
bedc7b07a2
commit
8ec75f8b4a
@ -65,7 +65,7 @@ private class PrsDecompressor(private val src: Cursor) {
|
||||
}
|
||||
|
||||
return Success(dst.seekStart(0))
|
||||
} catch (e: Throwable) {
|
||||
} catch (e: Exception) {
|
||||
return PwResult.build<Cursor>(logger)
|
||||
.addProblem(Severity.Error, "PRS-compressed stream is corrupt.", cause = e)
|
||||
.failure()
|
||||
|
@ -6,4 +6,6 @@ class Vec2(val x: Float, val y: Float)
|
||||
|
||||
class Vec3(val x: Float, val y: Float, val z: Float)
|
||||
|
||||
fun Cursor.vec2Float(): Vec2 = Vec2(float(), float())
|
||||
|
||||
fun Cursor.vec3Float(): Vec3 = Vec3(float(), float(), float())
|
||||
|
@ -10,11 +10,11 @@ import world.phantasmal.lib.fileFormats.vec3Float
|
||||
|
||||
private const val NJCM: Int = 0x4D434A4E
|
||||
|
||||
fun parseNj(cursor: Cursor): PwResult<List<NinjaObject<NjcmModel>>> =
|
||||
parseNinja(cursor, ::parseNjcmModel, mutableMapOf())
|
||||
fun parseNj(cursor: Cursor): PwResult<List<NinjaObject<NjModel>>> =
|
||||
parseNinja(cursor, ::parseNjModel, mutableMapOf())
|
||||
|
||||
fun parseXj(cursor: Cursor): PwResult<List<NinjaObject<XjModel>>> =
|
||||
parseNinja(cursor, { _, _ -> XjModel() }, Unit)
|
||||
parseNinja(cursor, { c, _ -> parseXjModel(c) }, Unit)
|
||||
|
||||
private fun <Model : NinjaModel, Context> parseNinja(
|
||||
cursor: Cursor,
|
||||
|
@ -38,17 +38,17 @@ sealed class NinjaModel
|
||||
/**
|
||||
* The model type used in .nj files.
|
||||
*/
|
||||
class NjcmModel(
|
||||
class NjModel(
|
||||
/**
|
||||
* Sparse list of vertices.
|
||||
*/
|
||||
val vertices: List<NjcmVertex?>,
|
||||
val meshes: List<NjcmTriangleStrip>,
|
||||
val vertices: List<NjVertex?>,
|
||||
val meshes: List<NjTriangleStrip>,
|
||||
val collisionSphereCenter: Vec3,
|
||||
val collisionSphereRadius: Float,
|
||||
) : NinjaModel()
|
||||
|
||||
class NjcmVertex(
|
||||
class NjVertex(
|
||||
val position: Vec3,
|
||||
val normal: Vec3?,
|
||||
val boneWeight: Float,
|
||||
@ -56,7 +56,7 @@ class NjcmVertex(
|
||||
val calcContinue: Boolean,
|
||||
)
|
||||
|
||||
class NjcmTriangleStrip(
|
||||
class NjTriangleStrip(
|
||||
val ignoreLight: Boolean,
|
||||
val ignoreSpecular: Boolean,
|
||||
val ignoreAmbient: Boolean,
|
||||
@ -70,25 +70,25 @@ class NjcmTriangleStrip(
|
||||
var textureId: UInt?,
|
||||
var srcAlpha: UByte?,
|
||||
var dstAlpha: UByte?,
|
||||
val vertices: List<NjcmMeshVertex>,
|
||||
val vertices: List<NjMeshVertex>,
|
||||
)
|
||||
|
||||
class NjcmMeshVertex(
|
||||
val index: UShort,
|
||||
class NjMeshVertex(
|
||||
val index: Int,
|
||||
val normal: Vec3?,
|
||||
val texCoords: Vec2?,
|
||||
)
|
||||
|
||||
sealed class NjcmChunk(val typeId: UByte) {
|
||||
class Unknown(typeId: UByte) : NjcmChunk(typeId)
|
||||
sealed class NjChunk(val typeId: UByte) {
|
||||
class Unknown(typeId: UByte) : NjChunk(typeId)
|
||||
|
||||
object Null : NjcmChunk(0u)
|
||||
object Null : NjChunk(0u)
|
||||
|
||||
class Bits(typeId: UByte, val srcAlpha: UByte, val dstAlpha: UByte) : NjcmChunk(typeId)
|
||||
class Bits(typeId: UByte, val srcAlpha: UByte, val dstAlpha: UByte) : NjChunk(typeId)
|
||||
|
||||
class CachePolygonList(val cacheIndex: UByte, val offset: Int) : NjcmChunk(4u)
|
||||
class CachePolygonList(val cacheIndex: UByte, val offset: Int) : NjChunk(4u)
|
||||
|
||||
class DrawPolygonList(val cacheIndex: UByte) : NjcmChunk(5u)
|
||||
class DrawPolygonList(val cacheIndex: UByte) : NjChunk(5u)
|
||||
|
||||
class Tiny(
|
||||
typeId: UByte,
|
||||
@ -100,27 +100,27 @@ sealed class NjcmChunk(val typeId: UByte) {
|
||||
val filterMode: UInt,
|
||||
val superSample: Boolean,
|
||||
val textureId: UInt,
|
||||
) : NjcmChunk(typeId)
|
||||
) : NjChunk(typeId)
|
||||
|
||||
class Material(
|
||||
typeId: UByte,
|
||||
val srcAlpha: UByte,
|
||||
val dstAlpha: UByte,
|
||||
val diffuse: NjcmArgb?,
|
||||
val ambient: NjcmArgb?,
|
||||
val specular: NjcmErgb?,
|
||||
) : NjcmChunk(typeId)
|
||||
val diffuse: NjArgb?,
|
||||
val ambient: NjArgb?,
|
||||
val specular: NjErgb?,
|
||||
) : NjChunk(typeId)
|
||||
|
||||
class Vertex(typeId: UByte, val vertices: List<NjcmChunkVertex>) : NjcmChunk(typeId)
|
||||
class Vertex(typeId: UByte, val vertices: List<NjChunkVertex>) : NjChunk(typeId)
|
||||
|
||||
class Volume(typeId: UByte) : NjcmChunk(typeId)
|
||||
class Volume(typeId: UByte) : NjChunk(typeId)
|
||||
|
||||
class Strip(typeId: UByte, val triangleStrips: List<NjcmTriangleStrip>) : NjcmChunk(typeId)
|
||||
class Strip(typeId: UByte, val triangleStrips: List<NjTriangleStrip>) : NjChunk(typeId)
|
||||
|
||||
object End : NjcmChunk(255u)
|
||||
object End : NjChunk(255u)
|
||||
}
|
||||
|
||||
class NjcmChunkVertex(
|
||||
class NjChunkVertex(
|
||||
val index: Int,
|
||||
val position: Vec3,
|
||||
val normal: Vec3?,
|
||||
@ -132,14 +132,14 @@ class NjcmChunkVertex(
|
||||
/**
|
||||
* Channels are in range [0, 1].
|
||||
*/
|
||||
class NjcmArgb(
|
||||
class NjArgb(
|
||||
val a: Float,
|
||||
val r: Float,
|
||||
val g: Float,
|
||||
val b: Float,
|
||||
)
|
||||
|
||||
class NjcmErgb(
|
||||
class NjErgb(
|
||||
val e: UByte,
|
||||
val r: UByte,
|
||||
val g: UByte,
|
||||
@ -149,4 +149,30 @@ class NjcmErgb(
|
||||
/**
|
||||
* The model type used in .xj files.
|
||||
*/
|
||||
class XjModel : NinjaModel()
|
||||
class XjModel(
|
||||
val vertices: List<XjVertex>,
|
||||
val meshes: List<XjMesh>,
|
||||
val collisionSpherePosition: Vec3,
|
||||
val collisionSphereRadius: Float,
|
||||
) : NinjaModel()
|
||||
|
||||
class XjVertex(
|
||||
val position: Vec3,
|
||||
val normal: Vec3?,
|
||||
val uv: Vec2?,
|
||||
)
|
||||
|
||||
class XjMesh(
|
||||
val material: XjMaterial,
|
||||
val indices: List<Int>,
|
||||
)
|
||||
|
||||
class XjMaterial(
|
||||
val srcAlpha: Int?,
|
||||
val dstAlpha: Int?,
|
||||
val textureId: Int?,
|
||||
val diffuseR: Int?,
|
||||
val diffuseG: Int?,
|
||||
val diffuseB: Int?,
|
||||
val diffuseA: Int?,
|
||||
)
|
||||
|
@ -17,25 +17,25 @@ private const val ZERO_U8: UByte = 0u
|
||||
|
||||
// TODO: Simplify parser by not parsing chunks into vertices and meshes. Do the chunk to vertex/mesh
|
||||
// conversion at a higher level.
|
||||
fun parseNjcmModel(cursor: Cursor, cachedChunkOffsets: MutableMap<UByte, Int>): NjcmModel {
|
||||
fun parseNjModel(cursor: Cursor, cachedChunkOffsets: MutableMap<UByte, Int>): NjModel {
|
||||
val vlistOffset = cursor.int() // Vertex list
|
||||
val plistOffset = cursor.int() // Triangle strip index list
|
||||
val boundingSphereCenter = cursor.vec3Float()
|
||||
val boundingSphereRadius = cursor.float()
|
||||
val vertices: MutableList<NjcmVertex?> = mutableListOf()
|
||||
val meshes: MutableList<NjcmTriangleStrip> = mutableListOf()
|
||||
val collisionSphereCenter = cursor.vec3Float()
|
||||
val collisionSphereRadius = cursor.float()
|
||||
val vertices: MutableList<NjVertex?> = mutableListOf()
|
||||
val meshes: MutableList<NjTriangleStrip> = mutableListOf()
|
||||
|
||||
if (vlistOffset != 0) {
|
||||
cursor.seekStart(vlistOffset)
|
||||
|
||||
for (chunk in parseChunks(cursor, cachedChunkOffsets, true)) {
|
||||
if (chunk is NjcmChunk.Vertex) {
|
||||
if (chunk is NjChunk.Vertex) {
|
||||
for (vertex in chunk.vertices) {
|
||||
while (vertices.size <= vertex.index) {
|
||||
vertices.add(null)
|
||||
}
|
||||
|
||||
vertices[vertex.index] = NjcmVertex(
|
||||
vertices[vertex.index] = NjVertex(
|
||||
vertex.position,
|
||||
vertex.normal,
|
||||
vertex.boneWeight,
|
||||
@ -56,21 +56,21 @@ fun parseNjcmModel(cursor: Cursor, cachedChunkOffsets: MutableMap<UByte, Int>):
|
||||
|
||||
for (chunk in parseChunks(cursor, cachedChunkOffsets, false)) {
|
||||
when (chunk) {
|
||||
is NjcmChunk.Bits -> {
|
||||
is NjChunk.Bits -> {
|
||||
srcAlpha = chunk.srcAlpha
|
||||
dstAlpha = chunk.dstAlpha
|
||||
}
|
||||
|
||||
is NjcmChunk.Tiny -> {
|
||||
is NjChunk.Tiny -> {
|
||||
textureId = chunk.textureId
|
||||
}
|
||||
|
||||
is NjcmChunk.Material -> {
|
||||
is NjChunk.Material -> {
|
||||
srcAlpha = chunk.srcAlpha
|
||||
dstAlpha = chunk.dstAlpha
|
||||
}
|
||||
|
||||
is NjcmChunk.Strip -> {
|
||||
is NjChunk.Strip -> {
|
||||
for (strip in chunk.triangleStrips) {
|
||||
strip.textureId = textureId
|
||||
strip.srcAlpha = srcAlpha
|
||||
@ -87,11 +87,11 @@ fun parseNjcmModel(cursor: Cursor, cachedChunkOffsets: MutableMap<UByte, Int>):
|
||||
}
|
||||
}
|
||||
|
||||
return NjcmModel(
|
||||
return NjModel(
|
||||
vertices,
|
||||
meshes,
|
||||
boundingSphereCenter,
|
||||
boundingSphereRadius,
|
||||
collisionSphereCenter,
|
||||
collisionSphereRadius,
|
||||
)
|
||||
}
|
||||
|
||||
@ -100,8 +100,8 @@ private fun parseChunks(
|
||||
cursor: Cursor,
|
||||
cachedChunkOffsets: MutableMap<UByte, Int>,
|
||||
wideEndChunks: Boolean,
|
||||
): List<NjcmChunk> {
|
||||
val chunks: MutableList<NjcmChunk> = mutableListOf()
|
||||
): List<NjChunk> {
|
||||
val chunks: MutableList<NjChunk> = mutableListOf()
|
||||
var loop = true
|
||||
|
||||
while (loop) {
|
||||
@ -113,10 +113,10 @@ private fun parseChunks(
|
||||
|
||||
when (typeId.toInt()) {
|
||||
0 -> {
|
||||
chunks.add(NjcmChunk.Null)
|
||||
chunks.add(NjChunk.Null)
|
||||
}
|
||||
in 1..3 -> {
|
||||
chunks.add(NjcmChunk.Bits(
|
||||
chunks.add(NjChunk.Bits(
|
||||
typeId,
|
||||
srcAlpha = ((flagsUInt shr 3).toUByte() and 0b111u),
|
||||
dstAlpha = flags and 0b111u,
|
||||
@ -125,7 +125,7 @@ private fun parseChunks(
|
||||
4 -> {
|
||||
val offset = cursor.position
|
||||
|
||||
chunks.add(NjcmChunk.CachePolygonList(
|
||||
chunks.add(NjChunk.CachePolygonList(
|
||||
cacheIndex = flags,
|
||||
offset,
|
||||
))
|
||||
@ -141,7 +141,7 @@ private fun parseChunks(
|
||||
chunks.addAll(parseChunks(cursor, cachedChunkOffsets, wideEndChunks))
|
||||
}
|
||||
|
||||
chunks.add(NjcmChunk.DrawPolygonList(
|
||||
chunks.add(NjChunk.DrawPolygonList(
|
||||
cacheIndex = flags,
|
||||
))
|
||||
}
|
||||
@ -149,7 +149,7 @@ private fun parseChunks(
|
||||
size = 2
|
||||
val textureBitsAndId = cursor.uShort().toUInt()
|
||||
|
||||
chunks.add(NjcmChunk.Tiny(
|
||||
chunks.add(NjChunk.Tiny(
|
||||
typeId,
|
||||
flipU = (typeId.toUInt() and 0x80u) != 0u,
|
||||
flipV = (typeId.toUInt() and 0x40u) != 0u,
|
||||
@ -164,12 +164,12 @@ private fun parseChunks(
|
||||
in 17..31 -> {
|
||||
size = 2 + 2 * cursor.short()
|
||||
|
||||
var diffuse: NjcmArgb? = null
|
||||
var ambient: NjcmArgb? = null
|
||||
var specular: NjcmErgb? = null
|
||||
var diffuse: NjArgb? = null
|
||||
var ambient: NjArgb? = null
|
||||
var specular: NjErgb? = null
|
||||
|
||||
if ((flagsUInt and 0b1u) != 0u) {
|
||||
diffuse = NjcmArgb(
|
||||
diffuse = NjArgb(
|
||||
b = cursor.uByte().toFloat() / 255f,
|
||||
g = cursor.uByte().toFloat() / 255f,
|
||||
r = cursor.uByte().toFloat() / 255f,
|
||||
@ -178,7 +178,7 @@ private fun parseChunks(
|
||||
}
|
||||
|
||||
if ((flagsUInt and 0b10u) != 0u) {
|
||||
ambient = NjcmArgb(
|
||||
ambient = NjArgb(
|
||||
b = cursor.uByte().toFloat() / 255f,
|
||||
g = cursor.uByte().toFloat() / 255f,
|
||||
r = cursor.uByte().toFloat() / 255f,
|
||||
@ -187,7 +187,7 @@ private fun parseChunks(
|
||||
}
|
||||
|
||||
if ((flagsUInt and 0b100u) != 0u) {
|
||||
specular = NjcmErgb(
|
||||
specular = NjErgb(
|
||||
b = cursor.uByte(),
|
||||
g = cursor.uByte(),
|
||||
r = cursor.uByte(),
|
||||
@ -195,7 +195,7 @@ private fun parseChunks(
|
||||
)
|
||||
}
|
||||
|
||||
chunks.add(NjcmChunk.Material(
|
||||
chunks.add(NjChunk.Material(
|
||||
typeId,
|
||||
srcAlpha = ((flagsUInt shr 3).toUByte() and 0b111u),
|
||||
dstAlpha = flags and 0b111u,
|
||||
@ -206,32 +206,32 @@ private fun parseChunks(
|
||||
}
|
||||
in 32..50 -> {
|
||||
size = 2 + 4 * cursor.short()
|
||||
chunks.add(NjcmChunk.Vertex(
|
||||
chunks.add(NjChunk.Vertex(
|
||||
typeId,
|
||||
vertices = parseVertexChunk(cursor, typeId, flags),
|
||||
))
|
||||
}
|
||||
in 56..58 -> {
|
||||
size = 2 + 2 * cursor.short()
|
||||
chunks.add(NjcmChunk.Volume(
|
||||
chunks.add(NjChunk.Volume(
|
||||
typeId,
|
||||
))
|
||||
}
|
||||
in 64..75 -> {
|
||||
size = 2 + 2 * cursor.short()
|
||||
chunks.add(NjcmChunk.Strip(
|
||||
chunks.add(NjChunk.Strip(
|
||||
typeId,
|
||||
triangleStrips = parseTriangleStripChunk(cursor, typeId, flags),
|
||||
))
|
||||
}
|
||||
255 -> {
|
||||
size = if (wideEndChunks) 2 else 0
|
||||
chunks.add(NjcmChunk.End)
|
||||
chunks.add(NjChunk.End)
|
||||
loop = false
|
||||
}
|
||||
else -> {
|
||||
size = 2 + 2 * cursor.short()
|
||||
chunks.add(NjcmChunk.Unknown(
|
||||
chunks.add(NjChunk.Unknown(
|
||||
typeId,
|
||||
))
|
||||
logger.warn { "Unknown chunk type $typeId at offset ${chunkStartPosition}." }
|
||||
@ -248,14 +248,14 @@ private fun parseVertexChunk(
|
||||
cursor: Cursor,
|
||||
chunkTypeId: UByte,
|
||||
flags: UByte,
|
||||
): List<NjcmChunkVertex> {
|
||||
): List<NjChunkVertex> {
|
||||
val boneWeightStatus = (flags and 0b11u).toInt()
|
||||
val calcContinue = (flags and 0x80u) != ZERO_U8
|
||||
|
||||
val index = cursor.uShort()
|
||||
val vertexCount = cursor.uShort()
|
||||
|
||||
val vertices: MutableList<NjcmChunkVertex> = mutableListOf()
|
||||
val vertices: MutableList<NjChunkVertex> = mutableListOf()
|
||||
|
||||
for (i in (0u).toUShort() until vertexCount) {
|
||||
var vertexIndex = index + i
|
||||
@ -317,7 +317,7 @@ private fun parseVertexChunk(
|
||||
}
|
||||
}
|
||||
|
||||
vertices.add(NjcmChunkVertex(
|
||||
vertices.add(NjChunkVertex(
|
||||
vertexIndex.toInt(),
|
||||
position,
|
||||
normal,
|
||||
@ -334,7 +334,7 @@ private fun parseTriangleStripChunk(
|
||||
cursor: Cursor,
|
||||
chunkTypeId: UByte,
|
||||
flags: UByte,
|
||||
): List<NjcmTriangleStrip> {
|
||||
): List<NjTriangleStrip> {
|
||||
val ignoreLight = (flags and 0b1u) != ZERO_U8
|
||||
val ignoreSpecular = (flags and 0b10u) != ZERO_U8
|
||||
val ignoreAmbient = (flags and 0b100u) != ZERO_U8
|
||||
@ -380,17 +380,17 @@ private fun parseTriangleStripChunk(
|
||||
else -> error("Unexpected chunk type ID: ${chunkTypeId}.")
|
||||
}
|
||||
|
||||
val strips: MutableList<NjcmTriangleStrip> = mutableListOf()
|
||||
val strips: MutableList<NjTriangleStrip> = mutableListOf()
|
||||
|
||||
repeat(stripCount) {
|
||||
val windingFlagAndIndexCount = cursor.short()
|
||||
val clockwiseWinding = windingFlagAndIndexCount < 1
|
||||
val indexCount = abs(windingFlagAndIndexCount.toInt())
|
||||
|
||||
val vertices: MutableList<NjcmMeshVertex> = mutableListOf()
|
||||
val vertices: MutableList<NjMeshVertex> = mutableListOf()
|
||||
|
||||
for (j in 0 until indexCount) {
|
||||
val index = cursor.uShort()
|
||||
val index = cursor.uShort().toInt()
|
||||
|
||||
val texCoords = if (hasTexCoords) {
|
||||
Vec2(cursor.uShort().toFloat() / 255f, cursor.uShort().toFloat() / 255f)
|
||||
@ -419,14 +419,14 @@ private fun parseTriangleStripChunk(
|
||||
cursor.seek(2 * userFlagsSize)
|
||||
}
|
||||
|
||||
vertices.add(NjcmMeshVertex(
|
||||
vertices.add(NjMeshVertex(
|
||||
index,
|
||||
normal,
|
||||
texCoords,
|
||||
))
|
||||
}
|
||||
|
||||
strips.add(NjcmTriangleStrip(
|
||||
strips.add(NjTriangleStrip(
|
||||
ignoreLight,
|
||||
ignoreSpecular,
|
||||
ignoreAmbient,
|
@ -1,2 +1,174 @@
|
||||
package world.phantasmal.lib.fileFormats.ninja
|
||||
|
||||
import mu.KotlinLogging
|
||||
import world.phantasmal.lib.cursor.Cursor
|
||||
import world.phantasmal.lib.fileFormats.Vec2
|
||||
import world.phantasmal.lib.fileFormats.Vec3
|
||||
import world.phantasmal.lib.fileFormats.vec2Float
|
||||
import world.phantasmal.lib.fileFormats.vec3Float
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
fun parseXjModel(cursor: Cursor): XjModel {
|
||||
cursor.seek(4) // Flags according to QEdit, seemingly always 0.
|
||||
val vertexInfoTableOffset = cursor.int()
|
||||
val vertexInfoCount = cursor.int()
|
||||
val triangleStripTableOffset = cursor.int()
|
||||
val triangleStripCount = cursor.int()
|
||||
val transparentTriangleStripTableOffset = cursor.int()
|
||||
val transparentTriangleStripCount = cursor.int()
|
||||
val collisionSpherePosition = cursor.vec3Float()
|
||||
val collisionSphereRadius = cursor.float()
|
||||
|
||||
val vertices = mutableListOf<XjVertex>()
|
||||
|
||||
if (vertexInfoCount >= 1) {
|
||||
// TODO: parse all vertex info tables.
|
||||
vertices.addAll(parseVertexInfoTable(cursor, vertexInfoTableOffset))
|
||||
}
|
||||
|
||||
val meshes = mutableListOf<XjMesh>()
|
||||
|
||||
meshes.addAll(
|
||||
parseTriangleStripTable(cursor, triangleStripTableOffset, triangleStripCount),
|
||||
)
|
||||
|
||||
meshes.addAll(
|
||||
parseTriangleStripTable(
|
||||
cursor,
|
||||
transparentTriangleStripTableOffset,
|
||||
transparentTriangleStripCount,
|
||||
),
|
||||
)
|
||||
|
||||
return XjModel(
|
||||
vertices,
|
||||
meshes,
|
||||
collisionSpherePosition,
|
||||
collisionSphereRadius,
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseVertexInfoTable(cursor: Cursor, vertexInfoTableOffset: Int): List<XjVertex> {
|
||||
cursor.seekStart(vertexInfoTableOffset)
|
||||
val vertexType = cursor.short().toInt()
|
||||
cursor.seek(2) // Flags?
|
||||
val vertexTableOffset = cursor.int()
|
||||
val vertexSize = cursor.int()
|
||||
val vertexCount = cursor.int()
|
||||
|
||||
return (0 until vertexCount).map { i ->
|
||||
cursor.seekStart(vertexTableOffset + i * vertexSize)
|
||||
|
||||
val position = cursor.vec3Float()
|
||||
var normal: Vec3? = null
|
||||
var uv: Vec2? = null
|
||||
|
||||
when (vertexType) {
|
||||
2 -> {
|
||||
normal = cursor.vec3Float()
|
||||
}
|
||||
3 -> {
|
||||
normal = cursor.vec3Float()
|
||||
uv = cursor.vec2Float()
|
||||
}
|
||||
4 -> {
|
||||
// Skip 4 bytes.
|
||||
}
|
||||
5 -> {
|
||||
cursor.seek(4)
|
||||
uv = cursor.vec2Float()
|
||||
}
|
||||
6 -> {
|
||||
normal = cursor.vec3Float()
|
||||
// Skip 4 bytes.
|
||||
}
|
||||
7 -> {
|
||||
normal = cursor.vec3Float()
|
||||
uv = cursor.vec2Float()
|
||||
}
|
||||
else -> {
|
||||
logger.warn { "Unknown vertex type $vertexType with size ${vertexSize}." }
|
||||
}
|
||||
}
|
||||
|
||||
XjVertex(
|
||||
position,
|
||||
normal,
|
||||
uv,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseTriangleStripTable(
|
||||
cursor: Cursor,
|
||||
triangle_strip_list_offset: Int,
|
||||
triangle_strip_count: Int,
|
||||
): List<XjMesh> {
|
||||
return (0 until triangle_strip_count).map { i ->
|
||||
cursor.seekStart(triangle_strip_list_offset + i * 20)
|
||||
|
||||
val materialTableOffset = cursor.int()
|
||||
val materialTableSize = cursor.int()
|
||||
val indexListOffset = cursor.int()
|
||||
val indexCount = cursor.int()
|
||||
|
||||
val material = parseTriangleStripMaterial(
|
||||
cursor,
|
||||
materialTableOffset,
|
||||
materialTableSize,
|
||||
)
|
||||
|
||||
cursor.seekStart(indexListOffset)
|
||||
val indices = cursor.uShortArray(indexCount)
|
||||
|
||||
XjMesh(
|
||||
material,
|
||||
indices = List(indexCount) { indices[it].toInt() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseTriangleStripMaterial(
|
||||
cursor: Cursor,
|
||||
offset: Int,
|
||||
size: Int,
|
||||
): XjMaterial {
|
||||
var srcAlpha: Int? = null
|
||||
var dstAlpha: Int? = null
|
||||
var textureId: Int? = null
|
||||
var diffuseR: Int? = null
|
||||
var diffuseG: Int? = null
|
||||
var diffuseB: Int? = null
|
||||
var diffuseA: Int? = null
|
||||
|
||||
for (i in 0 until size) {
|
||||
cursor.seekStart(offset + i * 16)
|
||||
|
||||
when (cursor.int()) {
|
||||
2 -> {
|
||||
srcAlpha = cursor.int()
|
||||
dstAlpha = cursor.int()
|
||||
}
|
||||
3 -> {
|
||||
textureId = cursor.int()
|
||||
}
|
||||
5 -> {
|
||||
diffuseR = cursor.uByte().toInt()
|
||||
diffuseG = cursor.uByte().toInt()
|
||||
diffuseB = cursor.uByte().toInt()
|
||||
diffuseA = cursor.uByte().toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return XjMaterial(
|
||||
srcAlpha,
|
||||
dstAlpha,
|
||||
textureId,
|
||||
diffuseR,
|
||||
diffuseG,
|
||||
diffuseB,
|
||||
diffuseA,
|
||||
)
|
||||
}
|
||||
|
@ -348,7 +348,7 @@ private fun parseSegment(
|
||||
SegmentType.String ->
|
||||
parseStringSegment(offsetToSegment, cursor, endOffset, labels, dcGcFormat)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
} catch (e: Exception) {
|
||||
if (lenient) {
|
||||
logger.error(e) { "Couldn't fully parse byte code segment." }
|
||||
} else {
|
||||
@ -391,7 +391,7 @@ private fun parseInstructionsSegment(
|
||||
try {
|
||||
val args = parseInstructionArguments(cursor, opcode, dcGcFormat)
|
||||
instructions.add(Instruction(opcode, args, null))
|
||||
} catch (e: Throwable) {
|
||||
} catch (e: Exception) {
|
||||
if (lenient) {
|
||||
logger.error(e) {
|
||||
"Exception occurred while parsing arguments for instruction ${opcode.mnemonic}."
|
||||
|
@ -19,6 +19,7 @@ import world.phantasmal.web.core.stores.UiStore
|
||||
import world.phantasmal.web.externals.babylon.Engine
|
||||
import world.phantasmal.web.huntOptimizer.HuntOptimizer
|
||||
import world.phantasmal.web.questEditor.QuestEditor
|
||||
import world.phantasmal.web.viewer.Viewer
|
||||
import world.phantasmal.webui.DisposableContainer
|
||||
import world.phantasmal.webui.dom.disposableListener
|
||||
|
||||
@ -47,8 +48,8 @@ class Application(
|
||||
val uiStore = addDisposable(UiStore(scope, applicationUrl))
|
||||
|
||||
// Controllers.
|
||||
val navigationController = addDisposable(NavigationController(scope, uiStore))
|
||||
val mainContentController = addDisposable(MainContentController(scope, uiStore))
|
||||
val navigationController = addDisposable(NavigationController(uiStore))
|
||||
val mainContentController = addDisposable(MainContentController(uiStore))
|
||||
|
||||
// Initialize application view.
|
||||
val applicationWidget = addDisposable(
|
||||
@ -56,18 +57,24 @@ class Application(
|
||||
scope,
|
||||
NavigationWidget(scope, navigationController),
|
||||
MainContentWidget(scope, mainContentController, mapOf(
|
||||
PwTool.Viewer to { widgetScope ->
|
||||
addDisposable(Viewer(
|
||||
widgetScope,
|
||||
createEngine,
|
||||
)).createWidget()
|
||||
},
|
||||
PwTool.QuestEditor to { widgetScope ->
|
||||
addDisposable(QuestEditor(
|
||||
widgetScope,
|
||||
assetLoader,
|
||||
createEngine
|
||||
createEngine,
|
||||
)).createWidget()
|
||||
},
|
||||
PwTool.HuntOptimizer to { widgetScope ->
|
||||
addDisposable(HuntOptimizer(
|
||||
widgetScope,
|
||||
assetLoader,
|
||||
uiStore
|
||||
uiStore,
|
||||
)).createWidget()
|
||||
},
|
||||
))
|
||||
|
@ -1,11 +1,10 @@
|
||||
package world.phantasmal.web.application.controllers
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.web.core.stores.PwTool
|
||||
import world.phantasmal.web.core.stores.UiStore
|
||||
import world.phantasmal.webui.controllers.Controller
|
||||
|
||||
class MainContentController(scope: CoroutineScope, uiStore: UiStore) : Controller(scope) {
|
||||
class MainContentController(uiStore: UiStore) : Controller() {
|
||||
val tools: Map<PwTool, Val<Boolean>> = uiStore.toolToActive
|
||||
}
|
||||
|
@ -1,15 +1,11 @@
|
||||
package world.phantasmal.web.application.controllers
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.web.core.stores.PwTool
|
||||
import world.phantasmal.web.core.stores.UiStore
|
||||
import world.phantasmal.webui.controllers.Controller
|
||||
|
||||
class NavigationController(
|
||||
scope: CoroutineScope,
|
||||
private val uiStore: UiStore,
|
||||
) : Controller(scope) {
|
||||
class NavigationController(private val uiStore: UiStore) : Controller() {
|
||||
val tools: Map<PwTool, Val<Boolean>> = uiStore.toolToActive
|
||||
|
||||
fun setCurrentTool(tool: PwTool) {
|
||||
|
@ -1,6 +1,5 @@
|
||||
package world.phantasmal.web.core.controllers
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import world.phantasmal.web.core.stores.PwTool
|
||||
import world.phantasmal.web.core.stores.UiStore
|
||||
import world.phantasmal.webui.controllers.Tab
|
||||
@ -9,11 +8,10 @@ import world.phantasmal.webui.controllers.TabController
|
||||
open class PathAwareTab(override val title: String, val path: String) : Tab
|
||||
|
||||
open class PathAwareTabController<T : PathAwareTab>(
|
||||
scope: CoroutineScope,
|
||||
private val uiStore: UiStore,
|
||||
private val tool: PwTool,
|
||||
tabs: List<T>,
|
||||
) : TabController<T>(scope, tabs) {
|
||||
) : TabController<T>(tabs) {
|
||||
init {
|
||||
observe(uiStore.path) { path ->
|
||||
if (uiStore.currentTool.value == tool) {
|
||||
|
@ -1,29 +1,46 @@
|
||||
package world.phantasmal.web.core.rendering
|
||||
|
||||
import mu.KotlinLogging
|
||||
import org.w3c.dom.HTMLCanvasElement
|
||||
import world.phantasmal.core.disposable.TrackedDisposable
|
||||
import world.phantasmal.web.externals.babylon.Engine
|
||||
import world.phantasmal.web.externals.babylon.Scene
|
||||
import world.phantasmal.web.externals.babylon.*
|
||||
import world.phantasmal.webui.DisposableContainer
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
abstract class Renderer(
|
||||
protected val canvas: HTMLCanvasElement,
|
||||
protected val engine: Engine,
|
||||
) : TrackedDisposable() {
|
||||
) : DisposableContainer() {
|
||||
protected val scene = Scene(engine)
|
||||
private val light = HemisphericLight("Light", Vector3(-1.0, 1.0, 0.0), scene)
|
||||
protected abstract val camera: Camera
|
||||
|
||||
init {
|
||||
engine.runRenderLoop {
|
||||
scene.render()
|
||||
scene.clearColor = Color4(0.09, 0.09, 0.09, 1.0)
|
||||
}
|
||||
|
||||
fun startRendering() {
|
||||
logger.trace { "${this::class.simpleName} - start rendering." }
|
||||
engine.runRenderLoop(::render)
|
||||
}
|
||||
|
||||
fun stopRendering() {
|
||||
logger.trace { "${this::class.simpleName} - stop rendering." }
|
||||
engine.stopRenderLoop()
|
||||
}
|
||||
|
||||
override fun internalDispose() {
|
||||
camera.dispose()
|
||||
light.dispose()
|
||||
scene.dispose()
|
||||
engine.dispose()
|
||||
super.internalDispose()
|
||||
}
|
||||
|
||||
fun scheduleRender() {
|
||||
// TODO: Remove scheduleRender?
|
||||
private fun render() {
|
||||
val lightDirection = Vector3(-1.0, 1.0, 0.0)
|
||||
lightDirection.rotateByQuaternionToRef(camera.absoluteRotation, lightDirection)
|
||||
light.direction = lightDirection
|
||||
scene.render()
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import mu.KotlinLogging
|
||||
import world.phantasmal.lib.fileFormats.Vec3
|
||||
import world.phantasmal.lib.fileFormats.ninja.NinjaModel
|
||||
import world.phantasmal.lib.fileFormats.ninja.NinjaObject
|
||||
import world.phantasmal.lib.fileFormats.ninja.NjcmModel
|
||||
import world.phantasmal.lib.fileFormats.ninja.NjModel
|
||||
import world.phantasmal.lib.fileFormats.ninja.XjModel
|
||||
import world.phantasmal.web.externals.babylon.*
|
||||
import kotlin.math.cos
|
||||
@ -40,7 +40,7 @@ private class NinjaToVertexDataConverter(private val builder: VertexDataBuilder)
|
||||
if (ef.noTranslate) NO_TRANSLATION else vec3ToBabylon(obj.position),
|
||||
)
|
||||
|
||||
parentMatrix.multiplyToRef(matrix, matrix)
|
||||
matrix.multiplyToRef(parentMatrix, matrix)
|
||||
|
||||
if (!ef.hidden) {
|
||||
obj.model?.let { model ->
|
||||
@ -59,11 +59,11 @@ private class NinjaToVertexDataConverter(private val builder: VertexDataBuilder)
|
||||
|
||||
private fun modelToVertexData(model: NinjaModel, matrix: Matrix) =
|
||||
when (model) {
|
||||
is NjcmModel -> njcmModelToVertexData(model, matrix)
|
||||
is NjModel -> njModelToVertexData(model, matrix)
|
||||
is XjModel -> xjModelToVertexData(model, matrix)
|
||||
}
|
||||
|
||||
private fun njcmModelToVertexData(model: NjcmModel, matrix: Matrix) {
|
||||
private fun njModelToVertexData(model: NjModel, matrix: Matrix) {
|
||||
val normalMatrix = Matrix.Identity()
|
||||
matrix.toNormalMatrix(normalMatrix)
|
||||
|
||||
@ -93,7 +93,7 @@ private class NinjaToVertexDataConverter(private val builder: VertexDataBuilder)
|
||||
var i = 0
|
||||
|
||||
for (meshVertex in mesh.vertices) {
|
||||
val vertices = vertexHolder.get(meshVertex.index.toInt())
|
||||
val vertices = vertexHolder.get(meshVertex.index)
|
||||
|
||||
if (vertices.isEmpty()) {
|
||||
logger.debug {
|
||||
@ -112,7 +112,7 @@ private class NinjaToVertexDataConverter(private val builder: VertexDataBuilder)
|
||||
)
|
||||
|
||||
if (i >= 2) {
|
||||
if (i % 2 == if (mesh.clockwiseWinding) 1 else 0) {
|
||||
if (i % 2 == if (mesh.clockwiseWinding) 0 else 1) {
|
||||
builder.addIndex(index - 2)
|
||||
builder.addIndex(index - 1)
|
||||
builder.addIndex(index)
|
||||
@ -151,7 +151,87 @@ private class NinjaToVertexDataConverter(private val builder: VertexDataBuilder)
|
||||
}
|
||||
}
|
||||
|
||||
private fun xjModelToVertexData(model: XjModel, matrix: Matrix) {}
|
||||
private fun xjModelToVertexData(model: XjModel, matrix: Matrix) {
|
||||
val indexOffset = builder.vertexCount
|
||||
val normalMatrix = Matrix.Identity()
|
||||
matrix.toNormalMatrix(normalMatrix)
|
||||
|
||||
for (vertex in model.vertices) {
|
||||
val p = vec3ToBabylon(vertex.position)
|
||||
Vector3.TransformCoordinatesToRef(p, matrix, p)
|
||||
|
||||
val n = vertex.normal?.let(::vec3ToBabylon) ?: Vector3.Up()
|
||||
Vector3.TransformCoordinatesToRef(n, normalMatrix, n)
|
||||
|
||||
val uv = vertex.uv?.let(::vec2ToBabylon) ?: DEFAULT_UV
|
||||
|
||||
builder.addVertex(p, n, uv)
|
||||
}
|
||||
|
||||
var currentMatIdx: Int? = null
|
||||
var currentSrcAlpha: Int? = null
|
||||
var currentDstAlpha: Int? = null
|
||||
|
||||
for (mesh in model.meshes) {
|
||||
val startIndexCount = builder.indexCount
|
||||
var clockwise = true
|
||||
|
||||
for (j in 2 until mesh.indices.size) {
|
||||
val a = indexOffset + mesh.indices[j - 2]
|
||||
val b = indexOffset + mesh.indices[j - 1]
|
||||
val c = indexOffset + mesh.indices[j]
|
||||
val pa = builder.getPosition(a)
|
||||
val pb = builder.getPosition(b)
|
||||
val pc = builder.getPosition(c)
|
||||
val na = builder.getNormal(a)
|
||||
val nb = builder.getNormal(b)
|
||||
val nc = builder.getNormal(c)
|
||||
|
||||
// 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
|
||||
// most models.
|
||||
val normal = pb.subtract(pa).cross(pc.subtract(pa))
|
||||
|
||||
if (!clockwise) {
|
||||
normal.negateInPlace()
|
||||
}
|
||||
|
||||
val oppositeCount =
|
||||
(if (Vector3.Dot(normal, na) < 0) 1 else 0) +
|
||||
(if (Vector3.Dot(normal, nb) < 0) 1 else 0) +
|
||||
(if (Vector3.Dot(normal, nc) < 0) 1 else 0)
|
||||
|
||||
if (oppositeCount >= 2) {
|
||||
clockwise = !clockwise
|
||||
}
|
||||
|
||||
if (clockwise) {
|
||||
builder.addIndex(b)
|
||||
builder.addIndex(a)
|
||||
builder.addIndex(c)
|
||||
} else {
|
||||
builder.addIndex(a)
|
||||
builder.addIndex(b)
|
||||
builder.addIndex(c)
|
||||
}
|
||||
|
||||
clockwise = !clockwise
|
||||
}
|
||||
|
||||
mesh.material.textureId?.let { currentMatIdx = it }
|
||||
mesh.material.srcAlpha?.let { currentSrcAlpha = it }
|
||||
mesh.material.dstAlpha?.let { currentDstAlpha = it }
|
||||
|
||||
// TODO: support multiple materials
|
||||
// builder.addGroup(
|
||||
// start_index_count,
|
||||
// this.builder.index_count - start_index_count,
|
||||
// current_mat_idx,
|
||||
// true,
|
||||
// current_src_alpha !== 4 || current_dst_alpha !== 5,
|
||||
// );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class Vertex(
|
||||
@ -164,21 +244,21 @@ private class Vertex(
|
||||
)
|
||||
|
||||
private class VertexHolder {
|
||||
private val stack = mutableListOf<MutableList<Vertex>>()
|
||||
private val buffer = mutableListOf<MutableList<Vertex>>()
|
||||
|
||||
fun add(vertices: List<Vertex?>) {
|
||||
vertices.forEachIndexed { i, vertex ->
|
||||
if (i >= stack.size) {
|
||||
stack.add(mutableListOf())
|
||||
if (i >= buffer.size) {
|
||||
buffer.add(mutableListOf())
|
||||
}
|
||||
|
||||
if (vertex != null) {
|
||||
stack[i].add(vertex)
|
||||
buffer[i].add(vertex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun get(index: Int): List<Vertex> = stack[index]
|
||||
fun get(index: Int): List<Vertex> = buffer[index]
|
||||
}
|
||||
|
||||
private fun eulerToQuat(angles: Vec3, zxyRotationOrder: Boolean): Quaternion {
|
||||
|
@ -21,6 +21,12 @@ class VertexDataBuilder {
|
||||
val indexCount: Int
|
||||
get() = indices.size
|
||||
|
||||
fun getPosition(index: Int): Vector3 =
|
||||
positions[index]
|
||||
|
||||
fun getNormal(index: Int): Vector3 =
|
||||
normals[index]
|
||||
|
||||
fun addVertex(position: Vector3, normal: Vector3, uv: Vector2) {
|
||||
positions.add(position)
|
||||
normals.add(normal)
|
||||
@ -48,6 +54,9 @@ class VertexDataBuilder {
|
||||
// }
|
||||
|
||||
fun build(): VertexData {
|
||||
check(this.positions.size == this.normals.size)
|
||||
check(this.positions.size == this.uvs.size)
|
||||
|
||||
val positions = Float32Array(3 * positions.size)
|
||||
val normals = Float32Array(3 * normals.size)
|
||||
val uvs = Float32Array(2 * uvs.size)
|
||||
|
@ -12,13 +12,23 @@ class RendererWidget(
|
||||
scope: CoroutineScope,
|
||||
private val createRenderer: (HTMLCanvasElement) -> Renderer,
|
||||
) : Widget(scope) {
|
||||
private var renderer: Renderer? = null
|
||||
|
||||
override fun Node.createElement() =
|
||||
canvas {
|
||||
className = "pw-core-renderer"
|
||||
tabIndex = -1
|
||||
|
||||
observeResize()
|
||||
addDisposable(createRenderer(this))
|
||||
renderer = addDisposable(createRenderer(this))
|
||||
|
||||
observe(selfOrAncestorHidden) { hidden ->
|
||||
if (hidden) {
|
||||
renderer?.stopRendering()
|
||||
} else {
|
||||
renderer?.startRendering()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun resized(width: Double, height: Double) {
|
||||
|
@ -1,6 +1,6 @@
|
||||
@file:JsModule("@babylonjs/core")
|
||||
@file:JsNonModule
|
||||
@file:Suppress("FunctionName", "unused")
|
||||
@file:Suppress("FunctionName", "unused", "CovariantEquals")
|
||||
|
||||
package world.phantasmal.web.externals.babylon
|
||||
|
||||
@ -13,13 +13,17 @@ external class Vector2(x: Double, y: Double) {
|
||||
var y: Double
|
||||
|
||||
fun addInPlace(otherVector: Vector2): Vector2
|
||||
|
||||
fun addInPlaceFromFloats(x: Double, y: Double): Vector2
|
||||
|
||||
fun subtract(otherVector: Vector2): Vector2
|
||||
fun negate(): Vector2
|
||||
fun negateInPlace(): Vector2
|
||||
fun clone(): Vector2
|
||||
fun copyFrom(source: Vector2): Vector2
|
||||
fun equals(otherVector: Vector2): Boolean
|
||||
|
||||
companion object {
|
||||
fun Zero(): Vector2
|
||||
fun Dot(left: Vector2, right: Vector2): Double
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,17 +33,22 @@ external class Vector3(x: Double, y: Double, z: Double) {
|
||||
var z: Double
|
||||
|
||||
fun toQuaternion(): Quaternion
|
||||
|
||||
fun addInPlace(otherVector: Vector3): Vector3
|
||||
|
||||
fun addInPlaceFromFloats(x: Double, y: Double, z: Double): Vector3
|
||||
|
||||
fun subtract(otherVector: Vector3): Vector3
|
||||
fun negate(): Vector3
|
||||
fun negateInPlace(): Vector3
|
||||
fun cross(other: Vector3): Vector3
|
||||
fun rotateByQuaternionToRef(quaternion: Quaternion, result: Vector3): Vector3
|
||||
fun clone(): Vector3
|
||||
fun copyFrom(source: Vector3): Vector3
|
||||
fun equals(otherVector: Vector3): Boolean
|
||||
|
||||
companion object {
|
||||
fun One(): Vector3
|
||||
fun Up(): Vector3
|
||||
fun Zero(): Vector3
|
||||
fun Dot(left: Vector3, right: Vector3): Double
|
||||
fun TransformCoordinates(vector: Vector3, transformation: Matrix): Vector3
|
||||
fun TransformCoordinatesToRef(vector: Vector3, transformation: Matrix, result: Vector3)
|
||||
fun TransformNormal(vector: Vector3, transformation: Matrix): Vector3
|
||||
@ -71,6 +80,9 @@ external class Quaternion(
|
||||
*/
|
||||
fun multiplyToRef(q1: Quaternion, result: Quaternion): Quaternion
|
||||
|
||||
fun clone(): Quaternion
|
||||
fun copyFrom(other: Quaternion): Quaternion
|
||||
|
||||
companion object {
|
||||
fun Identity(): Quaternion
|
||||
fun FromEulerAngles(x: Double, y: Double, z: Double): Quaternion
|
||||
@ -82,6 +94,8 @@ external class Matrix {
|
||||
fun multiply(other: Matrix): Matrix
|
||||
fun multiplyToRef(other: Matrix, result: Matrix): Matrix
|
||||
fun toNormalMatrix(ref: Matrix)
|
||||
fun copyFrom(other: Matrix): Matrix
|
||||
fun equals(value: Matrix): Boolean
|
||||
|
||||
companion object {
|
||||
fun Identity(): Matrix
|
||||
@ -89,6 +103,27 @@ external class Matrix {
|
||||
}
|
||||
}
|
||||
|
||||
external class EventState
|
||||
|
||||
external class Observable<T> {
|
||||
fun add(
|
||||
callback: (eventData: T, eventState: EventState) -> Unit,
|
||||
mask: Int = definedExternally,
|
||||
insertFirst: Boolean = definedExternally,
|
||||
scope: Any = definedExternally,
|
||||
unregisterOnFirstCall: Boolean = definedExternally,
|
||||
): Observer<T>?
|
||||
|
||||
fun remove(observer: Observer<T>?): Boolean
|
||||
|
||||
fun removeCallback(
|
||||
callback: (eventData: T, eventState: EventState) -> Unit,
|
||||
scope: Any = definedExternally,
|
||||
): Boolean
|
||||
}
|
||||
|
||||
external class Observer<T>
|
||||
|
||||
open external class ThinEngine {
|
||||
val description: String
|
||||
|
||||
@ -98,6 +133,13 @@ open external class ThinEngine {
|
||||
*/
|
||||
fun runRenderLoop(renderFunction: () -> Unit)
|
||||
|
||||
/**
|
||||
* stop executing a render loop function and remove it from the execution array
|
||||
* @param renderFunction defines the function to be removed. If not provided all functions will
|
||||
* be removed.
|
||||
*/
|
||||
fun stopRenderLoop(renderFunction: () -> Unit = definedExternally)
|
||||
|
||||
fun dispose()
|
||||
}
|
||||
|
||||
@ -107,6 +149,8 @@ external class Engine(
|
||||
) : ThinEngine
|
||||
|
||||
external class Scene(engine: Engine) {
|
||||
var clearColor: Color4
|
||||
|
||||
fun render()
|
||||
fun addLight(light: Light)
|
||||
fun addMesh(newMesh: AbstractMesh, recursive: Boolean? = definedExternally)
|
||||
@ -120,11 +164,11 @@ external class Scene(engine: Engine) {
|
||||
open external class Node {
|
||||
var metadata: Any?
|
||||
var parent: Node?
|
||||
var position: Vector3
|
||||
var rotation: Vector3
|
||||
var scaling: Vector3
|
||||
|
||||
fun setEnabled(value: Boolean)
|
||||
fun getViewMatrix(force: Boolean = definedExternally): Matrix
|
||||
fun getProjectionMatrix(force: Boolean = definedExternally): Matrix
|
||||
fun getTransformationMatrix(): Matrix
|
||||
|
||||
/**
|
||||
* Releases resources associated with this node.
|
||||
@ -138,6 +182,11 @@ open external class Node {
|
||||
}
|
||||
|
||||
open external class Camera : Node {
|
||||
val absoluteRotation: Quaternion
|
||||
val onProjectionMatrixChangedObservable: Observable<Camera>
|
||||
val onViewMatrixChangedObservable: Observable<Camera>
|
||||
val onAfterCheckInputsObservable: Observable<Camera>
|
||||
|
||||
fun attachControl(noPreventDefault: Boolean = definedExternally)
|
||||
}
|
||||
|
||||
@ -174,16 +223,25 @@ external class ArcRotateCamera(
|
||||
|
||||
abstract external class Light : Node
|
||||
|
||||
external class HemisphericLight(name: String, direction: Vector3, scene: Scene) : Light
|
||||
external class HemisphericLight(name: String, direction: Vector3, scene: Scene) : Light {
|
||||
var direction: Vector3
|
||||
}
|
||||
|
||||
open external class TransformNode(
|
||||
name: String,
|
||||
scene: Scene? = definedExternally,
|
||||
isPure: Boolean = definedExternally,
|
||||
) : Node {
|
||||
var position: Vector3
|
||||
var rotation: Vector3
|
||||
var rotationQuaternion: Quaternion?
|
||||
val absoluteRotation: Quaternion
|
||||
var scaling: Vector3
|
||||
}
|
||||
|
||||
abstract external class AbstractMesh : TransformNode
|
||||
abstract external class AbstractMesh : TransformNode {
|
||||
fun getBoundingInfo(): BoundingInfo
|
||||
}
|
||||
|
||||
external class Mesh(
|
||||
name: String,
|
||||
@ -198,6 +256,34 @@ external class Mesh(
|
||||
|
||||
external class InstancedMesh : AbstractMesh
|
||||
|
||||
external class BoundingInfo {
|
||||
val boundingBox: BoundingBox
|
||||
val boundingSphere: BoundingSphere
|
||||
}
|
||||
|
||||
external class BoundingBox {
|
||||
val center: Vector3
|
||||
val centerWorld: Vector3
|
||||
val directions: Array<Vector3>
|
||||
val extendSize: Vector3
|
||||
val extendSizeWorld: Vector3
|
||||
val maximum: Vector3
|
||||
val maximumWorld: Vector3
|
||||
val minimum: Vector3
|
||||
val minimumWorld: Vector3
|
||||
val vectors: Array<Vector3>
|
||||
val vectorsWorld: Array<Vector3>
|
||||
}
|
||||
|
||||
external class BoundingSphere {
|
||||
val center: Vector3
|
||||
val centerWorld: Vector3
|
||||
val maximum: Vector3
|
||||
val minimum: Vector3
|
||||
val radius: Double
|
||||
val radiusWorld: Double
|
||||
}
|
||||
|
||||
external class MeshBuilder {
|
||||
companion object {
|
||||
interface CreateCylinderOptions {
|
||||
@ -226,3 +312,25 @@ external class VertexData {
|
||||
|
||||
fun applyToMesh(mesh: Mesh, updatable: Boolean = definedExternally): VertexData
|
||||
}
|
||||
|
||||
external class Color3(
|
||||
r: Double = definedExternally,
|
||||
g: Double = definedExternally,
|
||||
b: Double = definedExternally,
|
||||
) {
|
||||
var r: Double
|
||||
var g: Double
|
||||
var b: Double
|
||||
}
|
||||
|
||||
external class Color4(
|
||||
r: Double = definedExternally,
|
||||
g: Double = definedExternally,
|
||||
b: Double = definedExternally,
|
||||
a: Double = definedExternally,
|
||||
) {
|
||||
var r: Double
|
||||
var g: Double
|
||||
var b: Double
|
||||
var a: Double
|
||||
}
|
||||
|
@ -18,9 +18,9 @@ class HuntOptimizer(
|
||||
) : DisposableContainer() {
|
||||
private val huntMethodStore = addDisposable(HuntMethodStore(scope, uiStore, assetLoader))
|
||||
|
||||
private val huntOptimizerController = addDisposable(HuntOptimizerController(scope, uiStore))
|
||||
private val huntOptimizerController = addDisposable(HuntOptimizerController(uiStore))
|
||||
private val methodsController =
|
||||
addDisposable(MethodsController(scope, uiStore, huntMethodStore))
|
||||
addDisposable(MethodsController(uiStore, huntMethodStore))
|
||||
|
||||
fun createWidget(): Widget =
|
||||
HuntOptimizerWidget(
|
||||
|
@ -1,15 +1,13 @@
|
||||
package world.phantasmal.web.huntOptimizer.controllers
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import world.phantasmal.web.core.controllers.PathAwareTab
|
||||
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.huntOptimizer.HuntOptimizerUrls
|
||||
|
||||
class HuntOptimizerController(scope: CoroutineScope, uiStore: UiStore) :
|
||||
class HuntOptimizerController(uiStore: UiStore) :
|
||||
PathAwareTabController<PathAwareTab>(
|
||||
scope,
|
||||
uiStore,
|
||||
PwTool.HuntOptimizer,
|
||||
listOf(
|
||||
|
@ -1,6 +1,5 @@
|
||||
package world.phantasmal.web.huntOptimizer.controllers
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import world.phantasmal.lib.fileFormats.quest.Episode
|
||||
import world.phantasmal.observable.value.list.ListVal
|
||||
import world.phantasmal.observable.value.list.MutableListVal
|
||||
@ -16,11 +15,9 @@ import world.phantasmal.web.huntOptimizer.stores.HuntMethodStore
|
||||
class MethodsTab(title: String, path: String, val episode: Episode) : PathAwareTab(title, path)
|
||||
|
||||
class MethodsController(
|
||||
scope: CoroutineScope,
|
||||
uiStore: UiStore,
|
||||
huntMethodStore: HuntMethodStore,
|
||||
) : PathAwareTabController<MethodsTab>(
|
||||
scope,
|
||||
uiStore,
|
||||
PwTool.HuntOptimizer,
|
||||
listOf(
|
||||
|
@ -29,9 +29,9 @@ class QuestEditor(
|
||||
|
||||
// Controllers
|
||||
private val toolbarController =
|
||||
addDisposable(QuestEditorToolbarController(scope, questLoader, questEditorStore))
|
||||
private val questInfoController = addDisposable(QuestInfoController(scope, questEditorStore))
|
||||
private val npcCountsController = addDisposable(NpcCountsController(scope, questEditorStore))
|
||||
addDisposable(QuestEditorToolbarController(questLoader, questEditorStore))
|
||||
private val questInfoController = addDisposable(QuestInfoController(questEditorStore))
|
||||
private val npcCountsController = addDisposable(NpcCountsController(questEditorStore))
|
||||
|
||||
fun createWidget(): Widget =
|
||||
QuestEditorWidget(
|
||||
|
@ -1,6 +1,5 @@
|
||||
package world.phantasmal.web.questEditor.controllers
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import world.phantasmal.lib.fileFormats.quest.NpcType
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.list.emptyListVal
|
||||
@ -8,7 +7,7 @@ import world.phantasmal.web.questEditor.models.QuestNpcModel
|
||||
import world.phantasmal.web.questEditor.stores.QuestEditorStore
|
||||
import world.phantasmal.webui.controllers.Controller
|
||||
|
||||
class NpcCountsController(scope: CoroutineScope, store: QuestEditorStore) : Controller(scope) {
|
||||
class NpcCountsController(store: QuestEditorStore) : Controller() {
|
||||
val unavailable: Val<Boolean> = store.currentQuest.map { it == null }
|
||||
|
||||
val npcCounts: Val<List<NameWithCount>> = store.currentQuest
|
||||
|
@ -1,6 +1,5 @@
|
||||
package world.phantasmal.web.questEditor.controllers
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import mu.KotlinLogging
|
||||
import org.w3c.files.File
|
||||
import world.phantasmal.core.*
|
||||
@ -21,10 +20,9 @@ import world.phantasmal.webui.readFile
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
class QuestEditorToolbarController(
|
||||
scope: CoroutineScope,
|
||||
private val questLoader: QuestLoader,
|
||||
private val questEditorStore: QuestEditorStore,
|
||||
) : Controller(scope) {
|
||||
) : Controller() {
|
||||
private val _resultDialogVisible = mutableVal(false)
|
||||
private val _result = mutableVal<PwResult<*>?>(null)
|
||||
|
||||
@ -72,7 +70,7 @@ class QuestEditorToolbarController(
|
||||
setCurrentQuest(parseResult.value)
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
} catch (e: Exception) {
|
||||
setResult(
|
||||
PwResult.build<Nothing>(logger)
|
||||
.addProblem(Severity.Error, "Couldn't parse file.", cause = e)
|
||||
|
@ -1,12 +1,11 @@
|
||||
package world.phantasmal.web.questEditor.controllers
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.value
|
||||
import world.phantasmal.web.questEditor.stores.QuestEditorStore
|
||||
import world.phantasmal.webui.controllers.Controller
|
||||
|
||||
class QuestInfoController(scope: CoroutineScope, store: QuestEditorStore) : Controller(scope) {
|
||||
class QuestInfoController(store: QuestEditorStore) : Controller() {
|
||||
val unavailable: Val<Boolean> = store.currentQuest.map { it == null }
|
||||
val disabled: Val<Boolean> = store.questEditingDisabled
|
||||
|
||||
|
@ -54,7 +54,7 @@ class EntityAssetLoader(
|
||||
mesh
|
||||
}
|
||||
} ?: defaultMesh
|
||||
} catch (e: Throwable) {
|
||||
} catch (e: Exception) {
|
||||
logger.error(e) { "Couldn't load mesh for $type (model: $model)." }
|
||||
defaultMesh
|
||||
}
|
||||
|
@ -102,7 +102,6 @@ class EntityMeshManager(
|
||||
val disposer = Disposer(
|
||||
entity.worldPosition.observe { (pos) ->
|
||||
mesh.position = pos
|
||||
renderer.scheduleRender()
|
||||
},
|
||||
|
||||
// TODO: Rotation.
|
||||
@ -126,7 +125,6 @@ class EntityMeshManager(
|
||||
}
|
||||
.observe(callNow = true) { (visible) ->
|
||||
mesh.setEnabled(visible)
|
||||
renderer.scheduleRender()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -15,8 +15,8 @@ class QuestRenderer(
|
||||
private val meshManager = createMeshManager(this, scene)
|
||||
private var entityMeshes = TransformNode("Entities", scene)
|
||||
private val entityToMesh = mutableMapOf<QuestEntityModel<*, *>, AbstractMesh>()
|
||||
private val camera = ArcRotateCamera("Camera", 0.0, PI / 6, 500.0, Vector3.Zero(), scene)
|
||||
private val light = HemisphericLight("Light", Vector3(1.0, 1.0, 0.0), scene)
|
||||
|
||||
override val camera = ArcRotateCamera("Camera", 0.0, PI / 6, 500.0, Vector3.Zero(), scene)
|
||||
|
||||
init {
|
||||
with(camera) {
|
||||
@ -41,8 +41,6 @@ class QuestRenderer(
|
||||
meshManager.dispose()
|
||||
entityMeshes.dispose()
|
||||
entityToMesh.clear()
|
||||
camera.dispose()
|
||||
light.dispose()
|
||||
super.internalDispose()
|
||||
}
|
||||
|
||||
@ -51,7 +49,6 @@ class QuestRenderer(
|
||||
entityToMesh.clear()
|
||||
|
||||
entityMeshes = TransformNode("Entities", scene)
|
||||
scheduleRender()
|
||||
}
|
||||
|
||||
fun addEntityMesh(mesh: AbstractMesh) {
|
||||
@ -69,15 +66,12 @@ class QuestRenderer(
|
||||
// if (entity === this.selected_entity) {
|
||||
// this.mark_selected(model)
|
||||
// }
|
||||
|
||||
this.scheduleRender()
|
||||
}
|
||||
|
||||
fun removeEntityMesh(entity: QuestEntityModel<*, *>) {
|
||||
entityToMesh.remove(entity)?.let { mesh ->
|
||||
mesh.parent = null
|
||||
mesh.dispose()
|
||||
this.scheduleRender()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
29
web/src/main/kotlin/world/phantasmal/web/viewer/Viewer.kt
Normal file
29
web/src/main/kotlin/world/phantasmal/web/viewer/Viewer.kt
Normal file
@ -0,0 +1,29 @@
|
||||
package world.phantasmal.web.viewer
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.w3c.dom.HTMLCanvasElement
|
||||
import world.phantasmal.web.externals.babylon.Engine
|
||||
import world.phantasmal.web.viewer.controller.ViewerToolbarController
|
||||
import world.phantasmal.web.viewer.rendering.MeshRenderer
|
||||
import world.phantasmal.web.viewer.store.ViewerStore
|
||||
import world.phantasmal.web.viewer.widgets.ViewerToolbar
|
||||
import world.phantasmal.web.viewer.widgets.ViewerWidget
|
||||
import world.phantasmal.webui.DisposableContainer
|
||||
import world.phantasmal.webui.widgets.Widget
|
||||
|
||||
class Viewer(
|
||||
private val scope: CoroutineScope,
|
||||
private val createEngine: (HTMLCanvasElement) -> Engine,
|
||||
) : DisposableContainer() {
|
||||
// Stores
|
||||
private val viewerStore = addDisposable(ViewerStore(scope))
|
||||
|
||||
// Controllers
|
||||
private val viewerToolbarController = addDisposable(ViewerToolbarController(viewerStore))
|
||||
|
||||
fun createWidget(): Widget =
|
||||
ViewerWidget(scope, ViewerToolbar(scope, viewerToolbarController), ::createViewerRenderer)
|
||||
|
||||
private fun createViewerRenderer(canvas: HTMLCanvasElement): MeshRenderer =
|
||||
MeshRenderer(viewerStore, canvas, createEngine(canvas))
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
package world.phantasmal.web.viewer.controller
|
||||
|
||||
import mu.KotlinLogging
|
||||
import org.w3c.files.File
|
||||
import world.phantasmal.core.PwResult
|
||||
import world.phantasmal.core.Severity
|
||||
import world.phantasmal.core.Success
|
||||
import world.phantasmal.lib.Endianness
|
||||
import world.phantasmal.lib.cursor.cursor
|
||||
import world.phantasmal.lib.fileFormats.ninja.parseNj
|
||||
import world.phantasmal.lib.fileFormats.ninja.parseXj
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.mutableVal
|
||||
import world.phantasmal.web.viewer.store.ViewerStore
|
||||
import world.phantasmal.webui.controllers.Controller
|
||||
import world.phantasmal.webui.readFile
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
class ViewerToolbarController(private val store: ViewerStore) : Controller() {
|
||||
private val _resultDialogVisible = mutableVal(false)
|
||||
private val _result = mutableVal<PwResult<*>?>(null)
|
||||
|
||||
val resultDialogVisible: Val<Boolean> = _resultDialogVisible
|
||||
val result: Val<PwResult<*>?> = _result
|
||||
|
||||
suspend fun openFiles(files: List<File>) {
|
||||
var modelFileFound = false
|
||||
val result = PwResult.build<Nothing>(logger)
|
||||
|
||||
try {
|
||||
for (file in files) {
|
||||
if (file.name.endsWith(".nj", ignoreCase = true)) {
|
||||
if (modelFileFound) continue
|
||||
|
||||
modelFileFound = true
|
||||
val njResult = parseNj(readFile(file).cursor(Endianness.Little))
|
||||
result.addResult(njResult)
|
||||
|
||||
if (njResult is Success) {
|
||||
store.setCurrentNinjaObject(njResult.value.firstOrNull())
|
||||
}
|
||||
} else if (file.name.endsWith(".xj", ignoreCase = true)) {
|
||||
if (modelFileFound) continue
|
||||
|
||||
modelFileFound = true
|
||||
val xjResult = parseXj(readFile(file).cursor(Endianness.Little))
|
||||
result.addResult(xjResult)
|
||||
|
||||
if (xjResult is Success) {
|
||||
store.setCurrentNinjaObject(xjResult.value.firstOrNull())
|
||||
}
|
||||
} else {
|
||||
result.addProblem(
|
||||
Severity.Error,
|
||||
"""File "${file.name}" has an unsupported file type."""
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
result.addProblem(Severity.Error, "Couldn't parse files.", cause = e)
|
||||
}
|
||||
|
||||
// Set failure result, because setResult doesn't care about the type.
|
||||
setResult(result.failure())
|
||||
}
|
||||
|
||||
private fun setResult(result: PwResult<*>) {
|
||||
_result.value = result
|
||||
|
||||
if (result.problems.isNotEmpty()) {
|
||||
_resultDialogVisible.value = true
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
package world.phantasmal.web.viewer.rendering
|
||||
|
||||
import org.w3c.dom.HTMLCanvasElement
|
||||
import world.phantasmal.lib.fileFormats.ninja.NinjaObject
|
||||
import world.phantasmal.web.core.rendering.Renderer
|
||||
import world.phantasmal.web.core.rendering.conversion.ninjaObjectToVertexData
|
||||
import world.phantasmal.web.externals.babylon.*
|
||||
import world.phantasmal.web.viewer.store.ViewerStore
|
||||
import kotlin.math.PI
|
||||
|
||||
class MeshRenderer(
|
||||
store: ViewerStore,
|
||||
canvas: HTMLCanvasElement,
|
||||
engine: Engine,
|
||||
) : Renderer(canvas, engine) {
|
||||
private var mesh: Mesh? = null
|
||||
|
||||
override val camera = ArcRotateCamera("Camera", 0.0, PI / 3, 70.0, Vector3.Zero(), scene)
|
||||
|
||||
init {
|
||||
with(camera) {
|
||||
attachControl(
|
||||
canvas,
|
||||
noPreventDefault = false,
|
||||
useCtrlForPanning = false,
|
||||
panningMouseButton = 0
|
||||
)
|
||||
inertia = 0.0
|
||||
angularSensibilityX = 200.0
|
||||
angularSensibilityY = 200.0
|
||||
panningInertia = 0.0
|
||||
panningSensibility = 20.0
|
||||
panningAxis = Vector3(1.0, 1.0, 0.0)
|
||||
pinchDeltaPercentage = 0.1
|
||||
wheelDeltaPercentage = 0.1
|
||||
}
|
||||
|
||||
observe(store.currentNinjaObject, ::ninjaObjectOrXvmChanged)
|
||||
}
|
||||
|
||||
override fun internalDispose() {
|
||||
mesh?.dispose()
|
||||
super.internalDispose()
|
||||
}
|
||||
|
||||
private fun ninjaObjectOrXvmChanged(ninjaObject: NinjaObject<*>?) {
|
||||
mesh?.dispose()
|
||||
|
||||
if (ninjaObject != null) {
|
||||
val mesh = Mesh("Model", scene)
|
||||
val vertexData = ninjaObjectToVertexData(ninjaObject)
|
||||
vertexData.applyToMesh(mesh)
|
||||
|
||||
// Make sure we rotate around the center of the model instead of its origin.
|
||||
val bb = mesh.getBoundingInfo().boundingBox
|
||||
val height = bb.maximum.y - bb.minimum.y
|
||||
mesh.position = mesh.position.addInPlaceFromFloats(0.0, -height / 2 - bb.minimum.y, 0.0)
|
||||
|
||||
this.mesh = mesh
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package world.phantasmal.web.viewer.store
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import world.phantasmal.lib.fileFormats.ninja.NinjaObject
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.mutableVal
|
||||
import world.phantasmal.webui.stores.Store
|
||||
|
||||
class ViewerStore(scope: CoroutineScope) : Store(scope) {
|
||||
private val _currentNinjaObject = mutableVal<NinjaObject<*>?>(null)
|
||||
|
||||
val currentNinjaObject: Val<NinjaObject<*>?> = _currentNinjaObject
|
||||
|
||||
fun setCurrentNinjaObject(ninjaObject: NinjaObject<*>?) {
|
||||
_currentNinjaObject.value = ninjaObject
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package world.phantasmal.web.viewer.widgets
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.w3c.dom.Node
|
||||
import world.phantasmal.web.viewer.controller.ViewerToolbarController
|
||||
import world.phantasmal.webui.dom.Icon
|
||||
import world.phantasmal.webui.dom.div
|
||||
import world.phantasmal.webui.widgets.FileButton
|
||||
import world.phantasmal.webui.widgets.Toolbar
|
||||
import world.phantasmal.webui.widgets.Widget
|
||||
|
||||
class ViewerToolbar(
|
||||
scope: CoroutineScope,
|
||||
private val ctrl: ViewerToolbarController,
|
||||
) : Widget(scope) {
|
||||
override fun Node.createElement() =
|
||||
div {
|
||||
className = "pw-viewer-toolbar"
|
||||
|
||||
addChild(Toolbar(
|
||||
scope,
|
||||
children = listOf(
|
||||
FileButton(
|
||||
scope,
|
||||
text = "Open file...",
|
||||
iconLeft = Icon.File,
|
||||
accept = ".afs, .nj, .njm, .xj, .xvm",
|
||||
multiple = true,
|
||||
filesSelected = { files -> scope.launch { ctrl.openFiles(files) } }
|
||||
)
|
||||
)
|
||||
))
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
package world.phantasmal.web.viewer.widgets
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.w3c.dom.HTMLCanvasElement
|
||||
import org.w3c.dom.Node
|
||||
import world.phantasmal.web.core.rendering.Renderer
|
||||
import world.phantasmal.web.core.widgets.RendererWidget
|
||||
import world.phantasmal.webui.dom.div
|
||||
import world.phantasmal.webui.widgets.Widget
|
||||
|
||||
class ViewerWidget(
|
||||
scope: CoroutineScope,
|
||||
private val toolbar: Widget,
|
||||
private val createRenderer: (HTMLCanvasElement) -> Renderer,
|
||||
) : Widget(scope) {
|
||||
override fun Node.createElement() =
|
||||
div {
|
||||
className = "pw-viewer-viewer"
|
||||
|
||||
addChild(toolbar)
|
||||
div {
|
||||
className = "pw-viewer-viewer-container"
|
||||
|
||||
addChild(RendererWidget(scope, createRenderer))
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
init {
|
||||
@Suppress("CssUnusedSymbol")
|
||||
// language=css
|
||||
style("""
|
||||
.pw-viewer-viewer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.pw-viewer-viewer-container {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
""".trimIndent())
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +1,5 @@
|
||||
package world.phantasmal.webui.controllers
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import world.phantasmal.webui.DisposableContainer
|
||||
|
||||
abstract class Controller(protected val scope: CoroutineScope) :
|
||||
DisposableContainer(),
|
||||
CoroutineScope by scope
|
||||
abstract class Controller : DisposableContainer()
|
||||
|
@ -1,6 +1,5 @@
|
||||
package world.phantasmal.webui.controllers
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import world.phantasmal.observable.value.MutableVal
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.mutableVal
|
||||
@ -9,7 +8,7 @@ interface Tab {
|
||||
val title: String
|
||||
}
|
||||
|
||||
open class TabController<T : Tab>(scope: CoroutineScope, val tabs: List<T>) : Controller(scope) {
|
||||
open class TabController<T : Tab>(val tabs: List<T>) : Controller() {
|
||||
private val _activeTab: MutableVal<T?> = mutableVal(tabs.firstOrNull())
|
||||
|
||||
val activeTab: Val<T?> = _activeTab
|
||||
|
Loading…
Reference in New Issue
Block a user