From bfec2d4531c86a77333040972bcfc455bb736141 Mon Sep 17 00:00:00 2001 From: Nixxx19 <185968020+Nixxx19@users.noreply.github.com> Date: Sun, 7 Jun 2026 15:07:47 +0530 Subject: [PATCH 1/4] add geometrypart class --- src/webgl/index.js | 2 ++ src/webgl/p5.GeometryPart.js | 48 ++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 src/webgl/p5.GeometryPart.js diff --git a/src/webgl/index.js b/src/webgl/index.js index 9bf7a3c353..97281c368a 100644 --- a/src/webgl/index.js +++ b/src/webgl/index.js @@ -8,6 +8,7 @@ import renderBuffer from './p5.RenderBuffer'; import quat from './p5.Quat'; import matrix from '../math/p5.Matrix'; import geometry from './p5.Geometry'; +import geometryPart from './p5.GeometryPart'; import framebuffer from './p5.Framebuffer'; import dataArray from './p5.DataArray'; import camera from './p5.Camera'; @@ -28,6 +29,7 @@ export default function(p5){ p5.registerAddon(quat); p5.registerAddon(matrix); p5.registerAddon(geometry); + p5.registerAddon(geometryPart); p5.registerAddon(camera); p5.registerAddon(framebuffer); p5.registerAddon(dataArray); diff --git a/src/webgl/p5.GeometryPart.js b/src/webgl/p5.GeometryPart.js new file mode 100644 index 0000000000..25ec9efcfa --- /dev/null +++ b/src/webgl/p5.GeometryPart.js @@ -0,0 +1,48 @@ +/** + * @module Shape + * @submodule 3D Primitives + * @for p5 + */ + +// fresh part state. fields use p5 names (fill, texture...), not obj/mtl tokens. +// importers translate into this and drop anything we can't draw yet. +function createPartState() { + return { + fill: null, // Kd + ambientColor: null, // Ka + specularColor: null, // Ks + shininess: null, // Ns + opacity: null, // d + texture: null // map_Kd + }; +} + +// one part of a geometry. a multi-material model is a p5.Geometry made of +// several parts, each holding the verts/faces/uvs for one material plus the +// state to draw them. single-material models are just one part. +class GeometryPart { + constructor(gid, partState) { + // renderer caches buffers by this, derived from the parent geometry's gid + this.gid = gid; + + this.vertices = []; + this.vertexNormals = []; + this.faces = []; + this.uvs = []; + this.vertexColors = []; + + this.partState = partState || createPartState(); + this.dirtyFlags = {}; + } +} + +function geometryPart(p5, fn) { + p5.GeometryPart = GeometryPart; +} + +export default geometryPart; +export { GeometryPart, createPartState }; + +if (typeof p5 !== 'undefined') { + geometryPart(p5, p5.prototype); +} From e812daef4c1cc4941486231ad7f0bd9f7a416314 Mon Sep 17 00:00:00 2001 From: Nixxx19 <185968020+Nixxx19@users.noreply.github.com> Date: Sun, 7 Jun 2026 15:07:48 +0530 Subject: [PATCH 2/4] wrap geometry in a single part --- src/webgl/p5.Geometry.js | 42 ++++++++++++++ test/unit/webgl/p5.GeometryPart.js | 89 ++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 test/unit/webgl/p5.GeometryPart.js diff --git a/src/webgl/p5.Geometry.js b/src/webgl/p5.Geometry.js index 67658cfb49..8ac4656961 100644 --- a/src/webgl/p5.Geometry.js +++ b/src/webgl/p5.Geometry.js @@ -8,6 +8,7 @@ import * as constants from '../core/constants'; import { DataArray } from './p5.DataArray'; +import { GeometryPart } from './p5.GeometryPart'; import { Vector } from '../math/p5.Vector'; import { downloadFile } from '../io/utilities'; @@ -63,9 +64,50 @@ class Geometry { this.gid = `_p5_Geometry_${Geometry.nextId}`; Geometry.nextId++; + + // every geometry is one or more parts (see p5.GeometryPart). loaders that + // know about materials fill _parts themselves; anything built the old way + // gets wrapped in a single part below. + this._parts = []; + if (callback instanceof Function) { callback.call(this); } + + if (this._parts.length === 0) { + this._wrapInSinglePart(); + } + } + + // wrap this geometry's own buffers in one part, for anything built the old way + // (primitives, new p5.Geometry(cb)). the part is a live view onto our arrays, + // not a copy, so reassigning an array or changing gid later can't desync it. + _wrapInSinglePart() { + const geometry = this; + const part = new GeometryPart(`${this.gid}|part0`); + for (const field of [ + 'vertices', + 'vertexNormals', + 'faces', + 'uvs', + 'vertexColors' + ]) { + Object.defineProperty(part, field, { + get() { + return geometry[field]; + }, + enumerable: true, + configurable: true + }); + } + Object.defineProperty(part, 'gid', { + get() { + return `${geometry.gid}|part0`; + }, + enumerable: true, + configurable: true + }); + this._parts = [part]; } /** diff --git a/test/unit/webgl/p5.GeometryPart.js b/test/unit/webgl/p5.GeometryPart.js new file mode 100644 index 0000000000..e481e93cf2 --- /dev/null +++ b/test/unit/webgl/p5.GeometryPart.js @@ -0,0 +1,89 @@ +import p5 from '../../../src/app.js'; + +suite('p5.GeometryPart', function() { + let myp5; + + beforeAll(function() { + myp5 = new p5(function(p) { + p.setup = function() {}; + p.draw = function() {}; + }); + }); + + afterAll(function() { + myp5.remove(); + }); + + test('is registered on p5', function() { + expect(p5.GeometryPart).toBeDefined(); + }); + + test('starts with empty buffers', function() { + const part = new p5.GeometryPart('_part_test'); + expect(part.vertices).toEqual([]); + expect(part.vertexNormals).toEqual([]); + expect(part.faces).toEqual([]); + expect(part.uvs).toEqual([]); + expect(part.vertexColors).toEqual([]); + }); + + test('keeps the gid it was given', function() { + const part = new p5.GeometryPart('parent|part0'); + expect(part.gid).toEqual('parent|part0'); + }); + + test('defaults part state to nulls', function() { + const part = new p5.GeometryPart('_part_test'); + expect(part.partState).toEqual({ + fill: null, + ambientColor: null, + specularColor: null, + shininess: null, + opacity: null, + texture: null + }); + }); + + test('uses the part state it was given', function() { + const state = { fill: [255, 0, 0], opacity: 0.5 }; + const part = new p5.GeometryPart('_part_test', state); + expect(part.partState).toBe(state); + }); + + test('each part gets its own buffers', function() { + const a = new p5.GeometryPart('a'); + const b = new p5.GeometryPart('b'); + a.vertices.push([0, 0, 0]); + expect(b.vertices).toEqual([]); + }); + + suite('single-part wrap on p5.Geometry', function() { + test('a plain geometry gets exactly one part', function() { + const geom = new p5.Geometry(undefined, undefined, undefined, + myp5._renderer); + expect(geom._parts.length).toEqual(1); + }); + + test('the part is a live view onto the geometry buffers', function() { + const geom = new p5.Geometry(undefined, undefined, undefined, + myp5._renderer); + expect(geom._parts[0].vertices).toBe(geom.vertices); + expect(geom._parts[0].faces).toBe(geom.faces); + }); + + test('the part gid tracks the geometry gid after it changes', function() { + const geom = new p5.Geometry(undefined, undefined, undefined, + myp5._renderer); + geom.gid = 'my-model'; + expect(geom._parts[0].gid).toEqual('my-model|part0'); + }); + + test('a built-in primitive also gets one part', function() { + const geom = new p5.Geometry(1, 1, function() { + this.vertices.push(new p5.Vector(0, 0, 0)); + }, myp5._renderer); + expect(geom._parts.length).toEqual(1); + expect(geom._parts[0].vertices.length).toEqual(1); + }); + }); +}); From 962112d65fd119db858d8ac5e9e17214f81ac00c Mon Sep 17 00:00:00 2001 From: Nixxx19 <185968020+Nixxx19@users.noreply.github.com> Date: Sun, 7 Jun 2026 15:07:49 +0530 Subject: [PATCH 3/4] split obj models into per-material parts --- src/webgl/loading.js | 199 +++++++++++++++++++++++++++------- test/unit/assets/textured.mtl | 4 + test/unit/assets/textured.obj | 9 ++ test/unit/io/loadModel.js | 56 ++++++++++ test/unit/io/parseMtl.js | 76 +++++++++++++ 5 files changed, 304 insertions(+), 40 deletions(-) create mode 100644 test/unit/assets/textured.mtl create mode 100644 test/unit/assets/textured.obj create mode 100644 test/unit/io/parseMtl.js diff --git a/src/webgl/loading.js b/src/webgl/loading.js index 5edd0d0791..4d17c9bfd2 100755 --- a/src/webgl/loading.js +++ b/src/webgl/loading.js @@ -5,6 +5,7 @@ */ import { Geometry } from './p5.Geometry'; +import { GeometryPart, createPartState } from './p5.GeometryPart'; import { Vector } from '../math/p5.Vector'; import { request } from '../io/files'; @@ -17,6 +18,155 @@ async function fileExists(url) { } } +// parse mtl text into a map of material name -> props. split from the file +// request so it's testable on its own. +function parseMtlData(data) { + let currentMaterial = null; + const materials = {}; + const lines = data.split('\n'); + + for (let line = 0; line < lines.length; ++line) { + const tokens = lines[line].trim().split(/\s+/); + if (tokens[0] === 'newmtl') { + currentMaterial = tokens[1]; + materials[currentMaterial] = {}; + } else if (!currentMaterial) { + continue; + } 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] === 'Ns') { + //specular exponent (shininess) + materials[currentMaterial].shininess = parseFloat(tokens[1]); + } else if (tokens[0] === 'd') { + //dissolve, 1 is fully opaque + materials[currentMaterial].opacity = parseFloat(tokens[1]); + } else if (tokens[0] === 'Tr') { + //transparency, the inverse of d + materials[currentMaterial].opacity = 1 - parseFloat(tokens[1]); + } else if (tokens[0] === 'illum') { + //illumination model + materials[currentMaterial].illuminationModel = parseInt(tokens[1]); + } else if (tokens[0] === 'map_Kd') { + //diffuse texture + materials[currentMaterial].texturePath = tokens[1]; + } else if (tokens[0] === 'map_Ka') { + //ambient texture + materials[currentMaterial].ambientTexturePath = tokens[1]; + } else if (tokens[0] === 'map_Ks') { + //specular texture + materials[currentMaterial].specularTexturePath = tokens[1]; + } else if (tokens[0] === 'map_Bump' || tokens[0] === 'bump') { + //bump map. -bm etc can precede the path so take the last token. parsed + //but not used until the renderer handles it. + materials[currentMaterial].bumpTexturePath = tokens[tokens.length - 1]; + } + } + + return materials; +} + +// mtl material -> part state in p5's vocab. anything we can't draw yet is left +// off until support lands. +function mtlToPartState(material) { + const state = createPartState(); + if (!material) return state; + if (material.diffuseColor) state.fill = material.diffuseColor; + if (material.ambientColor) state.ambientColor = material.ambientColor; + if (material.specularColor) state.specularColor = material.specularColor; + if (material.shininess !== undefined) state.shininess = material.shininess; + if (material.opacity !== undefined) state.opacity = material.opacity; + if (material.texture) state.texture = material.texture; + return state; +} + +// load each material's diffuse texture (map_Kd) and hang it on the material so +// it lands on the part state. paths resolve relative to the model file, a +// texture that fails just gets skipped. no-op if there's no loadImage. only +// map_Kd for now since that's all the renderer can use. +async function loadMaterialTextures(materials, modelPath, instance) { + if (!instance || typeof instance.loadImage !== 'function') return; + + const slash = modelPath.lastIndexOf('/'); + const folder = slash >= 0 ? modelPath.slice(0, slash) : ''; + const resolve = file => (folder ? `${folder}/${file}` : file); + + const jobs = []; + for (const name in materials) { + const material = materials[name]; + if (!material.texturePath) continue; + const url = resolve(material.texturePath); + jobs.push( + instance.loadImage(url) + .then(img => { + material.texture = img; + }) + .catch(() => { + console.warn(`Texture not found, skipping: ${url}`); + }) + ); + } + + await Promise.all(jobs); +} + +// split the model's faces into one part per material. the combined arrays stay +// as the aggregate; each part gets its own localised verts with faces re-indexed +// against them, plus its material's state. +function buildMaterialParts(model, faceMaterials, materials) { + // one group per material, plus a null group for faces before any usemtl so + // none get dropped. no materials at all -> keep the default wrap. + const names = [...new Set(faceMaterials)]; + if (!names.some(name => name != null)) return; + + const hasUvs = model.uvs.length > 0; + const hasNormals = model.vertexNormals.length > 0; + const parts = []; + + for (const name of names) { + const part = new GeometryPart( + `${model.gid}|part${parts.length}`, + mtlToPartState(materials[name]) + ); + // global vertex index -> this part's local index, added on first use + const localIndex = new Map(); + for (let fi = 0; fi < model.faces.length; fi++) { + if (faceMaterials[fi] !== name) continue; + const localFace = model.faces[fi].map(vi => { + if (!localIndex.has(vi)) { + localIndex.set(vi, part.vertices.length); + part.vertices.push(model.vertices[vi]); + if (hasUvs) part.uvs.push(model.uvs[vi]); + if (hasNormals) part.vertexNormals.push(model.vertexNormals[vi]); + } + return localIndex.get(vi); + }); + part.faces.push(localFace); + } + parts.push(part); + } + + model._parts = parts; +} + function loading(p5, fn){ /** * Loads a 3D model to create a @@ -446,6 +596,7 @@ function loading(p5, fn){ const lines = data.split('\n'); const parsedMaterials = await getMaterials(lines); + await loadMaterialTextures(parsedMaterials, path, this); const cb = () => { parseObj(model, lines, parsedMaterials); @@ -482,47 +633,8 @@ function loading(p5, fn){ * @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; + return parseMtlData(data); } /** @@ -557,6 +669,8 @@ function loading(p5, fn){ // Map from source index → Map of material → destination index const usedVerts = {}; // Track colored vertices let currentMaterial = null; + // material per kept face, aligned with model.faces, for bucketing later + const faceMaterials = []; let hasColoredVertices = false; let hasColorlessVertices = false; for (let line = 0; line < lines.length; ++line) { @@ -642,6 +756,7 @@ function loading(p5, fn){ face[1] !== face[2] ) { model.faces.push(face); + faceMaterials.push(currentMaterial); } } } @@ -655,6 +770,9 @@ function loading(p5, fn){ model.vertexColors = []; } + // bucket faces into per-material parts (aggregate arrays above stay as-is) + buildMaterialParts(model, faceMaterials, materials); + return model; } @@ -1296,6 +1414,7 @@ function loading(p5, fn){ } export default loading; +export { parseMtlData, mtlToPartState, buildMaterialParts }; if(typeof p5 !== 'undefined'){ loading(p5, p5.prototype); diff --git a/test/unit/assets/textured.mtl b/test/unit/assets/textured.mtl new file mode 100644 index 0000000000..4dd92e97dd --- /dev/null +++ b/test/unit/assets/textured.mtl @@ -0,0 +1,4 @@ +newmtl mat0 +Kd 1 1 1 +Ns 50 +map_Kd cat.jpg diff --git a/test/unit/assets/textured.obj b/test/unit/assets/textured.obj new file mode 100644 index 0000000000..65d6fa38b9 --- /dev/null +++ b/test/unit/assets/textured.obj @@ -0,0 +1,9 @@ +mtllib textured.mtl +v 0 0 0 +v 1 0 0 +v 0 1 0 +vt 0 0 +vt 1 0 +vt 0 1 +usemtl mat0 +f 1/1 2/2 3/3 diff --git a/test/unit/io/loadModel.js b/test/unit/io/loadModel.js index 590a12bb1e..184240992a 100644 --- a/test/unit/io/loadModel.js +++ b/test/unit/io/loadModel.js @@ -79,6 +79,62 @@ suite('loadModel', function() { assert.deepEqual(model.vertexColors, expectedColors); }); + test('splits a multi-material OBJ into one part per material', async function() { + const model = await mockP5Prototype.loadModel(validObjFileforMtl); + + // octa-color.obj uses 8 materials, one per face + assert.equal(model._parts.length, 8); + + // every face ends up in exactly one part + const totalFaces = model._parts.reduce((sum, p) => sum + p.faces.length, 0); + assert.equal(totalFaces, model.faces.length); + + // first material (m000001) is Kd 0 0 0.5 -> part fill + assert.deepEqual(model._parts[0].partState.fill, [0, 0, 0.5]); + assert.equal(model._parts[0].partState.shininess, 100); + assert.equal(model._parts[0].partState.opacity, 1); + + // faces re-indexed against each part's own localised verts + for (const part of model._parts) { + for (const face of part.faces) { + for (const idx of face) { + assert.ok(idx >= 0 && idx < part.vertices.length); + } + } + } + }); + + test('loads the diffuse texture (map_Kd) onto the part state', async function() { + const fakeImage = { width: 1, height: 1 }; + mockP5Prototype.loadImage = async url => { + // texture path is resolved relative to the model folder + assert.ok(url.endsWith('/cat.jpg')); + return fakeImage; + }; + try { + const model = await mockP5Prototype.loadModel('/test/unit/assets/textured.obj'); + // single material -> one part carrying that material's state + assert.equal(model._parts.length, 1); + assert.equal(model._parts[0].partState.texture, fakeImage); + assert.equal(model._parts[0].partState.shininess, 50); + } finally { + delete mockP5Prototype.loadImage; + } + }); + + test('a texture that fails to load is skipped without failing the model', async function() { + mockP5Prototype.loadImage = async () => { + throw new Error('Not Found'); + }; + try { + const model = await mockP5Prototype.loadModel('/test/unit/assets/textured.obj'); + assert.equal(model._parts.length, 1); + assert.equal(model._parts[0].partState.texture, null); + } finally { + delete mockP5Prototype.loadImage; + } + }); + test('mixed material coloring loads model with sentinel colors for uncolored vertices', async function() { const model = await mockP5Prototype.loadModel(inconsistentColorObjFile); assert.instanceOf(model, Geometry); diff --git a/test/unit/io/parseMtl.js b/test/unit/io/parseMtl.js new file mode 100644 index 0000000000..5201b83c48 --- /dev/null +++ b/test/unit/io/parseMtl.js @@ -0,0 +1,76 @@ +import { parseMtlData, mtlToPartState } from '../../../src/webgl/loading'; + +suite('parseMtlData', function() { + test('parses a full set of material tokens', function() { + const mtl = [ + 'newmtl shiny', + 'Kd 0.1 0.2 0.3', + 'Ka 0.4 0.5 0.6', + 'Ks 0.7 0.8 0.9', + 'Ns 64', + 'd 0.5', + 'illum 2', + 'map_Kd diffuse.png', + 'map_Ka ambient.png', + 'map_Ks specular.png', + 'map_Bump -bm 0.5 bump.png' + ].join('\n'); + + const materials = parseMtlData(mtl); + const m = materials.shiny; + + expect(m.diffuseColor).toEqual([0.1, 0.2, 0.3]); + expect(m.ambientColor).toEqual([0.4, 0.5, 0.6]); + expect(m.specularColor).toEqual([0.7, 0.8, 0.9]); + expect(m.shininess).toEqual(64); + expect(m.opacity).toEqual(0.5); + expect(m.illuminationModel).toEqual(2); + expect(m.texturePath).toEqual('diffuse.png'); + expect(m.ambientTexturePath).toEqual('ambient.png'); + expect(m.specularTexturePath).toEqual('specular.png'); + // bump options like -bm precede the path, so the path is the last token. + expect(m.bumpTexturePath).toEqual('bump.png'); + }); + + test('Tr is read as the inverse of d', function() { + const materials = parseMtlData('newmtl glass\nTr 0.25'); + expect(materials.glass.opacity).toEqual(0.75); + }); + + test('keeps each material separate', function() { + const materials = parseMtlData( + 'newmtl a\nKd 1 0 0\nnewmtl b\nKd 0 1 0' + ); + expect(materials.a.diffuseColor).toEqual([1, 0, 0]); + expect(materials.b.diffuseColor).toEqual([0, 1, 0]); + }); + + test('ignores lines before any newmtl', function() { + const materials = parseMtlData('Kd 1 1 1\nnewmtl a\nKd 0 0 0'); + expect(materials.a.diffuseColor).toEqual([0, 0, 0]); + expect(Object.keys(materials)).toEqual(['a']); + }); +}); + +suite('mtlToPartState', function() { + test('maps mtl fields onto p5 part-state vocabulary', function() { + const state = mtlToPartState({ + diffuseColor: [1, 0, 0], + ambientColor: [0, 1, 0], + specularColor: [0, 0, 1], + shininess: 32, + opacity: 0.5 + }); + expect(state.fill).toEqual([1, 0, 0]); + expect(state.ambientColor).toEqual([0, 1, 0]); + expect(state.specularColor).toEqual([0, 0, 1]); + expect(state.shininess).toEqual(32); + expect(state.opacity).toEqual(0.5); + }); + + test('returns an all-null state for a missing material', function() { + const state = mtlToPartState(undefined); + expect(state.fill).toBeNull(); + expect(state.texture).toBeNull(); + }); +}); From b086d81545c640ef07ee5e19699cdd66ab1afcbf Mon Sep 17 00:00:00 2001 From: nityam Date: Wed, 10 Jun 2026 17:52:31 +0530 Subject: [PATCH 4/4] drop opacity from part state, make parts public --- src/webgl/loading.js | 3 +-- src/webgl/p5.Geometry.js | 8 ++++---- src/webgl/p5.GeometryPart.js | 1 - test/unit/io/loadModel.js | 21 ++++++++++----------- test/unit/io/parseMtl.js | 4 +--- test/unit/webgl/p5.GeometryPart.js | 13 ++++++------- 6 files changed, 22 insertions(+), 28 deletions(-) diff --git a/src/webgl/loading.js b/src/webgl/loading.js index 4d17c9bfd2..4e8e799db5 100755 --- a/src/webgl/loading.js +++ b/src/webgl/loading.js @@ -93,7 +93,6 @@ function mtlToPartState(material) { if (material.ambientColor) state.ambientColor = material.ambientColor; if (material.specularColor) state.specularColor = material.specularColor; if (material.shininess !== undefined) state.shininess = material.shininess; - if (material.opacity !== undefined) state.opacity = material.opacity; if (material.texture) state.texture = material.texture; return state; } @@ -164,7 +163,7 @@ function buildMaterialParts(model, faceMaterials, materials) { parts.push(part); } - model._parts = parts; + model.parts = parts; } function loading(p5, fn){ diff --git a/src/webgl/p5.Geometry.js b/src/webgl/p5.Geometry.js index 8ac4656961..a41f2b7092 100644 --- a/src/webgl/p5.Geometry.js +++ b/src/webgl/p5.Geometry.js @@ -66,15 +66,15 @@ class Geometry { Geometry.nextId++; // every geometry is one or more parts (see p5.GeometryPart). loaders that - // know about materials fill _parts themselves; anything built the old way + // know about materials fill parts themselves; anything built the old way // gets wrapped in a single part below. - this._parts = []; + this.parts = []; if (callback instanceof Function) { callback.call(this); } - if (this._parts.length === 0) { + if (this.parts.length === 0) { this._wrapInSinglePart(); } } @@ -107,7 +107,7 @@ class Geometry { enumerable: true, configurable: true }); - this._parts = [part]; + this.parts = [part]; } /** diff --git a/src/webgl/p5.GeometryPart.js b/src/webgl/p5.GeometryPart.js index 25ec9efcfa..748121fd78 100644 --- a/src/webgl/p5.GeometryPart.js +++ b/src/webgl/p5.GeometryPart.js @@ -12,7 +12,6 @@ function createPartState() { ambientColor: null, // Ka specularColor: null, // Ks shininess: null, // Ns - opacity: null, // d texture: null // map_Kd }; } diff --git a/test/unit/io/loadModel.js b/test/unit/io/loadModel.js index 184240992a..9538021405 100644 --- a/test/unit/io/loadModel.js +++ b/test/unit/io/loadModel.js @@ -83,19 +83,18 @@ suite('loadModel', function() { const model = await mockP5Prototype.loadModel(validObjFileforMtl); // octa-color.obj uses 8 materials, one per face - assert.equal(model._parts.length, 8); + assert.equal(model.parts.length, 8); // every face ends up in exactly one part - const totalFaces = model._parts.reduce((sum, p) => sum + p.faces.length, 0); + const totalFaces = model.parts.reduce((sum, p) => sum + p.faces.length, 0); assert.equal(totalFaces, model.faces.length); // first material (m000001) is Kd 0 0 0.5 -> part fill - assert.deepEqual(model._parts[0].partState.fill, [0, 0, 0.5]); - assert.equal(model._parts[0].partState.shininess, 100); - assert.equal(model._parts[0].partState.opacity, 1); + assert.deepEqual(model.parts[0].partState.fill, [0, 0, 0.5]); + assert.equal(model.parts[0].partState.shininess, 100); // faces re-indexed against each part's own localised verts - for (const part of model._parts) { + for (const part of model.parts) { for (const face of part.faces) { for (const idx of face) { assert.ok(idx >= 0 && idx < part.vertices.length); @@ -114,9 +113,9 @@ suite('loadModel', function() { try { const model = await mockP5Prototype.loadModel('/test/unit/assets/textured.obj'); // single material -> one part carrying that material's state - assert.equal(model._parts.length, 1); - assert.equal(model._parts[0].partState.texture, fakeImage); - assert.equal(model._parts[0].partState.shininess, 50); + assert.equal(model.parts.length, 1); + assert.equal(model.parts[0].partState.texture, fakeImage); + assert.equal(model.parts[0].partState.shininess, 50); } finally { delete mockP5Prototype.loadImage; } @@ -128,8 +127,8 @@ suite('loadModel', function() { }; try { const model = await mockP5Prototype.loadModel('/test/unit/assets/textured.obj'); - assert.equal(model._parts.length, 1); - assert.equal(model._parts[0].partState.texture, null); + assert.equal(model.parts.length, 1); + assert.equal(model.parts[0].partState.texture, null); } finally { delete mockP5Prototype.loadImage; } diff --git a/test/unit/io/parseMtl.js b/test/unit/io/parseMtl.js index 5201b83c48..5b9c2a158c 100644 --- a/test/unit/io/parseMtl.js +++ b/test/unit/io/parseMtl.js @@ -58,14 +58,12 @@ suite('mtlToPartState', function() { diffuseColor: [1, 0, 0], ambientColor: [0, 1, 0], specularColor: [0, 0, 1], - shininess: 32, - opacity: 0.5 + shininess: 32 }); expect(state.fill).toEqual([1, 0, 0]); expect(state.ambientColor).toEqual([0, 1, 0]); expect(state.specularColor).toEqual([0, 0, 1]); expect(state.shininess).toEqual(32); - expect(state.opacity).toEqual(0.5); }); test('returns an all-null state for a missing material', function() { diff --git a/test/unit/webgl/p5.GeometryPart.js b/test/unit/webgl/p5.GeometryPart.js index e481e93cf2..ac7868f223 100644 --- a/test/unit/webgl/p5.GeometryPart.js +++ b/test/unit/webgl/p5.GeometryPart.js @@ -39,7 +39,6 @@ suite('p5.GeometryPart', function() { ambientColor: null, specularColor: null, shininess: null, - opacity: null, texture: null }); }); @@ -61,29 +60,29 @@ suite('p5.GeometryPart', function() { test('a plain geometry gets exactly one part', function() { const geom = new p5.Geometry(undefined, undefined, undefined, myp5._renderer); - expect(geom._parts.length).toEqual(1); + expect(geom.parts.length).toEqual(1); }); test('the part is a live view onto the geometry buffers', function() { const geom = new p5.Geometry(undefined, undefined, undefined, myp5._renderer); - expect(geom._parts[0].vertices).toBe(geom.vertices); - expect(geom._parts[0].faces).toBe(geom.faces); + expect(geom.parts[0].vertices).toBe(geom.vertices); + expect(geom.parts[0].faces).toBe(geom.faces); }); test('the part gid tracks the geometry gid after it changes', function() { const geom = new p5.Geometry(undefined, undefined, undefined, myp5._renderer); geom.gid = 'my-model'; - expect(geom._parts[0].gid).toEqual('my-model|part0'); + expect(geom.parts[0].gid).toEqual('my-model|part0'); }); test('a built-in primitive also gets one part', function() { const geom = new p5.Geometry(1, 1, function() { this.vertices.push(new p5.Vector(0, 0, 0)); }, myp5._renderer); - expect(geom._parts.length).toEqual(1); - expect(geom._parts[0].vertices.length).toEqual(1); + expect(geom.parts.length).toEqual(1); + expect(geom.parts[0].vertices.length).toEqual(1); }); }); });