/ concept-collection / mesh-converter
Sign in
concept-collection / mesh-converter
mesh-converter / src / mesh / formats / tricol.ts
93 lines · 3.0 KBBlameHistoryRaw
1import type { MeshData, MeshFormat } from '../types'
2import { parseNumbers, validateIndices } from '../types'
4/**
5 * TRICOL — "TriColor Interchange" (invented, for illustration).
6 * Comma-separated records, one per line. Stores geometry and optional
7 * per-vertex colors, but no normals and no mesh name.
8 *
9 * # comment
10 * V,<x>,<y>,<z>[,<r>,<g>,<b>]
11 * F,<a>,<b>,<c>
12 */
13export const tricolFormat: MeshFormat = {
14 id: 'tricol',
15 label: 'TRICOL (TriColor Interchange)',
16 extension: '.tricol',
17 blurb: 'Geometry plus vertex colors. No normals, no mesh name.',
18 capabilities: { normals: false, colors: true, name: false },
20 parse(text: string): MeshData {
21 const positions: number[] = []
22 const colors: number[] = []
23 const indices: number[] = []
24 let sawColorless = false
26 for (const rawLine of text.split('\n')) {
27 const line = rawLine.trim()
28 if (line.length === 0 || line.startsWith('#')) continue
29 const tokens = line.split(',').map((t) => t.trim())
30 const kind = tokens[0]
31 if (kind === 'V') {
32 const nums = parseNumbers(tokens.slice(1), 'TRICOL vertex')
33 if (nums.length === 3) {
34 sawColorless = true
35 } else if (nums.length === 6) {
36 colors.push(nums[3], nums[4], nums[5])
37 } else {
38 throw new Error(`TRICOL: V record needs 3 or 6 numbers, got ${nums.length}`)
39 }
40 positions.push(nums[0], nums[1], nums[2])
41 } else if (kind === 'F') {
42 const nums = parseNumbers(tokens.slice(1), 'TRICOL face')
43 if (nums.length !== 3) {
44 throw new Error('TRICOL: F record must have exactly 3 indices')
45 }
46 indices.push(...nums)
47 } else {
48 throw new Error(`TRICOL: unknown record type "${kind}"`)
49 }
50 }
52 const nVertices = positions.length / 3
53 if (nVertices === 0) throw new Error('TRICOL: no vertices found')
54 if (colors.length > 0 && sawColorless) {
55 throw new Error('TRICOL: either all V records have colors or none do')
56 }
57 validateIndices(indices, nVertices, 'TRICOL')
59 return {
60 name: null,
61 positions,
62 indices,
63 normals: null,
64 colors: colors.length > 0 ? colors : null,
65 }
66 },
68 serialize(mesh: MeshData): string {
69 const nVertices = mesh.positions.length / 3
70 const nFaces = mesh.indices.length / 3
71 const out: string[] = ['# TRICOL mesh']
72 for (let i = 0; i < nVertices; i++) {
73 const p = [mesh.positions[3 * i], mesh.positions[3 * i + 1], mesh.positions[3 * i + 2]]
74 const fields = p.map(round6)
75 if (mesh.colors) {
76 fields.push(
77 round6(mesh.colors[3 * i]),
78 round6(mesh.colors[3 * i + 1]),
79 round6(mesh.colors[3 * i + 2]),
80 )
81 }
82 out.push(`V,${fields.join(',')}`)
83 }
84 for (let i = 0; i < nFaces; i++) {
85 out.push(`F,${mesh.indices[3 * i]},${mesh.indices[3 * i + 1]},${mesh.indices[3 * i + 2]}`)
86 }
87 return out.join('\n') + '\n'
88 },
91function round6(x: number): number {
92 return Math.round(x * 1e6) / 1e6
moveopenescclose