polygonjs-engine
Version:
node-based webgl 3D engine https://polygonjs.com
323 lines (300 loc) • 8.58 kB
text/typescript
/**
* Creates text
*
*
*
*/
import {TypedSopNode} from './_Base';
import {ArrayUtils} from '../../../core/ArrayUtils';
import {ObjectType} from '../../../core/geometry/Constant';
import {TextBufferGeometry} from 'three/src/geometries/TextBufferGeometry';
import {BufferGeometry} from 'three/src/core/BufferGeometry';
import {ShapeBufferGeometry} from 'three/src/geometries/ShapeBufferGeometry';
import {FontLoader} from 'three/src/loaders/FontLoader';
import {Font} from 'three/src/extras/core/Font';
import {Float32BufferAttribute} from 'three/src/core/BufferAttribute';
import {Vector3} from 'three/src/math/Vector3';
import {Path} from 'three/src/extras/core/Path';
import {Shape} from 'three/src/extras/core/Shape';
import {BufferGeometryUtils} from '../../../modules/three/examples/jsm/utils/BufferGeometryUtils';
import {ModuleName} from '../../poly/registers/modules/_BaseRegister';
import {Poly} from '../../Poly';
import {DEMO_ASSETS_ROOT_URL} from '../../../core/Assets';
const DEFAULT_FONT_URL = `${DEMO_ASSETS_ROOT_URL}/fonts/droid_sans_regular.typeface.json`;
export enum TEXT_TYPE {
MESH = 'mesh',
FLAT = 'flat',
LINE = 'line',
STROKE = 'stroke',
}
export const TEXT_TYPES: Array<TEXT_TYPE> = [TEXT_TYPE.MESH, TEXT_TYPE.FLAT, TEXT_TYPE.LINE, TEXT_TYPE.STROKE];
interface FontByUrl {
[propName: string]: Font;
}
const GENERATION_ERROR_MESSAGE = `failed to generate geometry. Try to remove some characters`;
import {NodeParamsConfig, ParamConfig} from '../utils/params/ParamsConfig';
class TextSopParamsConfig extends NodeParamsConfig {
/** @param font used */
font = ParamConfig.STRING(DEFAULT_FONT_URL);
/** @param text created */
text = ParamConfig.STRING('polygonjs', {multiline: true});
/** @param type of geometry created */
type = ParamConfig.INTEGER(0, {
menu: {
entries: TEXT_TYPES.map((type, i) => {
return {
name: type,
value: i,
};
}),
},
});
/** @param font size */
size = ParamConfig.FLOAT(1, {
range: [0, 1],
rangeLocked: [true, false],
});
/** @param extrude depth */
extrude = ParamConfig.FLOAT(0.1, {
visibleIf: {
type: TEXT_TYPES.indexOf(TEXT_TYPE.MESH),
},
});
/** @param segments count */
segments = ParamConfig.INTEGER(1, {
range: [1, 20],
rangeLocked: [true, false],
visibleIf: {
type: TEXT_TYPES.indexOf(TEXT_TYPE.MESH),
},
});
/** @param stroke width */
strokeWidth = ParamConfig.FLOAT(0.02, {
visibleIf: {
type: TEXT_TYPES.indexOf(TEXT_TYPE.STROKE),
},
});
}
const ParamsConfig = new TextSopParamsConfig();
export class TextSopNode extends TypedSopNode<TextSopParamsConfig> {
params_config = ParamsConfig;
static type() {
return 'text';
}
private _font_loader: FontLoader = new FontLoader();
private _loaded_fonts: FontByUrl = {};
initializeNode() {}
async cook() {
try {
this._loaded_fonts[this.pv.font] = this._loaded_fonts[this.pv.font] || (await this._load_url());
} catch (err) {
this.states.error.set(`count not load font (${this.pv.font})`);
return;
}
const font = this._loaded_fonts[this.pv.font];
if (font) {
switch (TEXT_TYPES[this.pv.type]) {
case TEXT_TYPE.MESH:
return this._create_geometry_from_type_mesh(font);
case TEXT_TYPE.FLAT:
return this._create_geometry_from_type_flat(font);
case TEXT_TYPE.LINE:
return this._create_geometry_from_type_line(font);
case TEXT_TYPE.STROKE:
return this._create_geometry_from_type_stroke(font);
default:
console.warn('type is not valid');
}
}
}
private _create_geometry_from_type_mesh(font: Font) {
const text = this.displayed_text();
const parameters = {
font: font,
size: this.pv.size,
height: this.pv.extrude,
curveSegments: this.pv.segments,
};
try {
const geometry = new TextBufferGeometry(text, parameters);
if (!geometry.index) {
const position_array = geometry.getAttribute('position').array;
geometry.setIndex(ArrayUtils.range(position_array.length / 3));
}
this.setGeometry(geometry);
} catch (err) {
this.states.error.set(GENERATION_ERROR_MESSAGE);
}
}
private _create_geometry_from_type_flat(font: Font) {
const shapes = this._get_shapes(font);
if (shapes) {
var geometry = new ShapeBufferGeometry(shapes);
this.setGeometry(geometry);
}
}
private _create_geometry_from_type_line(font: Font) {
const shapes = this.shapes_from_font(font);
if (shapes) {
const positions = [];
const indices = [];
let current_index = 0;
for (let i = 0; i < shapes.length; i++) {
const shape = shapes[i];
const points = shape.getPoints();
for (let j = 0; j < points.length; j++) {
const point = points[j];
positions.push(point.x);
positions.push(point.y);
positions.push(0);
indices.push(current_index);
if (j > 0 && j < points.length - 1) {
indices.push(current_index);
}
current_index += 1;
}
}
const geometry = new BufferGeometry();
geometry.setAttribute('position', new Float32BufferAttribute(positions, 3));
geometry.setIndex(indices);
this.setGeometry(geometry, ObjectType.LINE_SEGMENTS);
}
}
private async _create_geometry_from_type_stroke(font: Font) {
const shapes = this.shapes_from_font(font);
if (shapes) {
const loader = await this._load_svg_loader();
if (!loader) {
return;
}
// TODO: typescript: correct definition for last 3 optional args
var style = loader.getStrokeStyle(this.pv.strokeWidth, 'white', 'miter', 'butt', 4);
const geometries = [];
for (let i = 0; i < shapes.length; i++) {
const shape = shapes[i];
const points = shape.getPoints();
// TODO: typescript: correct definition for points, arcDivisions, and minDistance
const arcDivisions = 12;
const minDistance = 0.001;
const geometry = loader.pointsToStroke(
(<unknown>points) as Vector3[],
style,
arcDivisions,
minDistance
);
geometries.push(geometry);
}
const merged_geometry = BufferGeometryUtils.mergeBufferGeometries(geometries);
this.setGeometry(merged_geometry); //, CoreConstant.OBJECT_TYPE.LINE_SEGMENTS);
}
}
private shapes_from_font(font: Font) {
const shapes = this._get_shapes(font);
if (shapes) {
const holeShapes: Path[] = [];
for (let i = 0; i < shapes.length; i++) {
const shape = shapes[i];
if (shape.holes && shape.holes.length > 0) {
for (let j = 0; j < shape.holes.length; j++) {
const hole = shape.holes[j];
holeShapes.push(hole);
}
}
}
shapes.push.apply(shapes, holeShapes as Shape[]);
return shapes;
}
}
private _get_shapes(font: Font) {
const text = this.displayed_text();
try {
const shapes = font.generateShapes(text, this.pv.size);
return shapes;
} catch (err) {
this.states.error.set(GENERATION_ERROR_MESSAGE);
}
}
private displayed_text(): string {
return this.pv.text || '';
}
private _load_url() {
const url = `${this.pv.font}?${Date.now()}`;
const ext = this.get_extension();
switch (ext) {
case 'ttf': {
return this._load_ttf(url);
}
case 'json': {
return this._load_json(url);
}
default: {
return null;
}
}
}
async requiredModules() {
if (this.p.font.isDirty()) {
await this.p.font.compute();
}
const ext = this.get_extension();
switch (ext) {
case 'ttf': {
return [ModuleName.TTFLoader];
}
case 'json': {
return [ModuleName.SVGLoader];
}
}
}
private get_extension() {
const url = this.pv.font;
const elements1 = url.split('?')[0];
const elements2 = elements1.split('.');
return elements2[elements2.length - 1];
}
private _load_ttf(url: string): Promise<Font> {
return new Promise(async (resolve, reject) => {
const loaded_module = await this._load_ttf_loader();
if (!loaded_module) {
return;
}
loaded_module.load(
url,
(fnt: object) => {
const parsed = this._font_loader.parse(fnt);
resolve(parsed);
},
undefined,
() => {
reject();
}
);
});
}
private _load_json(url: string): Promise<Font> {
return new Promise((resolve, reject) => {
this._font_loader.load(
url,
(font) => {
resolve(font);
},
undefined,
() => {
reject();
}
);
});
}
private async _load_ttf_loader() {
const module = await Poly.modulesRegister.module(ModuleName.TTFLoader);
if (module) {
return new module.TTFLoader();
}
}
private async _load_svg_loader() {
const module = await Poly.modulesRegister.module(ModuleName.SVGLoader);
if (module) {
return module.SVGLoader;
}
}
}