mson-three/src/three/builder.ts

450 lines
12 KiB
TypeScript

import {
BoxGeometry,
CylinderGeometry,
Material,
MathUtils,
Mesh,
Object3D,
PlaneGeometry,
Vector2,
Vector3,
} from 'three';
import {
MsonBaseComponent,
MsonBox,
MsonComponent,
MsonCompound,
MsonCompoundComponent,
MsonCone,
MsonEvaluatedModel,
MsonFace,
MsonFaces,
MsonPlanar,
MsonPlanarXYZWHUVXY,
MsonPlane,
MsonSlot,
MsonTexture,
Vec3,
} from '../mson';
import { isArrayOfArrays } from '../util/array-of-array';
import { UVMapper } from './uv-mapper';
export class ThreeBuilder {
/**
* This is used to rotate the plane to face the right direction.
*/
static planeFaceNormals: 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),
};
/**
* This is used to correctly calculate the translation offset for the plane
* in each axis.
*/
static planeFaceOffsetConversions: Record<
MsonFace,
(pos: Vector3, size: Vector2) => Vector3
> = {
up: (pos: Vector3, size: Vector2): Vector3 =>
new Vector3(pos.x + size.x / 2, pos.y, pos.z + size.y / 2),
down: (pos: Vector3, size: Vector2): Vector3 =>
new Vector3(pos.x + size.x / 2, pos.y, pos.z + size.y / 2),
east: (pos: Vector3, size: Vector2): Vector3 =>
new Vector3(pos.x, pos.y - size.y / 2, pos.z + size.x / 2),
west: (pos: Vector3, size: Vector2): Vector3 =>
new Vector3(pos.x, pos.y - size.y / 2, pos.z + size.x / 2),
south: (pos: Vector3, size: Vector2): Vector3 =>
new Vector3(pos.x + size.x / 2, pos.y - size.y / 2, pos.z),
north: (pos: Vector3, size: Vector2): Vector3 =>
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, undefined, model.texture);
}
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,
texture?: MsonTexture,
) {
const wrapper = this.createWrapper(name, component);
parent.add(wrapper);
// Compound objects
if (!component.type || component.type === 'mson:compound') {
// mhm, go on ahead, nothing to see here
}
if (
component.type === 'mson:slot' ||
(!component.type && (component as unknown as MsonSlot).data)
) {
for (const [childName, child] of Object.entries(
(component as MsonSlot).data,
)) {
this.makeGeometry(childName, child, wrapper, component, {
...texture,
...component.texture,
...child.texture,
});
}
}
if (component.type === 'mson:planar') {
this.makeMsonPlanar(name, component, wrapper, parentComponent, {
...texture,
...component.texture,
});
}
if (component.type === 'mson:plane') {
this.makeMsonPlane(name, component, wrapper, parentComponent, {
...texture,
...component.texture,
});
}
if (component.type === 'mson:cone') {
this.makeMsonCone(name, component, wrapper, parentComponent, {
...texture,
...component.texture,
});
}
(component as MsonCompound).cubes?.forEach(
(part: MsonCompoundComponent) => {
if (!part.type || part.type === 'mson:box') {
this.makeMsonBox(name, part as MsonBox, wrapper, component, {
...texture,
...component.texture,
...part.texture,
});
return;
}
if (part.type === 'mson:plane') {
this.makeMsonPlane(name, part as MsonPlane, wrapper, component, {
...texture,
...component.texture,
...part.texture,
});
return;
}
if (part.type === 'mson:cone') {
this.makeMsonCone(name, part as MsonCone, wrapper, component, {
...texture,
...component.texture,
...part.texture,
});
return;
}
},
);
if (component.children) {
for (const [childName, child] of Object.entries(component.children)) {
this.makeGeometry(childName, child, wrapper, component, {
...texture,
...component.texture,
...child.texture,
});
}
}
}
protected makeMsonBox(
name: string,
component: MsonBox,
parent: Object3D,
parentComponent?: MsonComponent,
texture?: MsonTexture,
) {
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) || [0, 0, 0],
);
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,
);
// TODO: FIXME: this crap is to hack .toJSON() into including the entire geometry
// instead of just the properties used to initialize it.
(geometry as any).type = 'BufferGeometry';
delete (geometry as any).parameters;
UVMapper.mapBoxUVs(
size,
geometry,
texture,
(parentComponent as MsonCompound)?.mirror,
);
const mesh = new Mesh(geometry, this.material);
mesh.name = `${name}__mesh`;
mesh.updateMatrix();
parent.add(mesh);
}
protected makeMsonCone(
name: string,
component: MsonCone,
parent: Object3D,
parentComponent?: MsonComponent,
texture?: MsonTexture,
) {
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 as number,
component.dilate as number,
component.dilate as number,
);
}
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 adjustedSize = size.x * (component.taper as number) ?? 1;
let geometry = new CylinderGeometry(
(adjustedSize + dilate.x) / Math.sqrt(2),
(size.x + dilate.x) / Math.sqrt(2),
size.y + dilate.y,
4,
1,
);
geometry.rotateY(Math.PI / 4);
geometry.translate(
-adjustedTranslate.x,
adjustedTranslate.y - size.y,
adjustedTranslate.z,
);
geometry = geometry.toNonIndexed() as CylinderGeometry;
geometry.computeVertexNormals();
// TODO: FIXME: this crap is to hack .toJSON() into including the entire geometry
// instead of just the properties used to initialize it.
(geometry as any).type = 'BufferGeometry';
delete (geometry as any).parameters;
const mesh = new Mesh(geometry, this.material);
mesh.name = `${name}__cone`;
mesh.updateMatrix();
parent.add(mesh);
}
protected makeMsonPlaneFace(
face: MsonFace,
parent: Object3D,
pos: Vector3,
size: Vector2,
texture?: MsonTexture,
mirror?: boolean | [boolean, boolean],
) {
const planeGeom = new PlaneGeometry(size.x, size.y);
let axisNormal = ThreeBuilder.planeFaceNormals[face];
pos.setY(pos.y * -1);
const adjustedTranslate = ThreeBuilder.planeFaceOffsetConversions[face](
pos,
size,
);
planeGeom.lookAt(axisNormal);
planeGeom.translate(
-adjustedTranslate.x,
adjustedTranslate.y,
adjustedTranslate.z,
);
// TODO: FIXME: this crap is to hack .toJSON() into including the entire geometry
// instead of just the properties used to initialize it.
(planeGeom as any).type = 'BufferGeometry';
delete (planeGeom as any).parameters;
UVMapper.mapPlanarUvs(face, size, planeGeom, texture, mirror);
const mesh = new Mesh(planeGeom, this.material);
mesh.name = face;
parent.add(mesh);
}
protected makeMsonPlanar(
name: string,
component: MsonPlanar,
parent: Object3D,
parentComponent?: MsonComponent,
texture?: MsonTexture,
) {
const wrapper = new Object3D();
wrapper.name = name;
wrapper.userData.type = component.type;
wrapper.userData.implementation = component.implementation;
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[]);
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,
faceinfo.length > 5
? {
...texture,
u: uv.x,
v: uv.y,
}
: texture,
mirror,
);
}
}
wrapper.updateMatrix();
}
protected makeMsonPlane(
name: string,
component: MsonPlane,
parent: Object3D,
parentComponent?: MsonComponent,
texture?: MsonTexture,
) {
const wrapper = new Object3D();
wrapper.name = name;
wrapper.userData.type = component.type;
wrapper.userData.implementation = component.implementation;
parent.add(wrapper);
const planePos = new Vector3();
const size = 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[]);
if (component.mirror) mirror = component.mirror;
this.makeMsonPlaneFace(
component.face as MsonFace,
wrapper,
planePos,
size,
texture,
mirror,
);
}
protected createWrapper(name: string, component: MsonBaseComponent) {
let wrapper = new Object3D();
wrapper.name = name;
wrapper.userData.type = component.type || 'mson:component';
wrapper.visible = component.visible ?? true;
wrapper.userData.implementation = component.implementation;
const rotate = new Vector3();
if (component?.rotate) {
rotate.fromArray(
(component.rotate as Vec3).map((entry, index) =>
MathUtils.degToRad(index !== 2 ? entry * -1 : entry),
),
);
}
wrapper.rotation.setFromVector3(rotate, 'XYZ');
const pos = new Vector3();
if (component?.pivot) {
pos.fromArray(component.pivot as Vec3);
}
pos.setX(pos.x * -1);
pos.setY(pos.y * -1);
wrapper.position.copy(pos);
wrapper.updateMatrix();
return wrapper;
}
}