Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 63 additions & 8 deletions renderer/viewer/lib/entity/EntityMesh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,19 @@ interface GeoData {
skinWeights: number[]
}

interface UVFace {
uv: [number, number]
}

interface CubeFaces {
north?: UVFace
south?: UVFace
east?: UVFace
west?: UVFace
up?: UVFace
down?: UVFace
}

interface JsonBone {
name: string
pivot?: [number, number, number]
Expand All @@ -42,7 +55,7 @@ interface JsonBone {
interface JsonCube {
origin: [number, number, number]
size: [number, number, number]
uv: [number, number]
uv?: [number, number] | CubeFaces
inflate?: number
rotation?: [number, number, number]
}
Expand Down Expand Up @@ -143,6 +156,21 @@ function dot (a: number[], b: number[]): number {
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}

function getFaceUV (cube: JsonCube, face: keyof CubeFaces): [number, number] | undefined {
// Handle per-face UV format (new format)
if (typeof cube.uv === 'object' && !Array.isArray(cube.uv)) {
const faceUV = cube.uv[face.toLowerCase()]
if (faceUV?.uv) {
return faceUV.uv
}
}
// Handle legacy format (array format)
if (Array.isArray(cube.uv)) {
return cube.uv
}
return undefined
}

function addCube (
attr: GeoData,
boneId: number,
Expand All @@ -160,27 +188,54 @@ function addCube (
cubeRotation.y = -cube.rotation[1] * Math.PI / 180
cubeRotation.z = -cube.rotation[2] * Math.PI / 180
}

const faceToDirection: Record<keyof CubeFaces, string> = {
up: 'up',
down: 'down',
north: 'north',
south: 'south',
east: 'east',
west: 'west'
}

for (const { dir, corners, u0, v0, u1, v1 } of Object.values(elemFaces)) {
const ndx = Math.floor(attr.positions.length / 3)

const eastOrWest = dir[0] !== 0

// Determine which face we're processing based on direction
let currentFace: keyof CubeFaces | undefined
for (const [face, direction] of Object.entries(faceToDirection)) {
if (direction === Object.keys(elemFaces)[Object.values(elemFaces).indexOf({ dir, corners, u0, v0, u1, v1 })]) {
currentFace = face
break
}
}

const faceUvs: number[] = []
for (const pos of corners) {
let u: number
let v: number

const uvCoords = currentFace ? getFaceUV(cube, currentFace) : cube.uv
if (!uvCoords) {
errors.push(`Missing UV coordinates for face ${currentFace || 'unknown'}`)
continue
}

if (sameTextureForAllFaces) {
u = (cube.uv[0] + pos[3] * cube.size[0]) / texWidth
v = (cube.uv[1] + pos[4] * cube.size[1]) / texHeight
u = (uvCoords[0] + pos[3] * cube.size[0]) / texWidth
v = (uvCoords[1] + pos[4] * cube.size[1]) / texHeight
} else {
u = (cube.uv[0] + dot(pos[3] ? u1 : u0, cube.size)) / texWidth
v = (cube.uv[1] + dot(pos[4] ? v1 : v0, cube.size)) / texHeight
u = (uvCoords[0] + dot(pos[3] ? u1 : u0, cube.size)) / texWidth
v = (uvCoords[1] + dot(pos[4] ? v1 : v0, cube.size)) / texHeight
}

if (isNaN(u) || isNaN(v)) {
errors.push(`NaN u: ${u}, v: ${v}`)
errors.push(`NaN u: ${u}, v: ${v} for face ${currentFace || 'unknown'}`)
continue
}
if (u < 0 || u > 1 || v < 0 || v > 1) {
errors.push(`u: ${u}, v: ${v} out of range`)
errors.push(`u: ${u}, v: ${v} out of range for face ${currentFace || 'unknown'}`)
continue
}

Expand Down
60 changes: 60 additions & 0 deletions renderer/viewer/lib/entity/models/Arrow.obj
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Aspose.3D Wavefront OBJ Exporter
# Copyright 2004-2024 Aspose Pty Ltd.
# File created: 02/12/2025 20:01:28

mtllib material.lib
g Arrow

#
# object Arrow
#

v -160 8.146034E-06 50
v 160 8.146034E-06 50
v -160 -8.146034E-06 -50
v 160 -8.146034E-06 -50
v -160 -50 1.1920929E-05
v 160 -50 1.1920929E-05
v -160 50 -1.1920929E-05
v 160 50 -1.1920929E-05
v -140 -49.999992 50.000008
v -140 50.000008 49.999992
v -140 -50.000008 -49.999992
v -140 49.999992 -50.000008
# 12 vertices

vn 0 1 -1.6292068E-07
vn 0 1 -1.6292068E-07
vn 0 1 -1.6292068E-07
vn 0 1 -1.6292068E-07
vn 0 3.1391647E-07 1
vn 0 3.1391647E-07 1
vn 0 3.1391647E-07 1
vn 0 3.1391647E-07 1
vn -1 0 0
vn -1 0 0
vn -1 0 0
vn -1 0 0
# 12 vertex normals

vt 0 0.84375 0
vt 0.5 1 0
vt 0.5 1 0
vt 0.5 0.84375 0
vt 0 1 0
vt 0.15625 0.84375 0
vt 0.15625 0.6875 0
vt 0 0.84375 0
vt 0.5 0.84375 0
vt 0 1 0
vt 0 0.6875 0
vt 0 0.84375 0
# 12 texture coords

usemtl Arrow
s 1
f 1/1/1 2/9/2 4/2/3 3/10/4
f 5/8/5 6/4/6 8/3/7 7/5/8
f 9/11/9 10/7/10 12/6/11 11/12/12
#3 polygons

83 changes: 83 additions & 0 deletions renderer/viewer/lib/entity/testEntities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { z } from 'zod'
import entities from './entities.json'

// Define Zod schemas matching the TypeScript interfaces
const ElemFaceSchema = z.object({
dir: z.tuple([z.number(), z.number(), z.number()]),
u0: z.tuple([z.number(), z.number(), z.number()]),
v0: z.tuple([z.number(), z.number(), z.number()]),
u1: z.tuple([z.number(), z.number(), z.number()]),
v1: z.tuple([z.number(), z.number(), z.number()]),
corners: z.array(z.tuple([z.number(), z.number(), z.number(), z.number(), z.number()]))
})

const UVFaceSchema = z.object({
uv: z.tuple([z.number(), z.number()])
})

const CubeFacesSchema = z.object({
north: UVFaceSchema.optional(),
south: UVFaceSchema.optional(),
east: UVFaceSchema.optional(),
west: UVFaceSchema.optional(),
up: UVFaceSchema.optional(),
down: UVFaceSchema.optional()
})

const JsonCubeSchema = z.object({
origin: z.tuple([z.number(), z.number(), z.number()]),
size: z.tuple([z.number(), z.number(), z.number()]),
uv: z.union([
z.tuple([z.number(), z.number()]),
z.object({
north: z.object({ uv: z.tuple([z.number(), z.number()]) }).optional(),
south: z.object({ uv: z.tuple([z.number(), z.number()]) }).optional(),
east: z.object({ uv: z.tuple([z.number(), z.number()]) }).optional(),
west: z.object({ uv: z.tuple([z.number(), z.number()]) }).optional(),
up: z.object({ uv: z.tuple([z.number(), z.number()]) }).optional(),
down: z.object({ uv: z.tuple([z.number(), z.number()]) }).optional()
})
]).optional(),
inflate: z.number().optional(),
rotation: z.tuple([z.number(), z.number(), z.number()]).optional()
})

const JsonBoneSchema = z.object({
name: z.string(),
pivot: z.tuple([z.number(), z.number(), z.number()]).optional(),
bind_pose_rotation: z.tuple([z.number(), z.number(), z.number()]).optional(),
rotation: z.tuple([z.number(), z.number(), z.number()]).optional(),
parent: z.string().optional(),
cubes: z.array(JsonCubeSchema).optional(),
mirror: z.boolean().optional()
})

const JsonModelSchema = z.object({
texturewidth: z.number().optional(),
textureheight: z.number().optional(),
bones: z.array(JsonBoneSchema)
})

const EntityGeometrySchema = z.record(JsonModelSchema)

const EntitiesSchema = z.record(z.object({
geometry: EntityGeometrySchema,
textures: z.record(z.string())
}))

// Validate entities.json against schema
let validatedEntities
try {
validatedEntities = EntitiesSchema.parse(entities)
} catch (error) {
if (error instanceof z.ZodError) {
console.error('Validation errors:')
for (const err of error.errors) {
console.error(`- Path: ${err.path.join('.')}`)
console.error(` Error: ${err.message}`)
}
}
throw error
}

export default validatedEntities
Loading