mirror of
https://github.com/LucasDower/ObjToSchematic.git
synced 2025-12-11 20:15:30 +01:00
431 lines
18 KiB
TypeScript
431 lines
18 KiB
TypeScript
import { Bounds } from './bounds';
|
|
import { RGBA } from './colour';
|
|
import { degreesToRadians } from './math';
|
|
import { OtS_MeshSection } from './ots_materials';
|
|
import { OtS_Texture } from './ots_texture';
|
|
import { UV } from "./util";
|
|
import { ASSERT } from './util/error_util';
|
|
import { Result } from './util/type_util';
|
|
import { Vector3 } from "./vector";
|
|
|
|
type OtS_VertexData<T> = {
|
|
v0: T,
|
|
v1: T,
|
|
v2: T,
|
|
}
|
|
|
|
export type OtS_Triangle =
|
|
| { type: 'solid', colour: RGBA, data: OtS_VertexData<{ position: Vector3, normal: Vector3 }> }
|
|
| { type: 'coloured', data: OtS_VertexData<{ position: Vector3, normal: Vector3, colour: RGBA }> }
|
|
| { type: 'textured', texture: OtS_Texture, data: OtS_VertexData<{ position: Vector3, normal: Vector3, texcoord: UV }> }
|
|
|
|
type OtS_MeshError = 'bad-height' | 'bad-material-match' | 'bad-geometry';
|
|
|
|
export type OtS_MeshSectionMetadata = { name: string } & (
|
|
| { type: 'solid', colour: RGBA }
|
|
| { type: 'colour' }
|
|
| { type: 'textured', texture: OtS_Texture }
|
|
);
|
|
|
|
export class OtS_Mesh {
|
|
private _sections: OtS_MeshSection[];
|
|
|
|
private constructor() {
|
|
this._sections = [];
|
|
}
|
|
|
|
public static create() {
|
|
return new this();
|
|
/*
|
|
// TODO: Check non-zero height
|
|
if (false) {
|
|
return { ok: false, error: {
|
|
code: 'bad-height',
|
|
message: 'Geometry should have a non-zero height, consider rotating the mesh'
|
|
}};
|
|
}
|
|
|
|
// TODO: Check geometry using materials slots is valid, i.e. a triangle that uses a
|
|
// textured material must have texcoords
|
|
if (false) {
|
|
return { ok: false, error: {
|
|
code: 'bad-material-match',
|
|
message: 'Material \'x\' uses a textured material but has no texcoords'
|
|
}};
|
|
}
|
|
|
|
// TODO: Check material slots used by geometry are defined
|
|
|
|
if (!geometry.hasAttribute('position')) {
|
|
return { ok: false, error: {
|
|
code: 'bad-geometry',
|
|
message: 'Missing position data'
|
|
}};
|
|
}
|
|
|
|
const mesh = new this(geometry, materials);
|
|
return { ok: true, value: mesh };
|
|
*/
|
|
}
|
|
|
|
public addSection(section: OtS_MeshSection): Result<void, OtS_MeshError> {
|
|
// TODO: Validation
|
|
|
|
// TODO: Ensure section names do not clash
|
|
|
|
this._sections.push(section);
|
|
return { ok: true, value: undefined };
|
|
}
|
|
|
|
public translate(x: number, y: number, z: number) {
|
|
this._sections.forEach((section) => {
|
|
for (let i = 0; i < section.positionData.length; i += 3) {
|
|
section.positionData[i + 0] += x;
|
|
section.positionData[i + 1] += y;
|
|
section.positionData[i + 2] += z;
|
|
}
|
|
});
|
|
}
|
|
|
|
public scale(s: number) {
|
|
this._sections.forEach((section) => {
|
|
for (let i = 0; i < section.positionData.length; i += 3) {
|
|
section.positionData[i + 0] *= s;
|
|
section.positionData[i + 1] *= s;
|
|
section.positionData[i + 2] *= s;
|
|
}
|
|
});
|
|
}
|
|
|
|
public centre() {
|
|
const centre = this.calcBounds().getCentre();
|
|
this.translate(-centre.x, -centre.y, -centre.z);
|
|
}
|
|
|
|
public normalise(): boolean {
|
|
const bounds = this.calcBounds();
|
|
const size = Vector3.sub(bounds.max, bounds.min);
|
|
const scaleFactor = 1.0 / size.y;
|
|
|
|
if (isNaN(scaleFactor) || !isFinite(scaleFactor)) {
|
|
return false;
|
|
}
|
|
|
|
this.scale(scaleFactor);
|
|
return true;
|
|
}
|
|
|
|
public rotate(pitch: number, roll: number, yaw: number) {
|
|
const cosa = Math.cos(yaw * degreesToRadians);
|
|
const sina = Math.sin(yaw * degreesToRadians);
|
|
|
|
const cosb = Math.cos(pitch * degreesToRadians);
|
|
const sinb = Math.sin(pitch * degreesToRadians);
|
|
|
|
const cosc = Math.cos(roll * degreesToRadians);
|
|
const sinc = Math.sin(roll * degreesToRadians);
|
|
|
|
const Axx = cosa*cosb;
|
|
const Axy = cosa*sinb*sinc - sina*cosc;
|
|
const Axz = cosa*sinb*cosc + sina*sinc;
|
|
|
|
const Ayx = sina*cosb;
|
|
const Ayy = sina*sinb*sinc + cosa*cosc;
|
|
const Ayz = sina*sinb*cosc - cosa*sinc;
|
|
|
|
const Azx = -sinb;
|
|
const Azy = cosb*sinc;
|
|
const Azz = cosb*cosc;
|
|
|
|
this._sections.forEach((section) => {
|
|
for (let i = 0; i < section.positionData.length; i += 3) {
|
|
const px = section.positionData[i + 0];
|
|
const py = section.positionData[i + 1];
|
|
const pz = section.positionData[i + 2];
|
|
|
|
section.positionData[i + 0] = Axx * px + Axy * py + Axz * pz;
|
|
section.positionData[i + 1] = Ayx * px + Ayy * py + Ayz * pz;
|
|
section.positionData[i + 2] = Azx * px + Azy * py + Azz * pz;
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @note Returns a reference to the underlying materials, modifying these is dangerous
|
|
*/
|
|
public getTriangles(): IterableIterator<OtS_Triangle> {
|
|
const sectionCount = this._sections.length;
|
|
ASSERT(sectionCount > 0); // TODO: Don't assert,
|
|
|
|
let sectionIndex = 0;
|
|
let triangleCount = this._sections[0].indexData.length / 3;
|
|
let triangleIndex = 0;
|
|
|
|
return {
|
|
[Symbol.iterator]: function () {
|
|
return this;
|
|
},
|
|
next: () => {
|
|
if (triangleIndex >= triangleCount && sectionIndex < sectionCount - 1) {
|
|
++sectionIndex;
|
|
triangleCount = this._sections[sectionIndex].indexData.length / 3;
|
|
triangleIndex = 0;
|
|
}
|
|
|
|
if (triangleIndex < triangleCount) {
|
|
const section = this._sections[sectionIndex];
|
|
|
|
const index0 = section.indexData[triangleIndex * 3 + 0];
|
|
const index1 = section.indexData[triangleIndex * 3 + 1];
|
|
const index2 = section.indexData[triangleIndex * 3 + 2];
|
|
|
|
++triangleIndex;
|
|
|
|
switch (section.type) {
|
|
case 'solid': {
|
|
const triangle: OtS_Triangle = {
|
|
type: 'solid',
|
|
colour: section.colour,
|
|
data: {
|
|
v0: {
|
|
position: new Vector3(
|
|
section.positionData[index0 * 3 + 0],
|
|
section.positionData[index0 * 3 + 1],
|
|
section.positionData[index0 * 3 + 2],
|
|
),
|
|
normal: new Vector3(
|
|
section.normalData[index0 * 3 + 0],
|
|
section.normalData[index0 * 3 + 1],
|
|
section.normalData[index0 * 3 + 2],
|
|
),
|
|
},
|
|
v1: {
|
|
position: new Vector3(
|
|
section.positionData[index1 * 3 + 0],
|
|
section.positionData[index1 * 3 + 1],
|
|
section.positionData[index1 * 3 + 2],
|
|
),
|
|
normal: new Vector3(
|
|
section.normalData[index1 * 3 + 0],
|
|
section.normalData[index1 * 3 + 1],
|
|
section.normalData[index1 * 3 + 2],
|
|
),
|
|
},
|
|
v2: {
|
|
position: new Vector3(
|
|
section.positionData[index2 * 3 + 0],
|
|
section.positionData[index2 * 3 + 1],
|
|
section.positionData[index2 * 3 + 2],
|
|
),
|
|
normal: new Vector3(
|
|
section.normalData[index2 * 3 + 0],
|
|
section.normalData[index2 * 3 + 1],
|
|
section.normalData[index2 * 3 + 2],
|
|
),
|
|
},
|
|
},
|
|
}
|
|
return { done: false, value: triangle };
|
|
}
|
|
case 'colour': {
|
|
const triangle: OtS_Triangle = {
|
|
type: 'coloured',
|
|
data: {
|
|
v0: {
|
|
position: new Vector3(
|
|
section.positionData[index0 * 3 + 0],
|
|
section.positionData[index0 * 3 + 1],
|
|
section.positionData[index0 * 3 + 2],
|
|
),
|
|
normal: new Vector3(
|
|
section.normalData[index0 * 3 + 0],
|
|
section.normalData[index0 * 3 + 1],
|
|
section.normalData[index0 * 3 + 2],
|
|
),
|
|
colour: {
|
|
r: section.colourData[index0 * 4 + 0],
|
|
g: section.colourData[index0 * 4 + 1],
|
|
b: section.colourData[index0 * 4 + 2],
|
|
a: section.colourData[index0 * 4 + 3],
|
|
},
|
|
},
|
|
v1: {
|
|
position: new Vector3(
|
|
section.positionData[index1 * 3 + 0],
|
|
section.positionData[index1 * 3 + 1],
|
|
section.positionData[index1 * 3 + 2],
|
|
),
|
|
normal: new Vector3(
|
|
section.normalData[index1 * 3 + 0],
|
|
section.normalData[index1 * 3 + 1],
|
|
section.normalData[index1 * 3 + 2],
|
|
),
|
|
colour: {
|
|
r: section.colourData[index1 * 4 + 0],
|
|
g: section.colourData[index1 * 4 + 1],
|
|
b: section.colourData[index1 * 4 + 2],
|
|
a: section.colourData[index1 * 4 + 3],
|
|
},
|
|
},
|
|
v2: {
|
|
position: new Vector3(
|
|
section.positionData[index2 * 3 + 0],
|
|
section.positionData[index2 * 3 + 1],
|
|
section.positionData[index2 * 3 + 2],
|
|
),
|
|
normal: new Vector3(
|
|
section.normalData[index2 * 3 + 0],
|
|
section.normalData[index2 * 3 + 1],
|
|
section.normalData[index2 * 3 + 2],
|
|
),
|
|
colour: {
|
|
r: section.colourData[index2 * 4 + 0],
|
|
g: section.colourData[index2 * 4 + 1],
|
|
b: section.colourData[index2 * 4 + 2],
|
|
a: section.colourData[index2 * 4 + 3],
|
|
},
|
|
},
|
|
},
|
|
}
|
|
return { done: false, value: triangle };
|
|
}
|
|
case 'textured': {
|
|
const triangle: OtS_Triangle = {
|
|
type: 'textured',
|
|
texture: section.texture,
|
|
data: {
|
|
v0: {
|
|
position: new Vector3(
|
|
section.positionData[index0 * 3 + 0],
|
|
section.positionData[index0 * 3 + 1],
|
|
section.positionData[index0 * 3 + 2],
|
|
),
|
|
normal: new Vector3(
|
|
section.normalData[index0 * 3 + 0],
|
|
section.normalData[index0 * 3 + 1],
|
|
section.normalData[index0 * 3 + 2],
|
|
),
|
|
texcoord: {
|
|
u: section.texcoordData[index0 * 2 + 0],
|
|
v: section.texcoordData[index0 * 2 + 1],
|
|
},
|
|
},
|
|
v1: {
|
|
position: new Vector3(
|
|
section.positionData[index1 * 3 + 0],
|
|
section.positionData[index1 * 3 + 1],
|
|
section.positionData[index1 * 3 + 2],
|
|
),
|
|
normal: new Vector3(
|
|
section.normalData[index1 * 3 + 0],
|
|
section.normalData[index1 * 3 + 1],
|
|
section.normalData[index1 * 3 + 2],
|
|
),
|
|
texcoord: {
|
|
u: section.texcoordData[index1 * 2 + 0],
|
|
v: section.texcoordData[index1 * 2 + 1],
|
|
},
|
|
},
|
|
v2: {
|
|
position: new Vector3(
|
|
section.positionData[index2 * 3 + 0],
|
|
section.positionData[index2 * 3 + 1],
|
|
section.positionData[index2 * 3 + 2],
|
|
),
|
|
normal: new Vector3(
|
|
section.normalData[index2 * 3 + 0],
|
|
section.normalData[index2 * 3 + 1],
|
|
section.normalData[index2 * 3 + 2],
|
|
),
|
|
texcoord: {
|
|
u: section.texcoordData[index2 * 2 + 0],
|
|
v: section.texcoordData[index2 * 2 + 1],
|
|
},
|
|
},
|
|
},
|
|
}
|
|
return { done: false, value: triangle };
|
|
}
|
|
}
|
|
}
|
|
|
|
return { done: true, value: undefined };
|
|
},
|
|
};
|
|
}
|
|
|
|
public copy() {
|
|
const clone = OtS_Mesh.create();
|
|
|
|
for (const section of this._sections) {
|
|
const success = clone.addSection(section).ok;
|
|
ASSERT(success);
|
|
}
|
|
|
|
return clone;
|
|
}
|
|
|
|
public calcBounds() {
|
|
const bounds = Bounds.getEmptyBounds();
|
|
|
|
const vec = new Vector3(0, 0, 0);
|
|
this._sections.forEach((section) => {
|
|
for (let i = 0; i < section.positionData.length; i += 3) {
|
|
vec.set(
|
|
section.positionData[i + 0],
|
|
section.positionData[i + 1],
|
|
section.positionData[i + 2],
|
|
);
|
|
bounds.extendByPoint(vec);
|
|
}
|
|
});
|
|
|
|
return bounds;
|
|
}
|
|
|
|
public calcTriangleCount() {
|
|
return this._sections
|
|
.map((section) => {
|
|
return section.indexData.length / 3;
|
|
})
|
|
.reduce((total, count) => total + count, 0);
|
|
}
|
|
|
|
// TODO: Return copy
|
|
public getSectionData(): OtS_MeshSection[] {
|
|
return this._sections;
|
|
}
|
|
|
|
public getSectionMetadata(): OtS_MeshSectionMetadata[] {
|
|
const metadata: OtS_MeshSectionMetadata[] = [];
|
|
|
|
this._sections.forEach((section) => {
|
|
let entry: OtS_MeshSectionMetadata;
|
|
switch (section.type) {
|
|
case 'solid':
|
|
entry = {
|
|
type: 'solid',
|
|
name: section.name,
|
|
colour: section.colour,
|
|
};
|
|
break;
|
|
case 'colour': {
|
|
entry = {
|
|
type: 'colour',
|
|
name: section.name,
|
|
};
|
|
break;
|
|
}
|
|
case 'textured':
|
|
entry = {
|
|
type: 'textured',
|
|
name: section.name,
|
|
texture: section.texture,
|
|
}
|
|
}
|
|
metadata.push(entry);
|
|
});
|
|
|
|
return metadata;
|
|
}
|
|
} |