@needle-tools/engine
Version:
Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in.
321 lines • 12.9 kB
JavaScript
import { createLoaders } from "@needle-tools/gltf-progressive";
import { BoxGeometry, BufferGeometry, Color, CylinderGeometry, DoubleSide, ExtrudeGeometry, Group, Mesh, MeshBasicMaterial, MeshStandardMaterial, PlaneGeometry, Shape, SphereGeometry, Sprite, SpriteMaterial } from "three";
import { TextGeometry } from "three/examples/jsm/geometries/TextGeometry.js";
import { Font, FontLoader } from "three/examples/jsm/loaders/FontLoader.js";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
export var PrimitiveType;
(function (PrimitiveType) {
/**
* A quad with a width and height of 1 facing the positive Z axis
*/
PrimitiveType[PrimitiveType["Quad"] = 0] = "Quad";
/**
* A cube with a width, height, and depth of 1
*/
PrimitiveType[PrimitiveType["Cube"] = 1] = "Cube";
PrimitiveType[PrimitiveType["Sphere"] = 2] = "Sphere";
PrimitiveType[PrimitiveType["Cylinder"] = 3] = "Cylinder";
PrimitiveType[PrimitiveType["RoundedCube"] = 10] = "RoundedCube";
})(PrimitiveType || (PrimitiveType = {}));
/**
* Utility class to create primitive objects
* @example
* ```typescript
* const cube = ObjectUtils.createPrimitive("Cube", { name: "Cube", position: { x: 0, y: 0, z: 0 } });
* ```
*/
export class ObjectUtils {
/**
* Creates a 3D text object
* @param text The text to display
* @param opts Options to create the object
*/
static createText(text, opts) {
let geometry = null;
const font = opts?.font || loadFont(opts?.familyFamily || null);
if (font instanceof Font) {
geometry = this.#createTextGeometry(text, font, opts);
}
else if (geometry == null) {
geometry = new BufferGeometry();
}
const color = opts?.color || 0xffffff;
const mesh = new Mesh(geometry, opts?.material ?? new MeshStandardMaterial({ color: color }));
this.applyDefaultObjectOptions(mesh, opts);
if (font instanceof Promise) {
font.then(res => {
mesh.geometry = this.#createTextGeometry(text, res, opts);
if (opts?.onGeometry)
opts.onGeometry(mesh);
});
}
else {
if (opts?.onGeometry)
opts.onGeometry(mesh);
}
return mesh;
}
static #createTextGeometry(text, font, opts) {
const depth = opts?.depth || .1;
const geo = new TextGeometry(text, {
font,
size: 1,
depth: depth,
height: depth,
bevelEnabled: opts?.bevel || false,
bevelThickness: .01,
bevelOffset: .01,
bevelSize: .01,
});
return geo;
}
/**
* Creates an occluder object that only render depth but not color
* @param type The type of primitive to create
* @returns The created object
*/
static createOccluder(type) {
const occluderMaterial = new MeshBasicMaterial({ colorWrite: false, depthWrite: true, side: DoubleSide });
return this.createPrimitive(type, { material: occluderMaterial });
}
static createPrimitive(type, opts) {
let obj;
const color = opts?.color || 0xffffff;
switch (type) {
case "Quad":
case PrimitiveType.Quad:
{
const quadGeo = new PlaneGeometry(1, 1, 1, 1);
const mat = opts?.material ?? new MeshStandardMaterial({ color: color });
if (opts?.texture && "map" in mat)
mat.map = opts.texture;
obj = new Mesh(quadGeo, mat);
obj.name = "Quad";
}
break;
case "Cube":
case PrimitiveType.Cube:
{
const boxGeo = new BoxGeometry(1, 1, 1);
const mat = opts?.material ?? new MeshStandardMaterial({ color: color });
if (opts?.texture && "map" in mat)
mat.map = opts.texture;
obj = new Mesh(boxGeo, mat);
obj.name = "Cube";
}
break;
case PrimitiveType.RoundedCube:
case "RoundedCube":
{
const boxGeo = createBoxWithRoundedEdges(1, 1, 1, .1, 2);
const mat = opts?.material ?? new MeshStandardMaterial({ color: color });
if (opts?.texture && "map" in mat)
mat.map = opts.texture;
obj = new Mesh(boxGeo, mat);
obj.name = "RoundedCube";
}
break;
case "Sphere":
case PrimitiveType.Sphere:
{
const sphereGeo = new SphereGeometry(.5, 16, 16);
const mat = opts?.material ?? new MeshStandardMaterial({ color: color });
if (opts?.texture && "map" in mat)
mat.map = opts.texture;
obj = new Mesh(sphereGeo, mat);
obj.name = "Sphere";
}
break;
case "Cylinder":
case PrimitiveType.Cylinder:
{
const geo = new CylinderGeometry(.5, .5, 1, 32);
const mat = opts?.material ?? new MeshStandardMaterial({ color: color });
if (opts?.texture && "map" in mat)
mat.map = opts.texture;
obj = new Mesh(geo, mat);
obj.name = "Cylinder";
}
break;
case "ShaderBall":
{
obj = new Group();
obj.name = "ShaderBall";
loadShaderball(obj, opts);
}
break;
}
this.applyDefaultObjectOptions(obj, opts);
return obj;
}
/**
* Creates a Sprite object
* @param opts Options to create the object
* @returns The created object
*/
static createSprite(opts) {
const color = 0xffffff;
const mat = new SpriteMaterial({ color: color });
if (opts?.texture && "map" in mat)
mat.map = opts.texture;
const sprite = new Sprite(mat);
this.applyDefaultObjectOptions(sprite, opts);
return sprite;
}
static applyDefaultObjectOptions(obj, opts) {
obj.receiveShadow = true;
obj.castShadow = true;
if (opts?.name)
obj.name = opts.name;
if (opts?.position) {
if (Array.isArray(opts.position))
obj.position.set(opts.position[0], opts.position[1], opts.position[2]);
else
obj.position.set(opts.position.x, opts.position.y, opts.position.z);
}
if (opts?.rotation) {
if (Array.isArray(opts.rotation))
obj.rotation.set(opts.rotation[0], opts.rotation[1], opts.rotation[2]);
else
obj.rotation.set(opts.rotation.x, opts.rotation.y, opts.rotation.z);
}
if (opts?.scale) {
if (typeof opts.scale === "number")
obj.scale.set(opts.scale, opts.scale, opts.scale);
else if (Array.isArray(opts.scale)) {
obj.scale.set(opts.scale[0], opts.scale[1], opts.scale[2]);
}
else {
obj.scale.set(opts.scale.x, opts.scale.y, opts.scale.z);
}
}
if (opts?.receiveShadow != undefined) {
obj.receiveShadow = opts.receiveShadow;
}
if (opts?.castShadow != undefined) {
obj.castShadow = opts.castShadow;
}
if (opts?.parent) {
opts.parent.add(obj);
}
}
}
function createBoxWithRoundedEdges(width, height, _depth, radius0, smoothness) {
const shape = new Shape();
const eps = 0.00001;
const radius = radius0 - eps;
shape.absarc(eps, eps, eps, -Math.PI / 2, -Math.PI, true);
shape.absarc(eps, height - radius * 2, eps, Math.PI, Math.PI / 2, true);
shape.absarc(width - radius * 2, height - radius * 2, eps, Math.PI / 2, 0, true);
shape.absarc(width - radius * 2, eps, eps, 0, -Math.PI / 2, true);
const geometry = new ExtrudeGeometry(shape, {
bevelEnabled: true,
bevelSegments: smoothness * 2,
steps: 1,
bevelSize: radius,
bevelThickness: radius0,
curveSegments: smoothness,
});
geometry.scale(1, 1, 1 - radius0);
geometry.center();
geometry.computeVertexNormals();
return geometry;
}
const fontsDict = new Map();
function loadFont(family) {
let url = "";
switch (family) {
default:
case "OpenSans":
url = "https://cdn.needle.tools/static/fonts/facetype/Open Sans_Regular_ascii.json";
break;
case "Helvetiker":
url = "https://raw.githubusercontent.com/mrdoob/three.js/master/examples/fonts/helvetiker_regular.typeface.json";
break;
}
if (fontsDict.has(url)) {
const res = fontsDict.get(url);
if (res)
return res;
}
const loader = new FontLoader();
const promise = new Promise((resolve, reject) => {
loader.load(url, res => {
fontsDict.set(url, res);
resolve(res);
}, undefined, reject);
});
fontsDict.set(url, promise);
return promise;
}
let __shaderballIsLoading = false;
let __shaderballPromise = null;
/** Loads the shaderball mesh in the background and assigns it to the provided mesh */
function loadShaderball(group, opts) {
// invoke the loading process ONCE
if (__shaderballPromise === null) {
// to make the autofit work we need to insert a placeholder the first time this is invoked
const url = "https://cdn.needle.tools/static/models/shaderball.glb";
const loader = new GLTFLoader();
const loaders = createLoaders(null);
loader.setDRACOLoader(loaders.dracoLoader);
loader.setKTX2Loader(loaders.ktx2Loader);
__shaderballIsLoading = true;
__shaderballPromise = loader.loadAsync(url)
.then(res => {
const scene = res.scene;
// the shaderball pivot is on the bottom and has a size of 1x1x1
// to match the behaviour from all other object's we move it down by 0.5
scene.position.y -= .5;
return scene;
})
.catch(err => {
// Handle case if the shaderball mesh fails to load or is unavailable for any reason
console.warn("Failed to load shaderball mesh: " + err.message);
return createShaderballPlaceholder();
})
.finally(() => {
__shaderballIsLoading = false;
});
}
if (__shaderballIsLoading) {
// if the shaderball is still loading, insert a placeholder
// this is mainly so that the autofit works correctly
const placeholder = createShaderballPlaceholder();
placeholder.name = "ShaderBall-Placeholder";
const mesh = placeholder.children[0];
if (mesh?.type === "Mesh")
updateShaderballMaterial(mesh, opts);
group.add(placeholder);
}
// and for every object that needs it, clone the loaded mesh
__shaderballPromise.then(res => {
// remove placeholders
group.children.forEach(c => {
if (c.name === "ShaderBall-Placeholder")
group.remove(c);
});
// clone the loaded mesh
const instance = res.clone();
const mesh = instance.children[0];
if (mesh?.type === "Mesh") {
updateShaderballMaterial(mesh, opts);
}
group.add(instance);
});
}
function updateShaderballMaterial(mesh, opts) {
const needCustomMaterial = opts?.color || opts?.material || opts?.texture;
if (needCustomMaterial) {
const mat = opts?.material ?? mesh.material?.clone() ?? new MeshStandardMaterial();
if (opts.color && "color" in mat && mat.color instanceof Color)
mat.color.set(opts.color);
if (opts?.texture && "map" in mat)
mat.map = opts.texture;
mesh.material = mat;
}
}
function createShaderballPlaceholder() {
return new Group().add(ObjectUtils.createPrimitive("Sphere", { material: new MeshBasicMaterial({ transparent: true, opacity: .1 }) }));
}
//# sourceMappingURL=engine_create_objects.js.map