mson-three/src/three/builder.ts

349 lines
8.9 KiB
TypeScript

import {
BoxGeometry,
Euler,
Material,
MathUtils,
Mesh,
Object3D,
PlaneGeometry,
Vector2,
Vector3,
} from 'three';
import {
MsonBaseComponent,
MsonBox,
MsonComponent,
MsonComponentType,
MsonCompound,
MsonCompoundComponent,
MsonData,
MsonEvaluatedModel,
MsonFace,
MsonFaces,
MsonPlanar,
MsonPlanarPlane,
MsonPlanarXYZWHUVXY,
MsonPlane,
MsonSlot,
Vec3,
} from '../mson';
import { isArrayOfArrays } from '../util/array-of-array';
export class ThreeBuilder {
static normals: Record<MsonFace, Vector3> = {
up: new Vector3(0, -1, 0),
down: new Vector3(0, 1, 0),
east: new Vector3(1, 0, 0),
west: new Vector3(-1, 0, 0),
south: new Vector3(0, 0, 1),
north: new Vector3(0, 0, -1),
};
static pocConvert: Record<
MsonFace,
(pos: Vector3, size: Vector2) => Vector3
> = {
up: function (pos: Vector3, size: Vector2): Vector3 {
return new Vector3(pos.x + size.x / 2, pos.y, pos.z + size.y / 2);
},
down: function (pos: Vector3, size: Vector2): Vector3 {
return new Vector3(pos.x + size.x / 2, pos.y, pos.z + size.y / 2);
},
east: function (pos: Vector3, size: Vector2): Vector3 {
return new Vector3(pos.x, pos.y + size.y / 2, pos.z + size.x / 2);
},
west: function (pos: Vector3, size: Vector2): Vector3 {
return new Vector3(pos.x, pos.y + size.y / 2, pos.z + size.x / 2);
},
south: function (pos: Vector3, size: Vector2): Vector3 {
return new Vector3(pos.x + size.x / 2, pos.y + size.y / 2, pos.z);
},
north: function (pos: Vector3, size: Vector2): Vector3 {
return new Vector3(pos.x + size.x / 2, pos.y + size.y / 2, pos.z);
},
};
constructor(private readonly material: Material) {}
/**
* Create a THREE.js object from MSON evaluated model data.
* @param model MSON Evaluated model
* @returns THREE.js object
*/
buildGeometry(model: MsonEvaluatedModel) {
if (!model._dataEvaluated) {
throw new Error(
'Please evaluate the MSON model before building a geometry.',
);
}
const wrapper = new Object3D();
wrapper.name = model.name;
for (const [name, component] of Object.entries(model._dataEvaluated)) {
this.makeGeometry(name, component, wrapper);
}
return wrapper;
}
/**
* Generate geometry from a MSON component
* @param component Component type
* @param parent Parent object
* @returns Geometry object
*/
protected makeGeometry(
name: string,
component: MsonComponent,
parent: Object3D,
parentComponent?: MsonComponent,
) {
const wrapper = this.createWrapper(
name,
component.type || 'mson:compound',
component,
);
parent.add(wrapper);
// Compound objects
if (!component.type || component.type === 'mson:compound') {
this.makeMsonCompound(
name,
component as MsonCompound,
wrapper,
parentComponent,
);
}
if (component.type === 'mson:slot') {
for (const [childName, child] of Object.entries(component.data)) {
this.makeGeometry(childName, child, wrapper, component);
}
}
if (component.type === 'mson:planar') {
this.makeMsonPlanar(name, component, wrapper, parentComponent);
}
if (component.type === 'mson:plane') {
this.makeMsonPlane(name, component, wrapper, parentComponent);
}
(component as MsonCompound).cubes?.forEach(
(part: MsonCompoundComponent) => {
if (!part.type || part.type === 'mson:box') {
this.makeMsonBox(name, part as MsonBox, wrapper, component);
return;
}
if (part.type === 'mson:plane') {
this.makeMsonPlane(name, part as MsonPlane, wrapper, component);
return;
}
},
);
if (component.children) {
for (const [childName, child] of Object.entries(component.children)) {
this.makeGeometry(childName, child, wrapper, component);
}
}
}
protected makeMsonCompound(
name: string,
component: MsonCompound,
parent: Object3D,
parentComponent?: MsonComponent,
) {}
protected makeMsonBox(
name: string,
component: MsonBox,
parent: Object3D,
parentComponent?: MsonComponent,
) {
const size = new Vector3().fromArray(component.size as Vec3);
const dilate = new Vector3();
if (component.dilate) {
if (Array.isArray(component.dilate)) dilate.fromArray(component.dilate);
else dilate.set(component.dilate, component.dilate, component.dilate);
}
const offset = new Vector3().fromArray(component.from as Vec3);
const halfSize = size.clone().divideScalar(2);
offset.setY(offset.y * -1);
const adjustedTranslate = halfSize.clone().add(offset);
const geometry = new BoxGeometry(
size.x + dilate.x,
size.y + dilate.y,
size.z + dilate.z,
1,
1,
1,
);
geometry.translate(
adjustedTranslate.x,
adjustedTranslate.y - size.y,
adjustedTranslate.z,
);
// FIXME: hack toJSON
(geometry as any).type = 'BufferGeometry';
delete (geometry as any).parameters;
// TODO: apply UVs
const mesh = new Mesh(geometry, this.material);
mesh.name = `${name}__mesh`;
mesh.updateMatrix();
parent.add(mesh);
}
protected makeMsonPlaneFace(
face: MsonFace,
parent: Object3D,
pos: Vector3,
size: Vector2,
uv: Vector2,
mirror?: boolean | [boolean, boolean],
invertY = false,
) {
const planeGeom = new PlaneGeometry(size.x, size.y);
let axisNormal = ThreeBuilder.normals[face];
if (invertY) {
axisNormal = axisNormal.clone().setY(axisNormal.y * -1);
pos.setY(pos.y * -1);
if (['east', 'west', 'south', 'north'].includes(face))
size.setY(size.y * -1);
}
const adjustedTranslate = ThreeBuilder.pocConvert[face](pos, size);
planeGeom.lookAt(axisNormal);
planeGeom.translate(
adjustedTranslate.x,
adjustedTranslate.y,
adjustedTranslate.z,
);
// TODO: apply UVs
// FIXME: hack toJSON
(planeGeom as any).type = 'BufferGeometry';
delete (planeGeom as any).parameters;
const mesh = new Mesh(planeGeom, this.material);
// mesh.position.copy(pos);
mesh.name = face;
parent.add(mesh);
}
protected makeMsonPlanar(
name: string,
component: MsonPlanar,
parent: Object3D,
parentComponent?: MsonComponent,
) {
const wrapper = new Object3D();
wrapper.name = name;
wrapper.userData.type = component.type;
parent.add(wrapper);
for (const face of MsonFaces) {
if (!component[face]) continue;
const toMake = (
isArrayOfArrays(component[face]) ? component[face] : [component[face]]
) as MsonPlanarXYZWHUVXY[];
for (const faceinfo of toMake) {
const planePos = new Vector3();
const size = new Vector2();
const uv = new Vector2();
let mirror: [boolean, boolean] = [false, false];
planePos.fromArray(faceinfo as number[]);
// planePos.setY(planePos.y * -1);
size.fromArray(faceinfo as number[], 3);
if (faceinfo.length > 5) {
uv.fromArray(faceinfo as number[], 5);
}
if (faceinfo.length > 7) {
mirror = [faceinfo[7] as boolean, faceinfo[8] as boolean];
}
this.makeMsonPlaneFace(face, wrapper, planePos, size, uv, mirror, true);
}
}
wrapper.updateMatrix();
}
protected makeMsonPlane(
name: string,
component: MsonPlane,
parent: Object3D,
parentComponent?: MsonComponent,
) {
const wrapper = new Object3D();
wrapper.name = name;
wrapper.userData.type = component.type;
parent.add(wrapper);
const planePos = new Vector3();
const size = new Vector2();
const uv = new Vector2();
let mirror: [boolean, boolean] = [false, false];
if (component.position) planePos.fromArray(component.position as number[]);
if (component.size) size.fromArray(component.size as number[]);
uv.set(component.texture?.u ?? 0, component.texture?.v ?? 0);
if (component.mirror) mirror = component.mirror;
this.makeMsonPlaneFace(
component.face as MsonFace,
wrapper,
planePos,
size,
uv,
mirror,
true,
);
}
protected createWrapper(
name: string,
typeName: string,
component: MsonBaseComponent,
) {
let wrapper = new Object3D();
wrapper.name = name;
wrapper.userData.type = typeName;
wrapper.visible = component.visible ?? true;
const rotate = new Vector3();
if (component?.rotate) {
rotate.fromArray(
(component.rotate as Vec3).map((entry, index) =>
MathUtils.degToRad(index !== 1 ? entry * -1 : entry),
),
);
}
wrapper.rotation.setFromVector3(rotate, 'XYZ');
const pos = new Vector3();
if (component?.pivot) {
pos.fromArray(component.pivot as Vec3);
}
pos.setY(pos.y * -1);
wrapper.position.copy(pos);
wrapper.updateMatrix();
return wrapper;
}
}