p5
Version:
[](https://www.npmjs.com/package/p5)
1,316 lines (1,230 loc) • 40.9 kB
JavaScript
import { Geometry } from './p5.Geometry.js';
import { Vector } from '../math/p5.Vector.js';
import { a as request } from '../rendering-CvUVN-Vb.js';
import '../constants-BRcElHU3.js';
import './p5.DataArray.js';
import '../creating_reading-Cr8L2Jnm.js';
import 'colorjs.io/fn';
import '../color/color_spaces/hsb.js';
import '../dom/p5.Element.js';
import '../dom/p5.File.js';
import '../io/p5.XML.js';
import '../p5.Renderer-R23xoC7s.js';
import '../image/filters.js';
import '../shape/custom_shapes.js';
import '../core/States.js';
import '../io/utilities.js';
import 'file-saver';
import '../dom/p5.MediaElement.js';
import '../shape/2d_primitives.js';
import '../core/helpers.js';
import '../shape/attributes.js';
import '../shape/curves.js';
import '../shape/vertex.js';
import '../color/setting.js';
import 'omggif';
import '../io/csv.js';
import 'gifenc';
import '../image/pixels.js';
import '../core/transform.js';
import './GeometryBuilder.js';
import '../math/p5.Matrix.js';
import '../math/Matrices/Matrix.js';
import '../math/Matrices/MatrixInterface.js';
import './p5.Quat.js';
import './p5.RenderBuffer.js';
import './ShapeBuilder.js';
import 'libtess';
import './GeometryBufferCache.js';
import '../image/const.js';
import '../math/trigonometry.js';
/**
* @module Shape
* @submodule 3D Models
* @for p5
* @requires core
* @requires p5.Geometry
*/
async function fileExists(url) {
try {
const response = await fetch(url, { method: 'HEAD' });
return response.ok;
} catch (error) {
return false;
}
}
function loading(p5, fn){
/**
* Loads a 3D model to create a
* <a href="#/p5.Geometry">p5.Geometry</a> object.
*
* `loadModel()` can load 3D models from OBJ and STL files. Once the model is
* loaded, it can be displayed with the
* <a href="#/p5/model">model()</a> function, as in `model(shape)`.
*
* There are three ways to call `loadModel()` with optional parameters to help
* process the model.
*
* The first parameter, `path`, is a `String` with the path to the file. Paths
* to local files should be relative, as in `loadModel('assets/model.obj')`.
* URLs such as `'https://example.com/model.obj'` may be blocked due to browser
* security. The `path` parameter can also be defined as a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request)
* object for more advanced usage.
* Note: When loading a `.obj` file that references materials stored in
* `.mtl` files, p5.js will attempt to load and apply those materials.
* To ensure that the `.obj` file reads the `.mtl` file correctly include the
* `.mtl` file alongside it.
*
* The first way to call `loadModel()` has three optional parameters after the
* file path. The first optional parameter, `successCallback`, is a function
* to call once the model loads. For example,
* `loadModel('assets/model.obj', handleModel)` will call the `handleModel()`
* function once the model loads. The second optional parameter,
* `failureCallback`, is a function to call if the model fails to load. For
* example, `loadModel('assets/model.obj', handleModel, handleFailure)` will
* call the `handleFailure()` function if an error occurs while loading. The
* third optional parameter, `fileType`, is the model’s file extension as a
* string. For example,
* `loadModel('assets/model', handleModel, handleFailure, '.obj')` will try to
* load the file model as a `.obj` file.
*
* The second way to call `loadModel()` has four optional parameters after the
* file path. The first optional parameter is a `Boolean` value. If `true` is
* passed, as in `loadModel('assets/model.obj', true)`, then the model will be
* resized to ensure it fits the canvas. The next three parameters are
* `successCallback`, `failureCallback`, and `fileType` as described above.
*
* The third way to call `loadModel()` has one optional parameter after the
* file path. The optional parameter, `options`, is an `Object` with options,
* as in `loadModel('assets/model.obj', options)`. The `options` object can
* have the following properties:
*
* ```js
* let options = {
* // Enables standardized size scaling during loading if set to true.
* normalize: true,
*
* // Function to call once the model loads.
* successCallback: handleModel,
*
* // Function to call if an error occurs while loading.
* failureCallback: handleError,
*
* // Model's file extension.
* fileType: '.stl',
*
* // Flips the U texture coordinates of the model.
* flipU: false,
*
* // Flips the V texture coordinates of the model.
* flipV: false
* };
*
* // Pass the options object to loadModel().
* loadModel('assets/model.obj', options);
* ```
*
* This function returns a `Promise` and should be used in an `async` setup with
* `await`. See the examples for the usage syntax.
*
* Note: There’s no support for colored STL files. STL files with color will
* be rendered without color.
*
* @method loadModel
* @param {String|Request} path path of the model to be loaded.
* @param {String} [fileType] model’s file extension. Either `'.obj'` or `'.stl'`.
* @param {Boolean} normalize if `true`, scale the model to fit the canvas.
* @param {function(p5.Geometry)} [successCallback] function to call once the model is loaded. Will be passed
* the <a href="#/p5.Geometry">p5.Geometry</a> object.
* @param {function(Event)} [failureCallback] function to call if the model fails to load. Will be passed an `Error` event object.
* @return {Promise<p5.Geometry>} the <a href="#/p5.Geometry">p5.Geometry</a> object
*
* @example
* <div>
* <code>
* // Click and drag the mouse to view the scene from different angles.
*
* let shape;
*
* // Load the file and create a p5.Geometry object.
* async function setup() {
* shape = await loadModel('assets/teapot.obj');
*
* createCanvas(100, 100, WEBGL);
*
* describe('A white teapot drawn against a gray background.');
* }
*
* function draw() {
* background(200);
*
* // Enable orbiting with the mouse.
* orbitControl();
*
* // Draw the shape.
* model(shape);
* }
* </code>
* </div>
*
* <div>
* <code>
* // Click and drag the mouse to view the scene from different angles.
*
* let shape;
*
* // Load the file and create a p5.Geometry object.
* // Normalize the geometry's size to fit the canvas.
* async function setup() {
* shape = await loadModel('assets/teapot.obj', true);
*
* createCanvas(100, 100, WEBGL);
*
* describe('A white teapot drawn against a gray background.');
* }
*
* function draw() {
* background(200);
*
* // Enable orbiting with the mouse.
* orbitControl();
*
* // Draw the shape.
* model(shape);
* }
* </code>
* </div>
*
* <div>
* <code>
* // Click and drag the mouse to view the scene from different angles.
*
* let shape;
*
* // Load the file and create a p5.Geometry object.
* async function setup() {
* await loadModel('assets/teapot.obj', true, handleModel);
*
* createCanvas(100, 100, WEBGL);
*
* describe('A white teapot drawn against a gray background.');
* }
*
* function draw() {
* background(200);
*
* // Enable orbiting with the mouse.
* orbitControl();
*
* // Draw the shape.
* model(shape);
* }
*
* // Set the shape variable and log the geometry's
* // ID to the console.
* function handleModel(data) {
* shape = data;
* console.log(shape.gid);
* }
* </code>
* </div>
*
* <div class='notest'>
* <code>
* // Click and drag the mouse to view the scene from different angles.
*
* let shape;
*
* // Load the file and create a p5.Geometry object.
* async function setup() {
* await loadModel('assets/teapot.obj', true, handleModel, handleError);
*
* createCanvas(100, 100, WEBGL);
*
* describe('A white teapot drawn against a gray background.');
* }
*
* function draw() {
* background(200);
*
* // Enable orbiting with the mouse.
* orbitControl();
*
* // Draw the shape.
* model(shape);
* }
*
* // Set the shape variable and print the geometry's
* // ID to the console.
* function handleModel(data) {
* shape = data;
* console.log(shape.gid);
* }
*
* // Print an error message if the file doesn't load.
* function handleError(error) {
* console.error('Oops!', error);
* }
* </code>
* </div>
*
* <div>
* <code>
* // Click and drag the mouse to view the scene from different angles.
*
* let shape;
*
* // Load the file and create a p5.Geometry object.
* async function setup() {
* await loadModel('assets/teapot.obj', '.obj', true, handleModel, handleError);
*
* createCanvas(100, 100, WEBGL);
*
* describe('A white teapot drawn against a gray background.');
* }
*
* function draw() {
* background(200);
*
* // Enable orbiting with the mouse.
* orbitControl();
*
* // Draw the shape.
* model(shape);
* }
*
* // Set the shape variable and print the geometry's
* // ID to the console.
* function handleModel(data) {
* shape = data;
* console.log(shape.gid);
* }
*
* // Print an error message if the file doesn't load.
* function handleError(error) {
* console.error('Oops!', error);
* }
* </code>
* </div>
*
* <div>
* <code>
* // Click and drag the mouse to view the scene from different angles.
*
* let shape;
* let options = {
* fileType: '.obj',
* normalize: true,
* successCallback: handleModel,
* failureCallback: handleError
* };
*
* // Load the file and create a p5.Geometry object.
* async function setup() {
* await loadModel('assets/teapot.obj', options);
*
* createCanvas(100, 100, WEBGL);
*
* describe('A white teapot drawn against a gray background.');
* }
*
* function draw() {
* background(200);
*
* // Enable orbiting with the mouse.
* orbitControl();
*
* // Draw the shape.
* model(shape);
* }
*
* // Set the shape variable and print the geometry's
* // ID to the console.
* function handleModel(data) {
* shape = data;
* console.log(shape.gid);
* }
*
* // Print an error message if the file doesn't load.
* function handleError(error) {
* console.error('Oops!', error);
* }
* </code>
* </div>
*/
/**
* @method loadModel
* @param {String|Request} path
* @param {String} [fileType]
* @param {function(p5.Geometry)} [successCallback]
* @param {function(Event)} [failureCallback]
* @return {Promise<p5.Geometry>} new <a href="#/p5.Geometry">p5.Geometry</a> object.
*/
/**
* @method loadModel
* @param {String|Request} path
* @param {Object} [options] loading options.
* @param {String} [options.fileType]
* @param {function(p5.Geometry)} [options.successCallback]
* @param {function(Event)} [options.failureCallback]
* @param {Boolean} [options.normalize]
* @param {Boolean} [options.flipU]
* @param {Boolean} [options.flipV]
* @return {Promise<p5.Geometry>} new <a href="#/p5.Geometry">p5.Geometry</a> object.
*/
fn.loadModel = async function (path, fileType, normalize, successCallback, failureCallback) {
// p5._validateParameters('loadModel', arguments);
let flipU = false;
let flipV = false;
if (typeof fileType === 'object') {
// Passing in options object
normalize = fileType.normalize || false;
successCallback = fileType.successCallback;
failureCallback = fileType.failureCallback;
fileType = fileType.fileType || fileType;
flipU = fileType.flipU || false;
flipV = fileType.flipV || false;
} else {
// Passing in individual parameters
if(typeof arguments[arguments.length-1] === 'function'){
if(typeof arguments[arguments.length-2] === 'function'){
successCallback = arguments[arguments.length-2];
failureCallback = arguments[arguments.length-1];
}else {
successCallback = arguments[arguments.length-1];
}
}
if (typeof fileType === 'string') {
if(typeof normalize !== 'boolean') normalize = false;
} else if (typeof fileType === 'boolean') {
normalize = fileType;
fileType = path.slice(-4);
} else {
fileType = path.slice(-4);
normalize = false;
}
}
if (fileType.toLowerCase() !== '.obj' && fileType.toLowerCase() !== '.stl') {
fileType = '.obj';
}
const model = new Geometry(undefined, undefined, undefined, this._renderer);
model.gid = `${path}|${normalize}`;
async function getMaterials(lines) {
const parsedMaterialPromises = [];
for (let line of lines) {
const mtllibMatch = line.match(/^mtllib (.+)/);
if (mtllibMatch) {
// Object has material
let mtlPath = '';
const mtlFilename = mtllibMatch[1];
const objPathParts = path.split('/');
if (objPathParts.length > 1) {
objPathParts.pop();
const objFolderPath = objPathParts.join('/');
mtlPath = objFolderPath + '/' + mtlFilename;
} else {
mtlPath = mtlFilename;
}
parsedMaterialPromises.push(
fileExists(mtlPath).then(exists => {
if (exists) {
return parseMtl(mtlPath);
} else {
console.warn(`MTL file not found or error in parsing; proceeding without materials: ${mtlPath}`);
return {};
}
}).catch(error => {
console.warn(`Error loading MTL file: ${mtlPath}`, error);
return {};
})
);
}
}
try {
const parsedMaterials = await Promise.all(parsedMaterialPromises);
const materials = Object.assign({}, ...parsedMaterials);
return materials;
} catch (error) {
return {};
}
}
try{
if (fileType.match(/\.stl$/i)) {
const { data } = await request(path, 'arrayBuffer');
parseSTL(model, data);
if (normalize) {
model.normalize();
}
if (flipU) {
model.flipU();
}
if (flipV) {
model.flipV();
}
model._makeTriangleEdges();
if (successCallback) {
return successCallback(model);
} else {
return model;
}
} else if (fileType.match(/\.obj$/i)) {
const { data } = await request(path, 'text');
const lines = data.split('\n');
const parsedMaterials = await getMaterials(lines);
parseObj(model, lines, parsedMaterials);
if (normalize) {
model.normalize();
}
if (flipU) {
model.flipU();
}
if (flipV) {
model.flipV();
}
model._makeTriangleEdges();
if (successCallback) {
return successCallback(model);
} else {
return model;
}
}
} catch(err) {
p5._friendlyFileLoadError(3, path);
if(failureCallback) {
return failureCallback(err);
} else {
throw err;
}
}
};
/**
* @private
*/
async function parseMtl(mtlPath) {
let currentMaterial = null;
let materials = {};
const { data } = await request(mtlPath, "text");
const lines = data.split('\n');
for (let line = 0; line < lines.length; ++line) {
const tokens = lines[line].trim().split(/\s+/);
if (tokens[0] === 'newmtl') {
const materialName = tokens[1];
currentMaterial = materialName;
materials[currentMaterial] = {};
} else if (tokens[0] === 'Kd') {
//Diffuse color
materials[currentMaterial].diffuseColor = [
parseFloat(tokens[1]),
parseFloat(tokens[2]),
parseFloat(tokens[3])
];
} else if (tokens[0] === 'Ka') {
//Ambient Color
materials[currentMaterial].ambientColor = [
parseFloat(tokens[1]),
parseFloat(tokens[2]),
parseFloat(tokens[3])
];
} else if (tokens[0] === 'Ks') {
//Specular color
materials[currentMaterial].specularColor = [
parseFloat(tokens[1]),
parseFloat(tokens[2]),
parseFloat(tokens[3])
];
} else if (tokens[0] === 'map_Kd') {
//Texture path
materials[currentMaterial].texturePath = tokens[1];
}
}
return materials;
}
/**
* @private
* Parse OBJ lines into model. For reference, this is what a simple model of a
* square might look like:
*
* v -0.5 -0.5 0.5
* v -0.5 -0.5 -0.5
* v -0.5 0.5 -0.5
* v -0.5 0.5 0.5
*
* f 4 3 2 1
*/
function parseObj(model, lines, materials = {}) {
// OBJ allows a face to specify an index for a vertex (in the above example),
// but it also allows you to specify a custom combination of vertex, UV
// coordinate, and vertex normal. So, "3/4/3" would mean, "use vertex 3 with
// UV coordinate 4 and vertex normal 3". In WebGL, every vertex with different
// parameters must be a different vertex, so loadedVerts is used to
// temporarily store the parsed vertices, normals, etc., and indexedVerts is
// used to map a specific combination (keyed on, for example, the string
// "3/4/3"), to the actual index of the newly created vertex in the final
// object.
const loadedVerts = {
v: [],
vt: [],
vn: []
};
// Map from source index → Map of material → destination index
const usedVerts = {}; // Track colored vertices
let currentMaterial = null;
let hasColoredVertices = false;
let hasColorlessVertices = false;
for (let line = 0; line < lines.length; ++line) {
// Each line is a separate object (vertex, face, vertex normal, etc)
// For each line, split it into tokens on whitespace. The first token
// describes the type.
const tokens = lines[line].trim().split(/\b\s+/);
if (tokens.length > 0) {
if (tokens[0] === 'usemtl') {
// Switch to a new material
currentMaterial = tokens[1];
} else if (tokens[0] === 'v' || tokens[0] === 'vn') {
// Check if this line describes a vertex or vertex normal.
// It will have three numeric parameters.
const vertex = new Vector(
parseFloat(tokens[1]),
parseFloat(tokens[2]),
parseFloat(tokens[3])
);
loadedVerts[tokens[0]].push(vertex);
} else if (tokens[0] === 'vt') {
// Check if this line describes a texture coordinate.
// It will have two numeric parameters U and V (W is omitted).
// Because of WebGL texture coordinates rendering behaviour, the V
// coordinate is inversed.
const texVertex = [parseFloat(tokens[1]), 1 - parseFloat(tokens[2])];
loadedVerts[tokens[0]].push(texVertex);
} else if (tokens[0] === 'f') {
// Check if this line describes a face.
// OBJ faces can have more than three points. Triangulate points.
for (let tri = 3; tri < tokens.length; ++tri) {
const face = [];
const vertexTokens = [1, tri - 1, tri];
for (let tokenInd = 0; tokenInd < vertexTokens.length; ++tokenInd) {
// Now, convert the given token into an index
const vertString = tokens[vertexTokens[tokenInd]];
let vertParts = vertString.split('/');
// TODO: Faces can technically use negative numbers to refer to the
// previous nth vertex. I haven't seen this used in practice, but
// it might be good to implement this in the future.
for (let i = 0; i < vertParts.length; i++) {
vertParts[i] = parseInt(vertParts[i]) - 1;
}
if (!usedVerts[vertString]) {
usedVerts[vertString] = {};
}
if (usedVerts[vertString][currentMaterial] === undefined) {
const vertIndex = model.vertices.length;
model.vertices.push(loadedVerts.v[vertParts[0]].copy());
model.uvs.push(loadedVerts.vt[vertParts[1]] ?
loadedVerts.vt[vertParts[1]].slice() : [0, 0]);
model.vertexNormals.push(loadedVerts.vn[vertParts[2]] ?
loadedVerts.vn[vertParts[2]].copy() : new Vector());
usedVerts[vertString][currentMaterial] = vertIndex;
face.push(vertIndex);
if (currentMaterial
&& materials[currentMaterial]
&& materials[currentMaterial].diffuseColor) {
hasColoredVertices = true;
const materialDiffuseColor =
materials[currentMaterial].diffuseColor;
model.vertexColors.push(materialDiffuseColor[0]);
model.vertexColors.push(materialDiffuseColor[1]);
model.vertexColors.push(materialDiffuseColor[2]);
model.vertexColors.push(1);
} else {
hasColorlessVertices = true;
}
} else {
face.push(usedVerts[vertString][currentMaterial]);
}
}
if (
face[0] !== face[1] &&
face[0] !== face[2] &&
face[1] !== face[2]
) {
model.faces.push(face);
}
}
}
}
}
// If the model doesn't have normals, compute the normals
if (model.vertexNormals.length === 0) {
model.computeNormals();
}
if (hasColoredVertices === hasColorlessVertices) {
// If both are true or both are false, throw an error because the model is inconsistent
throw new Error('Model coloring is inconsistent. Either all vertices should have colors or none should.');
}
return model;
}
/**
* @private
* STL files can be of two types, ASCII and Binary,
*
* We need to convert the arrayBuffer to an array of strings,
* to parse it as an ASCII file.
*/
function parseSTL(model, buffer) {
if (isBinary(buffer)) {
parseBinarySTL(model, buffer);
} else {
const reader = new DataView(buffer);
if (!('TextDecoder' in window)) {
console.warn(
'Sorry, ASCII STL loading only works in browsers that support TextDecoder (https://caniuse.com/#feat=textencoder)'
);
return model;
}
const decoder = new TextDecoder('utf-8');
const lines = decoder.decode(reader);
const lineArray = lines.split('\n');
parseASCIISTL(model, lineArray);
}
return model;
}
/**
* @private
* This function checks if the file is in ASCII format or in Binary format
*
* It is done by searching keyword `solid` at the start of the file.
*
* An ASCII STL data must begin with `solid` as the first six bytes.
* However, ASCII STLs lacking the SPACE after the `d` are known to be
* plentiful. So, check the first 5 bytes for `solid`.
*
* Several encodings, such as UTF-8, precede the text with up to 5 bytes:
* https://en.wikipedia.org/wiki/Byte_order_mark#Byte_order_marks_by_encoding
* Search for `solid` to start anywhere after those prefixes.
*/
function isBinary(data) {
const reader = new DataView(data);
// US-ASCII ordinal values for `s`, `o`, `l`, `i`, `d`
const solid = [115, 111, 108, 105, 100];
for (let off = 0; off < 5; off++) {
// If "solid" text is matched to the current offset, declare it to be an ASCII STL.
if (matchDataViewAt(solid, reader, off)) return false;
}
// Couldn't find "solid" text at the beginning; it is binary STL.
return true;
}
/**
* @private
* This function matches the `query` at the provided `offset`
*/
function matchDataViewAt(query, reader, offset) {
// Check if each byte in query matches the corresponding byte from the current offset
for (let i = 0, il = query.length; i < il; i++) {
if (query[i] !== reader.getUint8(offset + i, false)) return false;
}
return true;
}
/**
* @private
* This function parses the Binary STL files.
* https://en.wikipedia.org/wiki/STL_%28file_format%29#Binary_STL
*
* Currently there is no support for the colors provided in STL files.
*/
function parseBinarySTL(model, buffer) {
const reader = new DataView(buffer);
// Number of faces is present following the header
const faces = reader.getUint32(80, true);
let r,
g,
b,
hasColors = false,
colors;
let defaultR, defaultG, defaultB;
// Binary files contain 80-byte header, which is generally ignored.
for (let index = 0; index < 80 - 10; index++) {
// Check for `COLOR=`
if (
reader.getUint32(index, false) === 0x434f4c4f /*COLO*/ &&
reader.getUint8(index + 4) === 0x52 /*'R'*/ &&
reader.getUint8(index + 5) === 0x3d /*'='*/
) {
hasColors = true;
colors = [];
defaultR = reader.getUint8(index + 6) / 255;
defaultG = reader.getUint8(index + 7) / 255;
defaultB = reader.getUint8(index + 8) / 255;
// To be used when color support is added
// alpha = reader.getUint8(index + 9) / 255;
}
}
const dataOffset = 84;
const faceLength = 12 * 4 + 2;
// Iterate the faces
for (let face = 0; face < faces; face++) {
const start = dataOffset + face * faceLength;
const normalX = reader.getFloat32(start, true);
const normalY = reader.getFloat32(start + 4, true);
const normalZ = reader.getFloat32(start + 8, true);
if (hasColors) {
const packedColor = reader.getUint16(start + 48, true);
if ((packedColor & 0x8000) === 0) {
// facet has its own unique color
r = (packedColor & 0x1f) / 31;
g = ((packedColor >> 5) & 0x1f) / 31;
b = ((packedColor >> 10) & 0x1f) / 31;
} else {
r = defaultR;
g = defaultG;
b = defaultB;
}
}
const newNormal = new Vector(normalX, normalY, normalZ);
for (let i = 1; i <= 3; i++) {
const vertexstart = start + i * 12;
const newVertex = new Vector(
reader.getFloat32(vertexstart, true),
reader.getFloat32(vertexstart + 4, true),
reader.getFloat32(vertexstart + 8, true)
);
model.vertices.push(newVertex);
model.vertexNormals.push(newNormal);
if (hasColors) {
colors.push(r, g, b);
}
}
model.faces.push([3 * face, 3 * face + 1, 3 * face + 2]);
model.uvs.push([0, 0], [0, 0], [0, 0]);
}
return model;
}
/**
* @private
* ASCII STL file starts with `solid 'nameOfFile'`
* Then contain the normal of the face, starting with `facet normal`
* Next contain a keyword indicating the start of face vertex, `outer loop`
* Next comes the three vertex, starting with `vertex x y z`
* Vertices ends with `endloop`
* Face ends with `endfacet`
* Next face starts with `facet normal`
* The end of the file is indicated by `endsolid`
*/
function parseASCIISTL(model, lines) {
let state = '';
let curVertexIndex = [];
let newNormal, newVertex;
for (let iterator = 0; iterator < lines.length; ++iterator) {
const line = lines[iterator].trim();
const parts = line.split(' ');
for (let partsiterator = 0; partsiterator < parts.length; ++partsiterator) {
if (parts[partsiterator] === '') {
// Ignoring multiple whitespaces
parts.splice(partsiterator, 1);
}
}
if (parts.length === 0) {
// Remove newline
continue;
}
switch (state) {
case '': // First run
if (parts[0] !== 'solid') {
// Invalid state
console.error(line);
console.error(`Invalid state "${parts[0]}", should be "solid"`);
return;
} else {
state = 'solid';
}
break;
case 'solid': // First face
if (parts[0] !== 'facet' || parts[1] !== 'normal') {
// Invalid state
console.error(line);
console.error(
`Invalid state "${parts[0]}", should be "facet normal"`
);
return;
} else {
// Push normal for first face
newNormal = new Vector(
parseFloat(parts[2]),
parseFloat(parts[3]),
parseFloat(parts[4])
);
model.vertexNormals.push(newNormal, newNormal, newNormal);
state = 'facet normal';
}
break;
case 'facet normal': // After normal is defined
if (parts[0] !== 'outer' || parts[1] !== 'loop') {
// Invalid State
console.error(line);
console.error(`Invalid state "${parts[0]}", should be "outer loop"`);
return;
} else {
// Next should be vertices
state = 'vertex';
}
break;
case 'vertex':
if (parts[0] === 'vertex') {
//Vertex of triangle
newVertex = new Vector(
parseFloat(parts[1]),
parseFloat(parts[2]),
parseFloat(parts[3])
);
model.vertices.push(newVertex);
model.uvs.push([0, 0]);
curVertexIndex.push(model.vertices.indexOf(newVertex));
} else if (parts[0] === 'endloop') {
// End of vertices
model.faces.push(curVertexIndex);
curVertexIndex = [];
state = 'endloop';
} else {
// Invalid State
console.error(line);
console.error(
`Invalid state "${parts[0]}", should be "vertex" or "endloop"`
);
return;
}
break;
case 'endloop':
if (parts[0] !== 'endfacet') {
// End of face
console.error(line);
console.error(`Invalid state "${parts[0]}", should be "endfacet"`);
return;
} else {
state = 'endfacet';
}
break;
case 'endfacet':
if (parts[0] === 'endsolid') ; else if (parts[0] === 'facet' && parts[1] === 'normal') {
// Next face
newNormal = new Vector(
parseFloat(parts[2]),
parseFloat(parts[3]),
parseFloat(parts[4])
);
model.vertexNormals.push(newNormal, newNormal, newNormal);
state = 'facet normal';
} else {
// Invalid State
console.error(line);
console.error(
`Invalid state "${parts[0]
}", should be "endsolid" or "facet normal"`
);
return;
}
break;
default:
console.error(`Invalid state "${state}"`);
break;
}
}
return model;
}
/**
* Draws a <a href="#/p5.Geometry">p5.Geometry</a> object to the canvas.
*
* The parameter, `model`, is the
* <a href="#/p5.Geometry">p5.Geometry</a> object to draw.
* <a href="#/p5.Geometry">p5.Geometry</a> objects can be built with
* <a href="#/p5/buildGeometry">buildGeometry()</a>, or
* <a href="#/p5/beginGeometry">beginGeometry()</a> and
* <a href="#/p5/endGeometry">endGeometry()</a>. They can also be loaded from
* a file with <a href="#/p5/loadGeometry">loadGeometry()</a>.
*
* Note: `model()` can only be used in WebGL mode.
*
* @method model
* @param {p5.Geometry} model 3D shape to be drawn.
*
* @param {Number} [count=1] number of instances to draw.
* @example
* <div>
* <code>
* // Click and drag the mouse to view the scene from different angles.
*
* let shape;
*
* function setup() {
* createCanvas(100, 100, WEBGL);
*
* // Create the p5.Geometry object.
* shape = buildGeometry(createShape);
*
* describe('A white cone drawn on a gray background.');
* }
*
* function draw() {
* background(200);
*
* // Enable orbiting with the mouse.
* orbitControl();
*
* // Draw the p5.Geometry object.
* model(shape);
* }
*
* // Create p5.Geometry object from a single cone.
* function createShape() {
* cone();
* }
* </code>
* </div>
*
* <div>
* <code>
* // Click and drag the mouse to view the scene from different angles.
*
* let shape;
*
* function setup() {
* createCanvas(100, 100, WEBGL);
*
* // Create the p5.Geometry object.
* shape = buildGeometry(createArrow);
*
* describe('Two white arrows drawn on a gray background. The arrow on the right rotates slowly.');
* }
*
* function draw() {
* background(50);
*
* // Enable orbiting with the mouse.
* orbitControl();
*
* // Turn on the lights.
* lights();
*
* // Style the arrows.
* noStroke();
*
* // Draw the p5.Geometry object.
* model(shape);
*
* // Translate and rotate the coordinate system.
* translate(30, 0, 0);
* rotateZ(frameCount * 0.01);
*
* // Draw the p5.Geometry object again.
* model(shape);
* }
*
* function createArrow() {
* // Add shapes to the p5.Geometry object.
* push();
* rotateX(PI);
* cone(10);
* translate(0, -10, 0);
* cylinder(3, 20);
* pop();
* }
* </code>
* </div>
*
* <div>
* <code>
* // Click and drag the mouse to view the scene from different angles.
*
* let shape;
*
* async function setup() {
* shape = await loadModel('assets/octahedron.obj');
*
* createCanvas(100, 100, WEBGL);
*
* describe('A white octahedron drawn against a gray background.');
* }
*
* function draw() {
* background(200);
*
* // Enable orbiting with the mouse.
* orbitControl();
*
* // Draw the shape.
* model(shape);
* }
* </code>
* </div>
*/
fn.model = function (model, count = 1) {
this._assert3d('model');
// p5._validateParameters('model', arguments);
this._renderer.model(model, count);
};
/**
* Load a 3d model from an OBJ or STL string.
*
* OBJ and STL files lack a built-in sense of scale, causing models exported from different programs to vary in size.
* If your model doesn't display correctly, consider using `loadModel()` with `normalize` set to `true` to standardize its size.
* Further adjustments can be made using the `scale()` function.
*
* Also, the support for colored STL files is not present. STL files with color will be
* rendered without color properties.
*
* * Options can include:
* - `modelString`: Specifies the plain text string of either an stl or obj file to be loaded.
* - `fileType`: Defines the file extension of the model.
* - `normalize`: Enables standardized size scaling during loading if set to true.
* - `successCallback`: Callback for post-loading actions with the 3D model object.
* - `failureCallback`: Handles errors if model loading fails, receiving an event error.
* - `flipU`: Flips the U texture coordinates of the model.
* - `flipV`: Flips the V texture coordinates of the model.
*
*
* @method createModel
* @param {String} modelString String of the object to be loaded
* @param {String} [fileType] The file extension of the model
* (<code>.stl</code>, <code>.obj</code>).
* @param {Boolean} normalize If true, scale the model to a
* standardized size when loading
* @param {function(p5.Geometry)} [successCallback] Function to be called
* once the model is loaded. Will be passed
* the 3D model object.
* @param {function(Event)} [failureCallback] called with event error if
* the model fails to load.
* @return {p5.Geometry} the <a href="#/p5.Geometry">p5.Geometry</a> object
*
* @example
* <div>
* <code>
* const octahedron_model = `
* v 0.000000E+00 0.000000E+00 40.0000
* v 22.5000 22.5000 0.000000E+00
* v 22.5000 -22.5000 0.000000E+00
* v -22.5000 -22.5000 0.000000E+00
* v -22.5000 22.5000 0.000000E+00
* v 0.000000E+00 0.000000E+00 -40.0000
* f 1 2 3
* f 1 3 4
* f 1 4 5
* f 1 5 2
* f 6 5 4
* f 6 4 3
* f 6 3 2
* f 6 2 5
* `;
* //draw a spinning octahedron
* let octahedron;
*
* function setup() {
* createCanvas(100, 100, WEBGL);
* octahedron = createModel(octahedron_model, '.obj');
* describe('Vertically rotating 3D octahedron.');
* }
*
* function draw() {
* background(200);
* rotateX(frameCount * 0.01);
* rotateY(frameCount * 0.01);
* model(octahedron);
*}
* </code>
* </div>
*/
/**
* @method createModel
* @param {String} modelString
* @param {String} [fileType]
* @param {function(p5.Geometry)} [successCallback]
* @param {function(Event)} [failureCallback]
* @return {p5.Geometry} the <a href="#/p5.Geometry">p5.Geometry</a> object
*/
/**
* @method createModel
* @param {String} modelString
* @param {String} [fileType]
* @param {Object} [options]
* @param {function(p5.Geometry)} [options.successCallback]
* @param {function(Event)} [options.failureCallback]
* @param {boolean} [options.normalize]
* @param {boolean} [options.flipU]
* @param {boolean} [options.flipV]
* @return {p5.Geometry} the <a href="#/p5.Geometry">p5.Geometry</a> object
*/
let modelCounter = 0;
fn.createModel = function(modelString, fileType=' ', options) {
// p5._validateParameters('createModel', arguments);
let normalize= false;
let successCallback;
let failureCallback;
let flipU = false;
let flipV = false;
if (options && typeof options === 'object') {
normalize = options.normalize || false;
successCallback = options.successCallback;
failureCallback = options.failureCallback;
flipU = options.flipU || false;
flipV = options.flipV || false;
} else if (typeof options === 'boolean') {
normalize = options;
successCallback = arguments[3];
failureCallback = arguments[4];
} else {
successCallback = typeof arguments[2] === 'function' ? arguments[2] : undefined;
failureCallback = arguments[3];
}
const model = new p5.Geometry();
model.gid = `${fileType}|${normalize}|${modelCounter++}`;
if (fileType.match(/\.stl$/i)) {
try {
let uint8array = new TextEncoder().encode(modelString);
let arrayBuffer = uint8array.buffer;
parseSTL(model, arrayBuffer);
} catch (error) {
if (failureCallback) {
failureCallback(error);
} else {
p5._friendlyError('Error during parsing: ' + error.message);
}
return;
}
} else if (fileType.match(/\.obj$/i)) {
try {
const lines = modelString.split('\n');
parseObj(model, lines);
} catch (error) {
if (failureCallback) {
failureCallback(error);
} else {
p5._friendlyError('Error during parsing: ' + error.message);
}
return;
}
} else {
p5._friendlyFileLoadError(3, modelString);
if (failureCallback) {
failureCallback();
} else {
p5._friendlyError(
'Sorry, the file type is invalid. Only OBJ and STL files are supported.'
);
}
}
if (normalize) {
model.normalize();
}
if (flipU) {
model.flipU();
}
if (flipV) {
model.flipV();
}
model._makeTriangleEdges();
if (typeof successCallback === 'function') {
successCallback(model);
}
return model;
};
}
if(typeof p5 !== 'undefined'){
loading(p5, p5.prototype);
}
export { loading as default };