code stash

This commit is contained in:
Evert Prants 2024-02-18 12:42:09 +02:00
commit b5c0adca22
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
21 changed files with 1136 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules
lib

5
.prettierrc Normal file
View File

@ -0,0 +1,5 @@
{
"semi": true,
"singleQuote": true,
"trailingComma": "all"
}

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"editor.formatOnSave": true
}

13
README.md Normal file
View File

@ -0,0 +1,13 @@
# Mson -> THREE
WIP do not use
Parse [Mson](https://github.com/MineLittlePony/Mson).
## Setup
1. copy the folder at https://github.com/MineLittlePony/Mson/tree/1.20.2/src/main/resources/assets/mson/models/entity as `inputs/mson`
2. build `npm run build`
3. export `node lib/test-node.js`
THREE object will be placed in `outputs`

2
inputs/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

2
outputs/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

106
package-lock.json generated Normal file
View File

@ -0,0 +1,106 @@
{
"name": "mson-three",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mson-three",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"three": "^0.161.0"
},
"devDependencies": {
"@types/node": "^20.11.19",
"@types/three": "^0.161.2",
"prettier": "^3.2.5",
"typescript": "^5.3.3"
}
},
"node_modules/@types/node": {
"version": "20.11.19",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz",
"integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/@types/stats.js": {
"version": "0.17.3",
"resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.3.tgz",
"integrity": "sha512-pXNfAD3KHOdif9EQXZ9deK82HVNaXP5ZIF5RP2QG6OQFNTaY2YIetfrE9t528vEreGQvEPRDDc8muaoYeK0SxQ==",
"dev": true
},
"node_modules/@types/three": {
"version": "0.161.2",
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.161.2.tgz",
"integrity": "sha512-DazpZ+cIfBzbW/p0zm6G8CS03HBMd748A3R1ZOXHpqaXZLv2I5zNgQUrRG//UfJ6zYFp2cUoCQaOLaz8ubH07w==",
"dev": true,
"dependencies": {
"@types/stats.js": "*",
"@types/webxr": "*",
"fflate": "~0.6.10",
"meshoptimizer": "~0.18.1"
}
},
"node_modules/@types/webxr": {
"version": "0.5.14",
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.14.tgz",
"integrity": "sha512-UEMMm/Xn3DtEa+gpzUrOcDj+SJS1tk5YodjwOxcqStNhCfPcwgyC5Srg2ToVKyg2Fhq16Ffpb0UWUQHqoT9AMA==",
"dev": true
},
"node_modules/fflate": {
"version": "0.6.10",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz",
"integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==",
"dev": true
},
"node_modules/meshoptimizer": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz",
"integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==",
"dev": true
},
"node_modules/prettier": {
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz",
"integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==",
"dev": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/three": {
"version": "0.161.0",
"resolved": "https://registry.npmjs.org/three/-/three-0.161.0.tgz",
"integrity": "sha512-LC28VFtjbOyEu5b93K0bNRLw1rQlMJ85lilKsYj6dgTu+7i17W+JCCEbvrpmNHF1F3NAUqDSWq50UD7w9H2xQw=="
},
"node_modules/typescript": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
"integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
}
}
}

23
package.json Normal file
View File

@ -0,0 +1,23 @@
{
"name": "mson-three",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "tsc",
"watch": "tsc -w"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@types/node": "^20.11.19",
"@types/three": "^0.161.2",
"prettier": "^3.2.5",
"typescript": "^5.3.3"
},
"dependencies": {
"three": "^0.161.0"
}
}

1
src/index.ts Normal file
View File

@ -0,0 +1 @@
export * from './mson';

304
src/mson/eval.ts Normal file
View File

@ -0,0 +1,304 @@
import { deepClone } from '../util/deep-clone';
import { isObject } from '../util/merge-deep';
import {
MsonComponent,
MsonData,
MsonEvaluatedLocals,
MsonEvaluatedModel,
MsonExpression,
MsonLocals,
MsonSlot,
MsonVariable,
} from './mson.type';
import { ModelStore } from './store';
type MsonVariables = Record<string, number>;
/**
* This class is used to evaluate all variables within a MSON model definition.
*/
export class MsonEvaluate {
constructor(protected readonly store: ModelStore) {}
/**
* Evaluate a model, resolving all of its references and parameters.
* @param model MSON data to evaluate
* @param useLocals Override locals
* @returns Evaluated model, with (hopefully) all variables replaced.
*/
evaluateModel(
model: string,
useLocals?: MsonEvaluatedLocals,
): MsonEvaluatedModel {
const modelData = this.store.getModelByName(model);
if (!modelData) {
throw new Error(`Model ${model} not found for evaluation`);
}
// Deep copy the model
const localCopy = deepClone(modelData) as MsonEvaluatedModel;
localCopy.name = model;
localCopy._locals = { ...(localCopy.locals || {}) };
localCopy._data = { ...localCopy.data };
// Evaluate parents
if (modelData.parent) {
localCopy._parent = this.evaluateModel(modelData.parent);
// Overwrite / combine parent locals with current locals
localCopy._locals = {
...(localCopy._parent._locals || {}),
...localCopy._locals,
};
// Overwrite / combine parent data with current data
localCopy._data = {
...(localCopy._parent._data || {}),
...localCopy._data,
};
// Inherit texture parameters
localCopy.texture = {
...(localCopy._parent?.texture || {}),
...(localCopy.texture || {}),
};
}
// Overwrite / merge locals
if (useLocals) {
localCopy._locals = {
...localCopy._locals,
...useLocals,
};
}
// Evaluate locals with globally combined parameters
this.evaluateLocals(localCopy);
// Recursively substitute all locals within the data.
// Also, fill all slots.
localCopy._dataEvaluated = this.recursiveSubstitute(
localCopy._data,
localCopy._localsEvaluated,
);
return localCopy;
}
/**
* Evaluate a MSON expression (arithmetic)
* @param expression Mson Expression definition
* @param variables Scope variables
*/
protected evaluateExpression(
expression: MsonExpression,
variables: MsonVariables = {},
) {
if (!this.isExpression(expression)) {
throw new Error('Invalid expression');
}
let [src, oper, trg] = expression;
if (this.isExpression(src)) {
// TODO: resolve as unknown, probably with a tuple of some kind
src = this.evaluateExpression(
src as unknown as MsonExpression,
variables,
);
}
if (this.isExpression(trg)) {
// TODO: resolve as unknown, probably with a tuple of some kind
trg = this.evaluateExpression(
trg as unknown as MsonExpression,
variables,
);
}
src = this.substitute(src, variables);
trg = this.substitute(trg, variables);
// Assertions
if (isNaN(src)) {
throw new Error(
`Expression evaluation [${expression}] failed: ${src} is NaN`,
);
}
if (isNaN(trg)) {
throw new Error(
`Expression evaluation [${expression}] failed: ${trg} is NaN`,
);
}
if (!['+', '-', '*', '/', '%', '^'].includes(oper)) {
throw new Error(
`Expression evaluation [${expression}] failed: ${oper} is not supported`,
);
}
// Operators
switch (oper) {
case '+':
return src + trg;
case '-':
return src - trg;
case '*':
return src * trg;
case '/':
return src / trg;
case '%':
return src % trg;
case '^':
return Math.pow(src, trg);
}
}
/**
* Determine if input is an expression or not.
* @param input Questionable input
* @returns Is expression or not
*/
protected isExpression(input: MsonVariable | MsonExpression) {
return (
Array.isArray(input) &&
input.length === 3 &&
typeof input[1] === 'string' &&
!input[1].startsWith('#')
);
}
/**
* Substitute input with variables or return input.
* @param input Input to substitute
* @param variables Pool of variables
* @returns Substituted value
*/
protected substitute(input: MsonVariable, variables: MsonVariables = {}) {
if (typeof input !== 'string') return input;
const key = input.replace('#', '');
return variables[key] ?? input;
}
/**
* Substitute ALL variables recursively.
* Also fills slots. (TODO: separate)
* @param input Model data, a component or component metadata
* @param variables Variables to use
* @returns Substituted object
*/
protected recursiveSubstitute(
input: MsonData | MsonComponent,
variables: MsonVariables = {},
) {
const keyList = Object.keys(input);
// Handle slots, include the child model with current locals already applied.
const asSlot = input as MsonSlot;
if (keyList.includes('data') && typeof asSlot.data === 'string') {
const evaluateChild = this.evaluateModel(
asSlot.data as string,
asSlot.locals
? this.evaluateLocalsObject(asSlot.locals, variables)
: variables,
);
return {
type: 'mson:slot',
implementation: asSlot.implementation,
data: evaluateChild._dataEvaluated,
};
}
return keyList.reduce<any>((item, key) => {
const value = item[key];
// Do not substitute locals or generated values here.
if (key === 'locals' || key.startsWith('_')) return item;
if (isObject(value)) {
item[key] = this.recursiveSubstitute(value, variables);
}
if (typeof value === 'string' && value.startsWith('#')) {
item[key] = this.substitute(value, variables);
}
if (this.isExpression(value)) {
item[key] = this.evaluateExpression(value, variables);
}
if (Array.isArray(value)) {
item[key] = value.map((entry) =>
isObject(entry)
? this.recursiveSubstitute(entry, variables)
: this.substitute(entry, variables),
);
}
return item;
}, input);
}
/**
* Evaluate locals on model
* @param localCopy Local evaluated model
*/
protected evaluateLocals(localCopy: MsonEvaluatedModel) {
if (!localCopy._locals) {
if (localCopy._parent?._localsEvaluated) {
// Copy parent's globals
localCopy._localsEvaluated = deepClone(
localCopy._parent._localsEvaluated,
);
}
// Nothing more to evaluate
return;
}
localCopy._localsEvaluated = this.evaluateLocalsObject(
localCopy._locals,
localCopy._parent?._localsEvaluated,
);
}
/**
* Substitute locals' variables and evaluate expressions.
* @param locals Locals to evaluate
* @param parentEvals Parents evaluated locals, falls back to these.
* @returns Locals with variables substituted.
*/
protected evaluateLocalsObject(
locals: MsonLocals,
parentEvals: MsonEvaluatedLocals = {},
) {
return Object.keys(locals).reduce<MsonEvaluatedLocals>(
(pool, local) => {
const currentValue = locals?.[local];
if (currentValue === undefined) return pool;
// Number type just pass on
if (typeof currentValue === 'number') {
pool[local] = currentValue;
}
// Substitute strings, those are variables
if (typeof currentValue === 'string') {
pool[local] = this.substitute(currentValue, pool);
}
// Evaluate expressions
if (this.isExpression(currentValue)) {
pool[local] = this.evaluateExpression(
currentValue as MsonExpression,
pool,
);
}
return pool;
},
{ ...parentEvals },
);
}
}

3
src/mson/index.ts Normal file
View File

@ -0,0 +1,3 @@
export * from './mson.type';
export * from './store';
export * from './eval';

269
src/mson/mson.type.ts Normal file
View File

@ -0,0 +1,269 @@
import { PartialBy } from '../util/deep-partial.type';
export interface MsonTexture {
w?: number;
h?: number;
u?: number;
v?: number;
}
export type MsonOperand =
| '+' /* ADD */
| '-' /* SUBTRACT */
| '*' /* MULTIPLY */
| '/' /* DIVIDE */
| '%' /* MODULUS */
| '^' /* EXPONENT */;
export type MsonVariable = string | number;
export type Vec3 = [number, number, number];
export type Vec2 = [number, number];
export type MsonVec3 = Vec3 | [MsonVariable, MsonVariable, MsonVariable];
export type MsonVec2 = Vec2 | [MsonVariable, MsonVariable];
/**
* Mson allows you to use variables and do basic calculations within the model file.
* Unless otherwise stated, it can be assumed that every place where a numerical value
* is expected can also accept a reference to a variable (see below).
*
* Expressions with calculations can only be done in the `local` blocks where veriables are defined.
*/
export type MsonExpression = [MsonVariable, MsonOperand, MsonVariable];
export type MsonLocals = Record<string, MsonVariable | MsonExpression>;
export type MsonEvaluatedLocals = Record<string, number>;
export type MsonComponentType =
| 'mson:compound'
| 'mson:box'
| 'mson:plane'
| 'mson:planar'
| 'mson:slot'
| 'mson:cone'
| 'mson:quads'
| string;
export interface MsonBaseComponent {
type?: MsonComponentType;
/**
* Whether or not this part is visible. You shouldn't have to use this in most circumstances.
* @default true
*/
visible?: boolean;
/**
* The XYZ center of rotation of this part.
*/
pivot?: MsonVec3;
/**
* The XYZ rotation angle of this part, given in degrees.
*/
rotate?: MsonVec3;
texture?: MsonTexture;
/**
* A map of child components to load as part of this one.
*/
children?: MsonData;
__comment?: any;
}
export interface MsonBox extends MsonBaseComponent {
type: 'mson:box';
/**
* The relative XYZ position of the box
*/
from: MsonVec3;
/**
* The width, height, and depth of the box
*/
size: MsonVec3;
/**
* Part-specific dilation applied to any cubes created by components defined by this part.
*/
dilate?: Vec3 | number;
}
export interface MsonCompound extends MsonBaseComponent {
type: 'mson:compound';
dilate?: MsonVec3;
/**
* Whether to flip this part's textures.
* @default false
*/
mirror?: boolean | MsonVec3;
/**
* default type for components (if omitted): mson:box
* allowed types: <mson:box|mson:cone|mson:link|mson:quads|mson:plane|mson:slot>
*/
cubes?: MsonCompoundComponent[];
}
export type MsonFace = 'up' | 'down' | 'east' | 'west' | 'south' | 'north';
export type MsonPlanarXYZWH = [
MsonVariable,
MsonVariable,
MsonVariable,
MsonVariable,
MsonVariable,
];
export type MsonPlanarXYZWHUV = [
MsonVariable,
MsonVariable,
MsonVariable,
MsonVariable,
MsonVariable,
MsonVariable,
MsonVariable,
];
export type MsonPlanarXYZWHUVXY = [
MsonVariable,
MsonVariable,
MsonVariable,
MsonVariable,
MsonVariable,
MsonVariable,
MsonVariable,
boolean,
boolean,
];
/**
* First 3 are the position,
* Next two are the size,
* Last two are an optional texture coordinate. If not given, the parent's texture will be used.
*
* Two booleans indicate whether to flip the texture vertically and horizontally.
*/
export type MsonPlanarPlane =
| MsonPlanarXYZWH
| MsonPlanarXYZWHUV
| MsonPlanarXYZWHUVXY;
export interface MsonPlanar extends MsonBaseComponent {
type: 'mson:planar';
dilate?: MsonVec3;
/**
* Whether to flip this part's textures.
* @default false
*/
mirror?: boolean | MsonVec3;
up?: MsonPlanarPlane;
down?: MsonPlanarPlane;
east?: MsonPlanarPlane;
west?: MsonPlanarPlane;
south?: MsonPlanarPlane;
north?: MsonPlanarPlane;
}
export interface MsonPlane extends MsonBaseComponent {
type: 'mson:plane';
face?: MsonFace;
position?: MsonVec3;
size?: MsonVec2;
mirror?: [boolean, boolean];
dilate?: MsonVec3;
}
export interface MsonSlot extends MsonBaseComponent {
type: 'mson:slot';
implementation: string;
name: string;
data: MsonData | string;
locals?: MsonLocals;
}
export interface MsonCone extends MsonBaseComponent {
type: 'mson:cone';
size?: MsonVec3;
from?: MsonVec3;
dilate?: MsonVariable;
taper?: MsonVariable;
}
export interface MsonQuads extends MsonBaseComponent {
type: 'mson:quads';
// TODO: dunno
}
export type MsonComponent =
| MsonBox
| PartialBy<MsonCompound, 'type'>
| MsonPlanar
| MsonPlane
| MsonSlot
| MsonCone
| MsonQuads;
export type MsonCompoundComponent =
| PartialBy<MsonBox, 'type'>
| MsonCone
| MsonQuads
| MsonPlane
| MsonSlot;
export type MsonData = Record<MsonComponentType, MsonComponent>;
export interface MsonModel {
/**
* The parent file to extend from.
*/
parent?: MsonComponentType;
/**
* Texture definition specifying the default width and height for the file.
* This property is inherited by all components defined in this file
* and defining it will function similarly to defining variables in the locals block.
*/
texture?: MsonTexture;
/**
* The default growth (dilation) applied to all boxes defined by components in this file.
* Note that components may have their own dilation parameter, in which instance that value is *added*
* to this one
*
* ** DOES NOT SUPPORT VARIABLES **
*/
dilate?: Vec3;
/**
* A block of local variables that may be references by components within the data block.
* Values defined in here are typically applied over what is inherited from the parent file (if specified)
* and become available for use by the parent's components as well.
*/
locals?: MsonLocals;
/**
* default type for components (if omitted): mson:compound
* allowed types: <mson:compound|mson:slot|mson:link|mson:planar>
*/
data: MsonData;
}
export interface MsonEvaluatedModel extends MsonModel {
name: string;
/**
* Combined locals from whole dependency tree
*/
_locals?: MsonLocals;
/**
* Evaluated locals
*/
_localsEvaluated?: MsonEvaluatedLocals;
/**
* Combined data from whole dependency tree
*/
_data?: MsonData;
/**
* Evaluated data with evaluated locals
*/
_dataEvaluated?: MsonData;
/**
* Evaluated parent
*/
_parent?: MsonEvaluatedModel;
}

24
src/mson/store.ts Normal file
View File

@ -0,0 +1,24 @@
import { DeepPartial } from '../util/deep-partial.type';
import { mergeDeep } from '../util/merge-deep';
import { MsonModel } from './mson.type';
export class ModelStore {
public componentStore = new Map<string, MsonModel>();
getModelByName(name: string) {
return this.componentStore.get(name);
}
updateModel(name: string, upsert: DeepPartial<MsonModel>) {
const existing = this.getModelByName(name);
this.componentStore.set(name, mergeDeep(existing || {}, upsert));
}
insertModel(name: string, model: MsonModel) {
this.componentStore.set(name, model);
}
listModels() {
return Array.from(this.componentStore.keys());
}
}

27
src/test-node.ts Normal file
View File

@ -0,0 +1,27 @@
import { resolve } from 'path';
import { ModelStore, MsonEvaluate } from '.';
import { fillStoreFromFilesystem, saveGeometry } from './util/node';
import { ThreeBuilder } from './three';
import { MeshBasicMaterial } from 'three';
async function init() {
const store = new ModelStore();
const evaluate = new MsonEvaluate(store);
const mat = new MeshBasicMaterial();
const builder = new ThreeBuilder(mat);
await fillStoreFromFilesystem(store, resolve(process.cwd(), 'inputs'));
// mson:steve
// minelittlepony:steve_pony
const final = evaluate.evaluateModel('mson:biped');
// console.log(final.texture);
console.dir(final._dataEvaluated, {
depth: 20,
});
const geometry = builder.buildGeometry(final);
await saveGeometry(resolve(process.cwd(), 'outputs'), 'steve', geometry);
}
init().catch(console.error);

155
src/three/builder.ts Normal file
View File

@ -0,0 +1,155 @@
import {
BoxGeometry,
Euler,
Material,
MathUtils,
Mesh,
Object3D,
Vector3,
} from 'three';
import {
MsonBaseComponent,
MsonBox,
MsonComponent,
MsonComponentType,
MsonCompound,
MsonCompoundComponent,
MsonEvaluatedModel,
Vec3,
} from '../mson';
export class ThreeBuilder {
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,
) {
// Compound objects
if (!component.type || component.type === 'mson:compound') {
return this.makeMsonCompound(
name,
component as MsonCompound,
parent,
parentComponent,
);
}
}
protected makeMsonCompound(
name: string,
component: MsonCompound,
parent: Object3D,
parentComponent?: MsonComponent,
) {
const wrapper = this.createWrapper(name, component);
parent.add(wrapper);
component.cubes?.forEach((part: MsonCompoundComponent) => {
if (!part.type || part.type === 'mson:box') {
this.makeMsonBox(name, part as MsonBox, wrapper, component);
return;
}
});
}
protected makeMsonBox(
name: string,
component: MsonBox,
parent: Object3D,
parentComponent?: MsonComponent,
) {
const offset = new Vector3();
if (parentComponent?.pivot) {
offset.sub(new Vector3().fromArray(parentComponent.pivot as Vec3));
}
const size = new Vector3().fromArray(component.size as Vec3);
const pos = new Vector3().fromArray(component.from 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 rotate = new Vector3();
if (parentComponent?.rotate) {
rotate.fromArray(
(parentComponent.rotate as Vec3).map((entry) =>
MathUtils.degToRad(entry),
),
);
}
const halfOffset = offset.clone().divideScalar(2);
const halfSize = size.clone().divideScalar(2);
const halfDilation = dilate.clone().divideScalar(2);
const geometry = new BoxGeometry(
size.x + dilate.x,
size.y + dilate.y,
size.z + dilate.z,
1,
1,
1,
);
geometry.translate(halfOffset.x, halfOffset.y, halfOffset.z);
geometry.rotateX(rotate.x);
geometry.rotateY(rotate.y);
geometry.rotateZ(rotate.z);
// FIXME: hack toJSON
(geometry as any).type = 'BufferGeometry';
delete (geometry as any).parameters;
// TODO: apply UVs
pos.add(halfOffset).add(halfSize);
const mesh = new Mesh(geometry, this.material);
mesh.name = `${name}__mesh`;
mesh.position.copy(pos);
mesh.updateMatrix();
parent.add(mesh);
}
protected createWrapper(name: string, component: MsonBaseComponent) {
let wrapper = new Object3D();
wrapper.name = name;
wrapper.userData.type = 'mson:compound';
wrapper.visible = component.visible ?? true;
return wrapper;
}
}

1
src/three/index.ts Normal file
View File

@ -0,0 +1 @@
export * from './builder';

2
src/util/deep-clone.ts Normal file
View File

@ -0,0 +1,2 @@
export const deepClone = <T = any>(input: T): T =>
JSON.parse(JSON.stringify(input));

View File

@ -0,0 +1,6 @@
export type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
export type DeepPartial<T> = T extends object
? {
[P in keyof T]?: DeepPartial<T[P]>;
}
: T;

31
src/util/merge-deep.ts Normal file
View File

@ -0,0 +1,31 @@
/**
* Simple object check.
* @param item
* @returns {boolean}
*/
export function isObject(item: any) {
return item && typeof item === 'object' && !Array.isArray(item);
}
/**
* Deep merge two objects.
* @param target
* @param ...sources
*/
export function mergeDeep(target: any, ...sources: any[]) {
if (!sources.length) return target;
const source = sources.shift();
if (isObject(target) && isObject(source)) {
for (const key in source) {
if (isObject(source[key])) {
if (!target[key]) Object.assign(target, { [key]: {} });
mergeDeep(target[key], source[key]);
} else {
Object.assign(target, { [key]: source[key] });
}
}
}
return mergeDeep(target, ...sources);
}

56
src/util/node.ts Normal file
View File

@ -0,0 +1,56 @@
import { ModelStore } from '../mson';
import { promises as fs } from 'node:fs';
import { join } from 'node:path';
import { Object3D } from 'three';
export const fillStoreFromFilesystem = async (
store: ModelStore,
root: string,
) => {
// Get all mod prefixes from the input directory.
const prefixes = (await fs.readdir(root, { withFileTypes: true }))
.filter((item) => item.isDirectory())
.map((item) => item.name);
// Loop through all of the prefixes
for (const prefix of prefixes) {
// Prefix absolute path
const prefixPath = join(root, prefix);
// Recurse all of the files in the mod directory
const allFiles = await fs.readdir(prefixPath, {
recursive: true,
withFileTypes: true,
});
// Loop through all JSON files
for (const dirent of allFiles) {
if (!dirent.isFile()) continue;
if (!dirent.name.endsWith('.json')) continue;
const absPath = join(dirent.path, dirent.name);
// Relative path of model
const relPath = absPath.replace(prefixPath, '');
// Model file contents
const readFile = await fs.readFile(absPath, { encoding: 'utf-8' });
const parsed = JSON.parse(readFile);
// Component name referenced from other models
const componentName = `${prefix}:${relPath.replace('.json', '').substring(1)}`;
// Insert component into the store
store.insertModel(componentName, parsed);
}
}
};
export const saveGeometry = async (
root: string,
name: string,
object: Object3D,
) =>
await fs.writeFile(
join(root, `${name}.json`),
JSON.stringify(object.toJSON()),
);

101
tsconfig.json Normal file
View File

@ -0,0 +1,101 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
"outDir": "./lib", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}