diff --git a/api/csg.js b/api/csg.js index d41405be..32558ba0 100644 --- a/api/csg.js +++ b/api/csg.js @@ -5,6 +5,148 @@ export function setFlockReference(ref) { } export const flockCSG = { + _getMeshForCSG(mesh, context = "CSG") { + if (!mesh) return null; + if (typeof mesh.getVerticesData === "function") { + const positions = mesh.getVerticesData( + flock.BABYLON.VertexBuffer.PositionKind, + ); + if (positions && positions.length > 0) return mesh; + } + const childMeshes = + typeof mesh.getChildMeshes === "function" + ? mesh.getChildMeshes() + : typeof mesh.getDescendants === "function" + ? mesh.getDescendants() + : []; + const childSummary = childMeshes.map((child) => ({ + name: child.name, + vertices: + typeof child.getTotalVertices === "function" + ? child.getTotalVertices() + : 0, + })); + for (const child of childMeshes) { + const positions = child.getVerticesData?.( + flock.BABYLON.VertexBuffer.PositionKind, + ); + const totalVertices = + typeof child.getTotalVertices === "function" + ? child.getTotalVertices() + : 0; + if ( + (positions && positions.length > 0) || + (totalVertices && totalVertices > 0) + ) { + if (flock.manifoldDebug) { + console.log( + `[${context}] Using child mesh for CSG: ${child.name}`, + ); + } + return child; + } + } + if (flock.manifoldDebug) { + console.log(`[${context}] Mesh has no geometry: ${mesh.name}`, { + childCount: childMeshes.length, + children: childSummary, + }); + } + console.warn(`[${context}] No mesh with positions found: ${mesh.name}`, { + childCount: childMeshes.length, + children: childSummary, + }); + return null; + }, + _ensureMeshForCSG(mesh, context = "CSG") { + if (!mesh || typeof mesh.getVerticesData !== "function") return false; + let positions = mesh.getVerticesData( + flock.BABYLON.VertexBuffer.PositionKind, + ); + let indices = mesh.getIndices?.(); + const hasPositions = !!positions && positions.length > 0; + const hasIndices = !!indices && indices.length > 0; + + if (!hasPositions && mesh.metadata?.manifold?.toMesh) { + try { + const meshData = mesh.metadata.manifold.toMesh(); + const flatten = (data) => + Array.isArray(data) + ? data.flatMap((entry) => + Array.isArray(entry) + ? entry + : typeof entry === "object" + ? [ + entry.x ?? entry[0], + entry.y ?? entry[1], + entry.z ?? entry[2], + ] + : entry, + ) + : Array.from(data || []); + positions = flatten( + meshData.positions || + meshData.vertices || + meshData.vertProperties || + meshData.verts || + meshData.triangles, + ); + indices = flatten( + meshData.indices || meshData.triangles || meshData.triVerts, + ); + if (!indices.length && positions.length) { + indices = Array.from( + { length: positions.length / 3 }, + (_, i) => i, + ); + } + const vertexData = new flock.BABYLON.VertexData(); + vertexData.positions = positions; + vertexData.indices = indices; + vertexData.applyToMesh(mesh, true); + } catch (error) { + console.warn( + `[${context}] Failed to rebuild mesh from manifold data: ${mesh.name}`, + error, + ); + } + } + + positions = mesh.getVerticesData( + flock.BABYLON.VertexBuffer.PositionKind, + ); + indices = mesh.getIndices?.(); + const updatedHasPositions = !!positions && positions.length > 0; + const updatedHasIndices = !!indices && indices.length > 0; + + if (updatedHasPositions && !updatedHasIndices) { + indices = Array.from( + { length: positions.length / 3 }, + (_, i) => i, + ); + const vertexData = new flock.BABYLON.VertexData(); + vertexData.positions = positions; + vertexData.indices = indices; + vertexData.applyToMesh(mesh, true); + } + + const ready = + updatedHasPositions && (updatedHasIndices || indices?.length > 0); + if (!ready) { + console.warn( + `[${context}] Mesh is missing positions or indices: ${mesh.name}`, + { + textSource: mesh.metadata?.textSource, + }, + ); + } else if (flock.manifoldDebug) { + console.log(`[${context}] Mesh ready for CSG: ${mesh.name}`, { + positions: positions?.length ?? 0, + indices: indices?.length ?? 0, + }); + } + return ready; + }, mergeCompositeMesh(meshes) { if (!meshes || meshes.length === 0) return null; @@ -41,7 +183,26 @@ export const flockCSG = { firstMesh.flipFaces(); } } - let baseCSG = flock.BABYLON.CSG2.FromMesh(firstMesh, false); + const resolvedBase = this._getMeshForCSG( + firstMesh, + "mergeMeshes", + ); + if (!resolvedBase) { + console.warn( + "[mergeMeshes] Base mesh missing positions or indices.", + ); + return null; + } + if (!this._ensureMeshForCSG(resolvedBase, "mergeMeshes")) { + console.warn( + "[mergeMeshes] Base mesh missing positions or indices.", + ); + return null; + } + let baseCSG = flock.BABYLON.CSG2.FromMesh( + resolvedBase, + false, + ); // Merge subsequent meshes validMeshes.slice(1).forEach((mesh) => { @@ -54,11 +215,24 @@ export const flockCSG = { mesh.flipFaces(); } } - const meshCSG = flock.BABYLON.CSG2.FromMesh( + const resolvedMesh = this._getMeshForCSG( mesh, - false, + "mergeMeshes", ); - baseCSG = baseCSG.add(meshCSG); + if ( + resolvedMesh && + this._ensureMeshForCSG(resolvedMesh, "mergeMeshes") + ) { + const meshCSG = flock.BABYLON.CSG2.FromMesh( + resolvedMesh, + false, + ); + baseCSG = baseCSG.add(meshCSG); + } else { + console.warn( + `[mergeMeshes] Skipping mesh without indices: ${mesh.name}`, + ); + } }); const mergedMesh1 = baseCSG.toMesh( @@ -212,9 +386,25 @@ export const flockCSG = { actualBase, "baseDuplicate", ); - let outerCSG = tryCSG("FromMesh(baseDuplicate)", () => - flock.BABYLON.CSG2.FromMesh(baseDuplicate, false), + const resolvedBaseDuplicate = this._getMeshForCSG( + baseDuplicate, + "subtractMeshes", ); + let outerCSG = tryCSG("FromMesh(baseDuplicate)", () => { + if ( + !resolvedBaseDuplicate || + !this._ensureMeshForCSG( + resolvedBaseDuplicate, + "subtractMeshes", + ) + ) { + return null; + } + return flock.BABYLON.CSG2.FromMesh( + resolvedBaseDuplicate, + false, + ); + }); if (!outerCSG) { baseDuplicate.dispose(); @@ -275,9 +465,29 @@ export const flockCSG = { // EXECUTE SUBTRACTION subtractDuplicates.forEach((m, idx) => { + const resolvedMesh = this._getMeshForCSG( + m, + "subtractMeshes", + ); + if ( + !resolvedMesh || + !this._ensureMeshForCSG( + resolvedMesh, + "subtractMeshes", + ) + ) { + console.warn( + `[subtractMeshes] Skipping mesh without indices: ${m.name}`, + ); + return; + } const meshCSG = tryCSG( `FromMesh(tool[${idx}])`, - () => flock.BABYLON.CSG2.FromMesh(m, false), + () => + flock.BABYLON.CSG2.FromMesh( + resolvedMesh, + false, + ), ); if (!meshCSG) return; @@ -391,8 +601,24 @@ export const flockCSG = { actualBase, "baseDuplicate", ); - let outerCSG = flock.BABYLON.CSG2.FromMesh( + const resolvedBaseDuplicate = this._getMeshForCSG( baseDuplicate, + "subtractMeshesMerge", + ); + if ( + !resolvedBaseDuplicate || + !this._ensureMeshForCSG( + resolvedBaseDuplicate, + "subtractMeshesMerge", + ) + ) { + console.warn( + "[subtractMeshesMerge] Base mesh missing positions or indices.", + ); + return resolve(null); + } + let outerCSG = flock.BABYLON.CSG2.FromMesh( + resolvedBaseDuplicate, false, ); const subtractDuplicates = []; @@ -441,11 +667,27 @@ export const flockCSG = { subtractDuplicates.forEach((m) => { try { - const meshCSG = flock.BABYLON.CSG2.FromMesh( + const resolvedMesh = this._getMeshForCSG( m, - false, + "subtractMeshesMerge", ); - outerCSG = outerCSG.subtract(meshCSG); + if ( + resolvedMesh && + this._ensureMeshForCSG( + resolvedMesh, + "subtractMeshesMerge", + ) + ) { + const meshCSG = flock.BABYLON.CSG2.FromMesh( + resolvedMesh, + false, + ); + outerCSG = outerCSG.subtract(meshCSG); + } else { + console.warn( + `[subtractMeshesMerge] Skipping mesh without indices: ${m.name}`, + ); + } } catch (e) { console.warn(e); } @@ -524,8 +766,24 @@ export const flockCSG = { : actualBase.rotation.clone(); baseDuplicate.computeWorldMatrix(true); - let outerCSG = flock.BABYLON.CSG2.FromMesh( + const resolvedBaseDuplicate = this._getMeshForCSG( baseDuplicate, + "subtractMeshesIndividual", + ); + if ( + !resolvedBaseDuplicate || + !this._ensureMeshForCSG( + resolvedBaseDuplicate, + "subtractMeshesIndividual", + ) + ) { + console.warn( + "[subtractMeshesIndividual] Base mesh missing positions or indices.", + ); + return resolve(null); + } + let outerCSG = flock.BABYLON.CSG2.FromMesh( + resolvedBaseDuplicate, false, ); const allToolParts = []; @@ -542,11 +800,27 @@ export const flockCSG = { allToolParts.forEach((part) => { try { - const partCSG = flock.BABYLON.CSG2.FromMesh( + const resolvedPart = this._getMeshForCSG( part, - false, + "subtractMeshesIndividual", ); - outerCSG = outerCSG.subtract(partCSG); + if ( + resolvedPart && + this._ensureMeshForCSG( + resolvedPart, + "subtractMeshesIndividual", + ) + ) { + const partCSG = flock.BABYLON.CSG2.FromMesh( + resolvedPart, + false, + ); + outerCSG = outerCSG.subtract(partCSG); + } else { + console.warn( + `[subtractMeshesIndividual] Skipping mesh without indices: ${part.name}`, + ); + } } catch (e) { console.warn(e); } @@ -639,8 +913,27 @@ export const flockCSG = { firstMesh.flipFaces(); } } + const resolvedBase = this._getMeshForCSG( + firstMesh, + "intersectMeshes", + ); + if ( + !resolvedBase || + !this._ensureMeshForCSG( + resolvedBase, + "intersectMeshes", + ) + ) { + console.warn( + "[intersectMeshes] Base mesh missing positions or indices.", + ); + return null; + } // Create the base CSG - let baseCSG = flock.BABYLON.CSG2.FromMesh(firstMesh, false); + let baseCSG = flock.BABYLON.CSG2.FromMesh( + resolvedBase, + false, + ); // Intersect each subsequent mesh validMeshes.slice(1).forEach((mesh) => { @@ -653,11 +946,27 @@ export const flockCSG = { mesh.flipFaces(); } } - const meshCSG = flock.BABYLON.CSG2.FromMesh( + const resolvedMesh = this._getMeshForCSG( mesh, - false, + "intersectMeshes", ); - baseCSG = baseCSG.intersect(meshCSG); + if ( + resolvedMesh && + this._ensureMeshForCSG( + resolvedMesh, + "intersectMeshes", + ) + ) { + const meshCSG = flock.BABYLON.CSG2.FromMesh( + resolvedMesh, + false, + ); + baseCSG = baseCSG.intersect(meshCSG); + } else { + console.warn( + `[intersectMeshes] Skipping mesh without indices: ${mesh.name}`, + ); + } }); // Generate the resulting intersected mesh @@ -806,6 +1115,24 @@ export const flockCSG = { return new Promise((resolve) => { flock.whenModelReady(meshName, (mesh) => { if (mesh) { + if (flock.manifoldDebug) { + const positions = mesh.getVerticesData?.( + flock.BABYLON.VertexBuffer.PositionKind, + ); + const indices = mesh.getIndices?.(); + const childCount = + typeof mesh.getChildMeshes === "function" + ? mesh.getChildMeshes().length + : 0; + console.log("[prepareMeshes] Resolved mesh", { + meshName, + resolvedName: mesh.name, + positions: positions?.length ?? 0, + indices: indices?.length ?? 0, + childCount, + metadata: mesh.metadata, + }); + } mesh.name = modelId; mesh.metadata = mesh.metadata || {}; mesh.metadata.blockKey = blockId; diff --git a/api/shapes.js b/api/shapes.js index e0ca6eec..6377d617 100644 --- a/api/shapes.js +++ b/api/shapes.js @@ -420,18 +420,197 @@ export const flockShapes = { const loadPromise = new Promise(async (resolve, reject) => { try { const fontData = await (await fetch(font)).json(); + const manifold = + flock.manifold ?? + (typeof globalThis !== "undefined" ? globalThis.manifold : null); + if (flock.manifoldDebug) { + console.log("[create3DText] Manifold available:", !!manifold); + } + let mesh = null; + let manifoldText = null; + + const ensureArray = (data) => + Array.isArray(data) ? data : Array.from(data || []); + + const flattenVectorArray = (data) => { + if (!Array.isArray(data)) return ensureArray(data); + if (!data.length) return []; + if (typeof data[0] === "number") return data.slice(); + if (Array.isArray(data[0])) return data.flat(); + if (typeof data[0] === "object") { + return data.flatMap((entry) => [ + entry.x ?? entry[0], + entry.y ?? entry[1], + entry.z ?? entry[2], + ]); + } + return []; + }; + + const flattenIndexArray = (data) => { + if (!Array.isArray(data)) return ensureArray(data); + if (!data.length) return []; + if (typeof data[0] === "number") return data.slice(); + if (Array.isArray(data[0])) return data.flat(); + if (typeof data[0] === "object") { + return data.flatMap((entry) => [ + entry.a ?? entry[0], + entry.b ?? entry[1], + entry.c ?? entry[2], + ]); + } + return []; + }; + + const buildBabylonMeshFromManifold = (meshData) => { + if (!meshData) return null; + let positions = flattenVectorArray( + meshData.positions || + meshData.vertices || + meshData.vertProperties || + meshData.verts, + ); + let indices = flattenIndexArray( + meshData.indices || meshData.triangles || meshData.triVerts, + ); + + if (!positions.length && meshData.triangles) { + positions = flattenVectorArray(meshData.triangles); + indices = Array.from( + { length: positions.length / 3 }, + (_, i) => i, + ); + } + + if (!positions.length || !indices.length) return null; + if (flock.manifoldDebug) { + console.log("[create3DText] Manifold mesh data:", { + positions: positions.length, + indices: indices.length, + }); + } + const newMesh = new flock.BABYLON.Mesh(modelId, flock.scene); + const normals = []; + flock.BABYLON.VertexData.ComputeNormals( + positions, + indices, + normals, + ); + const vertexData = new flock.BABYLON.VertexData(); + vertexData.positions = positions; + vertexData.indices = indices; + vertexData.normals = normals; + vertexData.applyToMesh(newMesh, true); + return newMesh; + }; + + if (manifold) { + const createText = + manifold.createTextMesh || + manifold.createText || + manifold.text || + manifold.makeText; + if (flock.manifoldDebug) { + console.log("[create3DText] Manifold text factory:", { + createText: !!createText, + }); + } + if (createText) { + manifoldText = await createText({ + text, + font, + fontData, + size, + depth, + }); + if (flock.manifoldDebug) { + console.log("[create3DText] Manifold text result:", { + hasManifoldText: !!manifoldText, + }); + } + if (manifoldText) { + if (typeof manifoldText.toBabylonMesh === "function") { + if (flock.manifoldDebug) { + console.log( + "[create3DText] Using manifoldText.toBabylonMesh", + ); + } + mesh = manifoldText.toBabylonMesh(modelId, flock.scene); + } else if (typeof manifoldText.toMesh === "function") { + if (flock.manifoldDebug) { + console.log("[create3DText] Using manifoldText.toMesh"); + } + const meshData = manifoldText.toMesh(); + mesh = buildBabylonMeshFromManifold(meshData); + } else { + if (flock.manifoldDebug) { + console.log("[create3DText] Using manifoldText mesh data"); + } + mesh = buildBabylonMeshFromManifold( + manifoldText.mesh || manifoldText, + ); + } + } + } + } - const mesh = flock.BABYLON.MeshBuilder.CreateText( - modelId, - text, - fontData, - { - size: size, - depth: depth, - }, - flock.scene, - earcut, - ); + if (!mesh) { + if (flock.manifoldDebug) { + console.log("[create3DText] Falling back to BABYLON.CreateText"); + } + mesh = flock.BABYLON.MeshBuilder.CreateText( + modelId, + text, + fontData, + { + size: size, + depth: depth, + }, + flock.scene, + earcut, + ); + } + + const ensureMeshHasIndices = (targetMesh) => { + if (!targetMesh) return false; + const positions = targetMesh.getVerticesData( + flock.BABYLON.VertexBuffer.PositionKind, + ); + const indices = targetMesh.getIndices(); + if (positions && positions.length && indices && indices.length) { + return true; + } + if (!positions || !positions.length) return false; + const generated = Array.from( + { length: positions.length / 3 }, + (_, i) => i, + ); + const vertexData = new flock.BABYLON.VertexData(); + vertexData.positions = positions; + vertexData.indices = generated; + vertexData.applyToMesh(targetMesh, true); + return true; + }; + + if (!ensureMeshHasIndices(mesh)) { + if (flock.manifoldDebug) { + console.log( + "[create3DText] Missing indices; recreating with BABYLON.CreateText", + ); + } + mesh.dispose(); + mesh = flock.BABYLON.MeshBuilder.CreateText( + modelId, + text, + fontData, + { + size: size, + depth: depth, + }, + flock.scene, + earcut, + ); + } mesh.position.set(x, y, z); const material = new flock.BABYLON.StandardMaterial( @@ -450,6 +629,13 @@ export const flockShapes = { mesh.setEnabled(true); mesh.visibility = 1; + if (manifoldText) { + mesh.metadata = mesh.metadata || {}; + mesh.metadata.manifold = manifoldText; + } + mesh.metadata = mesh.metadata || {}; + mesh.metadata.textSource = manifoldText ? "manifold" : "babylon"; + const textShape = new flock.BABYLON.PhysicsShapeMesh(mesh, flock.scene); flock.applyPhysics(mesh, textShape); diff --git a/flock.js b/flock.js index ed2a85fd..cc205a6d 100644 --- a/flock.js +++ b/flock.js @@ -90,6 +90,7 @@ export const flock = { memoryMonitorInterval: 5000, materialsDebug: false, meshDebug: false, + manifoldDebug: false, performanceOverlay: false, maxMeshes: 5000, console: console, @@ -111,6 +112,7 @@ export const flock = { _animationFileCache: {}, characterNames: characterNames, alert: alert, + manifold: globalThis?.manifold ?? null, BABYLON: BABYLON, BABYLON_LOADER: BABYLON_LOADER, GradientMaterial: GradientMaterial,