From e78388d137e239e01def8ac26cfcdeeafa7dea4c Mon Sep 17 00:00:00 2001 From: Dr Tracy Gardner Date: Sun, 8 Feb 2026 16:34:26 +0000 Subject: [PATCH 1/5] Fix CSG subtract for composite bases --- api/csg.js | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/api/csg.js b/api/csg.js index 1c5bf77d..c85238f6 100644 --- a/api/csg.js +++ b/api/csg.js @@ -373,12 +373,10 @@ export const flockCSG = { flock.whenModelReady(baseMeshName, (baseMesh) => { if (!baseMesh) return resolve(null); + const referenceMesh = baseMesh.metadata?.modelName + ? flock._findFirstDescendantWithMaterial(baseMesh) || baseMesh + : baseMesh; let actualBase = baseMesh; - if (baseMesh.metadata?.modelName) { - const meshWithMaterial = - flock._findFirstDescendantWithMaterial(baseMesh); - if (meshWithMaterial) actualBase = meshWithMaterial; - } // Ensure base mesh has valid geometry for CSG actualBase = prepareMeshForCSG(actualBase); @@ -530,7 +528,7 @@ export const flockCSG = { flock.applyResultMeshProperties( resultMesh, - actualBase, + referenceMesh, modelId, blockId, ); @@ -593,10 +591,11 @@ export const flockCSG = { return new Promise((resolve) => { flock.whenModelReady(baseMeshName, (baseMesh) => { if (!baseMesh) return resolve(null); - let actualBase = baseMesh.metadata?.modelName + const referenceMesh = baseMesh.metadata?.modelName ? flock._findFirstDescendantWithMaterial(baseMesh) || baseMesh : baseMesh; + let actualBase = baseMesh; // Ensure base mesh has valid geometry for CSG actualBase = prepareMeshForCSG(actualBase); @@ -712,7 +711,7 @@ export const flockCSG = { resultMesh.computeWorldMatrix(true); flock.applyResultMeshProperties( resultMesh, - actualBase, + referenceMesh, modelId, blockId, ); @@ -751,12 +750,10 @@ export const flockCSG = { return new Promise((resolve) => { flock.whenModelReady(baseMeshName, (baseMesh) => { if (!baseMesh) return resolve(null); + const referenceMesh = baseMesh.metadata?.modelName + ? flock._findFirstDescendantWithMaterial(baseMesh) || baseMesh + : baseMesh; let actualBase = baseMesh; - if (baseMesh.metadata?.modelName) { - const meshWithMaterial = - flock._findFirstDescendantWithMaterial(baseMesh); - if (meshWithMaterial) actualBase = meshWithMaterial; - } // Ensure base mesh has valid geometry for CSG actualBase = prepareMeshForCSG(actualBase); @@ -849,7 +846,7 @@ export const flockCSG = { resultMesh.computeWorldMatrix(true); flock.applyResultMeshProperties( resultMesh, - actualBase, + referenceMesh, modelId, blockId, ); From 9d889085dc73fa7d7996940bf7cbb8681d1465a2 Mon Sep 17 00:00:00 2001 From: Dr Tracy Gardner Date: Sun, 8 Feb 2026 16:43:33 +0000 Subject: [PATCH 2/5] Handle composite bases in CSG subtract --- api/csg.js | 179 ++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 143 insertions(+), 36 deletions(-) diff --git a/api/csg.js b/api/csg.js index c85238f6..4c7ee016 100644 --- a/api/csg.js +++ b/api/csg.js @@ -136,6 +136,58 @@ function prepareMeshForCSG(mesh) { return merged; } +/** + * Collects all meshes with valid geometry for CSG operations. + * Falls back to converting non-indexed meshes when possible. + * @param {BABYLON.Mesh} mesh - The mesh to inspect + * @returns {BABYLON.Mesh[]} - Meshes suitable for CSG operations + */ +function prepareMeshesForCSG(mesh) { + if (!mesh) return []; + + const meshes = []; + const queue = [mesh]; + + while (queue.length) { + const current = queue.pop(); + if (!current) continue; + + const hasVertices = + current.getTotalVertices && current.getTotalVertices() > 0; + const hasPositions = + current.getVerticesData && + current.getVerticesData(flock.BABYLON.VertexBuffer.PositionKind); + const hasIndices = current.getIndices && current.getIndices(); + + if (hasPositions && !hasIndices) { + try { + current.convertToUnIndexedMesh(); + current.forceSharedVertices(); + } catch (e) { + // Ignore errors, continue with other approaches + } + } + + const hasValidGeometry = + current.getTotalVertices && + current.getTotalVertices() > 0 && + current.getIndices && + current.getIndices() && + current.getIndices().length > 0; + + if (hasValidGeometry) { + meshes.push(current); + } + + const children = current.getChildMeshes + ? current.getChildMeshes(true) + : []; + children.forEach((child) => queue.push(child)); + } + + return meshes; +} + export const flockCSG = { mergeCompositeMesh(meshes) { if (!meshes || meshes.length === 0) return null; @@ -379,8 +431,8 @@ export const flockCSG = { let actualBase = baseMesh; // Ensure base mesh has valid geometry for CSG - actualBase = prepareMeshForCSG(actualBase); - if (!actualBase) { + const baseMeshes = prepareMeshesForCSG(actualBase); + if (!baseMeshes.length) { console.warn("[subtractMeshes] Base mesh has no valid geometry for CSG."); return resolve(null); } @@ -391,22 +443,48 @@ export const flockCSG = { const scene = baseMesh.getScene(); // Prepare Base - const baseDuplicate = cloneForCSG( - actualBase, - "baseDuplicate", + const baseDuplicates = baseMeshes.map((base, index) => + cloneForCSG(base, `baseDuplicate_${index}`), ); - - let outerCSG = tryCSG("FromMesh(baseDuplicate)", () => - flock.BABYLON.CSG2.FromMesh(baseDuplicate, false), + + let outerCSG = tryCSG( + "FromMesh(baseDuplicate_0)", + () => + flock.BABYLON.CSG2.FromMesh( + baseDuplicates[0], + false, + ), ); if (!outerCSG) { - console.warn('[subtractMeshes] Failed to create CSG from base mesh'); - baseDuplicate.dispose(); + console.warn( + "[subtractMeshes] Failed to create CSG from base mesh", + ); + baseDuplicates.forEach((dup) => dup.dispose()); validMeshes.forEach((m) => m.dispose()); return resolve(null); } + for (let i = 1; i < baseDuplicates.length; i++) { + const baseCSG = tryCSG( + `FromMesh(baseDuplicate_${i})`, + () => + flock.BABYLON.CSG2.FromMesh( + baseDuplicates[i], + false, + ), + ); + if (baseCSG) { + const combined = tryCSG( + `add baseDuplicate_${i}`, + () => outerCSG.add(baseCSG), + ); + if (combined) { + outerCSG = combined; + } + } + } + const subtractDuplicates = []; validMeshes.forEach((mesh, meshIndex) => { @@ -504,7 +582,7 @@ export const flockCSG = { ).forEach(m => m.dispose()); // Cleanup - baseDuplicate.dispose(); + baseDuplicates.forEach((dup) => dup.dispose()); subtractDuplicates.forEach((m) => m.dispose()); // Don't dispose original meshes so user can still use them @@ -534,7 +612,7 @@ export const flockCSG = { ); // CLEANUP - baseDuplicate.dispose(); + baseDuplicates.forEach((dup) => dup.dispose()); subtractDuplicates.forEach((m) => m.dispose()); baseMesh.dispose(); validMeshes.forEach((m) => m.dispose()); @@ -598,8 +676,8 @@ export const flockCSG = { let actualBase = baseMesh; // Ensure base mesh has valid geometry for CSG - actualBase = prepareMeshForCSG(actualBase); - if (!actualBase) { + const baseMeshes = prepareMeshesForCSG(actualBase); + if (!baseMeshes.length) { console.warn("[subtractMeshesMerge] Base mesh has no valid geometry for CSG."); return resolve(null); } @@ -608,14 +686,28 @@ export const flockCSG = { .prepareMeshes(modelId, meshNames, blockId) .then((validMeshes) => { const scene = baseMesh.getScene(); - const baseDuplicate = cloneForCSG( - actualBase, - "baseDuplicate", + const baseDuplicates = baseMeshes.map((base, index) => + cloneForCSG(base, `baseDuplicate_${index}`), ); let outerCSG = flock.BABYLON.CSG2.FromMesh( - baseDuplicate, + baseDuplicates[0], false, ); + for (let i = 1; i < baseDuplicates.length; i++) { + try { + const baseCSG = + flock.BABYLON.CSG2.FromMesh( + baseDuplicates[i], + false, + ); + outerCSG = outerCSG.add(baseCSG); + } catch (e) { + console.warn( + `[subtractMeshesMerge] Base merge ${i} failed:`, + e.message, + ); + } + } const subtractDuplicates = []; validMeshes.forEach((mesh, meshIndex) => { @@ -700,7 +792,7 @@ export const flockCSG = { m.name === "resultMesh" && m.getTotalVertices() === 0 ).forEach(m => m.dispose()); - baseDuplicate.dispose(); + baseDuplicates.forEach((dup) => dup.dispose()); subtractDuplicates.forEach((m) => m.dispose()); return resolve(null); } @@ -716,7 +808,7 @@ export const flockCSG = { blockId, ); - baseDuplicate.dispose(); + baseDuplicates.forEach((dup) => dup.dispose()); subtractDuplicates.forEach((m) => m.dispose()); baseMesh.dispose(); validMeshes.forEach((m) => m.dispose()); @@ -756,8 +848,8 @@ export const flockCSG = { let actualBase = baseMesh; // Ensure base mesh has valid geometry for CSG - actualBase = prepareMeshForCSG(actualBase); - if (!actualBase) { + const baseMeshes = prepareMeshesForCSG(actualBase); + if (!baseMeshes.length) { console.warn("[subtractMeshesIndividual] Base mesh has no valid geometry for CSG."); return resolve(null); } @@ -766,22 +858,37 @@ export const flockCSG = { .prepareMeshes(modelId, meshNames, blockId) .then((validMeshes) => { const scene = baseMesh.getScene(); - const baseDuplicate = actualBase.clone("baseDuplicate"); - baseDuplicate.setParent(null); - baseDuplicate.position = actualBase - .getAbsolutePosition() - .clone(); - baseDuplicate.rotationQuaternion = null; - baseDuplicate.rotation = - actualBase.absoluteRotationQuaternion - ? actualBase.absoluteRotationQuaternion.toEulerAngles() - : actualBase.rotation.clone(); - baseDuplicate.computeWorldMatrix(true); + const baseDuplicates = baseMeshes.map((base, index) => { + const dup = base.clone(`baseDuplicate_${index}`); + dup.setParent(null); + dup.position = base.getAbsolutePosition().clone(); + dup.rotationQuaternion = null; + dup.rotation = base.absoluteRotationQuaternion + ? base.absoluteRotationQuaternion.toEulerAngles() + : base.rotation.clone(); + dup.computeWorldMatrix(true); + return dup; + }); let outerCSG = flock.BABYLON.CSG2.FromMesh( - baseDuplicate, + baseDuplicates[0], false, ); + for (let i = 1; i < baseDuplicates.length; i++) { + try { + const baseCSG = + flock.BABYLON.CSG2.FromMesh( + baseDuplicates[i], + false, + ); + outerCSG = outerCSG.add(baseCSG); + } catch (e) { + console.warn( + `[subtractMeshesIndividual] Base merge ${i} failed:`, + e.message, + ); + } + } const allToolParts = []; validMeshes.forEach((mesh) => { const parts = collectMaterialMeshesDeep(mesh); @@ -826,7 +933,7 @@ export const flockCSG = { m.name === "resultMesh" && m.getTotalVertices() === 0 ).forEach(m => m.dispose()); - baseDuplicate.dispose(); + baseDuplicates.forEach((dup) => dup.dispose()); allToolParts.forEach((t) => t.dispose()); return resolve(null); } @@ -851,7 +958,7 @@ export const flockCSG = { blockId, ); - baseDuplicate.dispose(); + baseDuplicates.forEach((dup) => dup.dispose()); allToolParts.forEach((t) => t.dispose()); baseMesh.dispose(); validMeshes.forEach((m) => m.dispose()); From 2977be109130159a68a980dfbd86b7a107f94dd4 Mon Sep 17 00:00:00 2001 From: Dr Tracy Gardner Date: Sun, 8 Feb 2026 16:46:11 +0000 Subject: [PATCH 3/5] Add debug logs for CSG base meshes --- api/csg.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/api/csg.js b/api/csg.js index 4c7ee016..eee47ca5 100644 --- a/api/csg.js +++ b/api/csg.js @@ -432,6 +432,9 @@ export const flockCSG = { // Ensure base mesh has valid geometry for CSG const baseMeshes = prepareMeshesForCSG(actualBase); + console.debug( + `[subtractMeshes] Base mesh candidates: ${baseMeshes.length}`, + ); if (!baseMeshes.length) { console.warn("[subtractMeshes] Base mesh has no valid geometry for CSG."); return resolve(null); @@ -446,6 +449,9 @@ export const flockCSG = { const baseDuplicates = baseMeshes.map((base, index) => cloneForCSG(base, `baseDuplicate_${index}`), ); + console.debug( + `[subtractMeshes] Base duplicates created: ${baseDuplicates.length}`, + ); let outerCSG = tryCSG( "FromMesh(baseDuplicate_0)", @@ -677,6 +683,9 @@ export const flockCSG = { // Ensure base mesh has valid geometry for CSG const baseMeshes = prepareMeshesForCSG(actualBase); + console.debug( + `[subtractMeshesMerge] Base mesh candidates: ${baseMeshes.length}`, + ); if (!baseMeshes.length) { console.warn("[subtractMeshesMerge] Base mesh has no valid geometry for CSG."); return resolve(null); @@ -689,6 +698,9 @@ export const flockCSG = { const baseDuplicates = baseMeshes.map((base, index) => cloneForCSG(base, `baseDuplicate_${index}`), ); + console.debug( + `[subtractMeshesMerge] Base duplicates created: ${baseDuplicates.length}`, + ); let outerCSG = flock.BABYLON.CSG2.FromMesh( baseDuplicates[0], false, @@ -849,6 +861,9 @@ export const flockCSG = { // Ensure base mesh has valid geometry for CSG const baseMeshes = prepareMeshesForCSG(actualBase); + console.debug( + `[subtractMeshesIndividual] Base mesh candidates: ${baseMeshes.length}`, + ); if (!baseMeshes.length) { console.warn("[subtractMeshesIndividual] Base mesh has no valid geometry for CSG."); return resolve(null); @@ -869,6 +884,9 @@ export const flockCSG = { dup.computeWorldMatrix(true); return dup; }); + console.debug( + `[subtractMeshesIndividual] Base duplicates created: ${baseDuplicates.length}`, + ); let outerCSG = flock.BABYLON.CSG2.FromMesh( baseDuplicates[0], From 22ebcbed242aa1f890da5825c563825cccfcb2cc Mon Sep 17 00:00:00 2001 From: Dr Tracy Gardner Date: Sun, 8 Feb 2026 16:52:11 +0000 Subject: [PATCH 4/5] Skip non-manifold base parts in CSG --- api/csg.js | 98 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 56 insertions(+), 42 deletions(-) diff --git a/api/csg.js b/api/csg.js index eee47ca5..5ed7e74d 100644 --- a/api/csg.js +++ b/api/csg.js @@ -453,14 +453,30 @@ export const flockCSG = { `[subtractMeshes] Base duplicates created: ${baseDuplicates.length}`, ); - let outerCSG = tryCSG( - "FromMesh(baseDuplicate_0)", - () => - flock.BABYLON.CSG2.FromMesh( - baseDuplicates[0], - false, - ), - ); + let outerCSG = null; + baseDuplicates.forEach((dup, index) => { + const baseCSG = tryCSG( + `FromMesh(baseDuplicate_${index})`, + () => flock.BABYLON.CSG2.FromMesh(dup, false), + ); + if (!baseCSG) { + console.warn( + `[subtractMeshes] Skipping non-manifold base part ${index}`, + ); + return; + } + if (!outerCSG) { + outerCSG = baseCSG; + return; + } + const combined = tryCSG( + `add baseDuplicate_${index}`, + () => outerCSG.add(baseCSG), + ); + if (combined) { + outerCSG = combined; + } + }); if (!outerCSG) { console.warn( @@ -471,26 +487,6 @@ export const flockCSG = { return resolve(null); } - for (let i = 1; i < baseDuplicates.length; i++) { - const baseCSG = tryCSG( - `FromMesh(baseDuplicate_${i})`, - () => - flock.BABYLON.CSG2.FromMesh( - baseDuplicates[i], - false, - ), - ); - if (baseCSG) { - const combined = tryCSG( - `add baseDuplicate_${i}`, - () => outerCSG.add(baseCSG), - ); - if (combined) { - outerCSG = combined; - } - } - } - const subtractDuplicates = []; validMeshes.forEach((mesh, meshIndex) => { @@ -701,24 +697,33 @@ export const flockCSG = { console.debug( `[subtractMeshesMerge] Base duplicates created: ${baseDuplicates.length}`, ); - let outerCSG = flock.BABYLON.CSG2.FromMesh( - baseDuplicates[0], - false, - ); - for (let i = 1; i < baseDuplicates.length; i++) { + let outerCSG = null; + baseDuplicates.forEach((dup, index) => { try { const baseCSG = flock.BABYLON.CSG2.FromMesh( - baseDuplicates[i], + dup, false, ); + if (!outerCSG) { + outerCSG = baseCSG; + return; + } outerCSG = outerCSG.add(baseCSG); } catch (e) { console.warn( - `[subtractMeshesMerge] Base merge ${i} failed:`, + `[subtractMeshesMerge] Skipping non-manifold base part ${index}:`, e.message, ); } + }); + if (!outerCSG) { + console.warn( + "[subtractMeshesMerge] Failed to create CSG from base mesh", + ); + baseDuplicates.forEach((dup) => dup.dispose()); + validMeshes.forEach((m) => m.dispose()); + return resolve(null); } const subtractDuplicates = []; @@ -888,24 +893,33 @@ export const flockCSG = { `[subtractMeshesIndividual] Base duplicates created: ${baseDuplicates.length}`, ); - let outerCSG = flock.BABYLON.CSG2.FromMesh( - baseDuplicates[0], - false, - ); - for (let i = 1; i < baseDuplicates.length; i++) { + let outerCSG = null; + baseDuplicates.forEach((dup, index) => { try { const baseCSG = flock.BABYLON.CSG2.FromMesh( - baseDuplicates[i], + dup, false, ); + if (!outerCSG) { + outerCSG = baseCSG; + return; + } outerCSG = outerCSG.add(baseCSG); } catch (e) { console.warn( - `[subtractMeshesIndividual] Base merge ${i} failed:`, + `[subtractMeshesIndividual] Skipping non-manifold base part ${index}:`, e.message, ); } + }); + if (!outerCSG) { + console.warn( + "[subtractMeshesIndividual] Failed to create CSG from base mesh", + ); + baseDuplicates.forEach((dup) => dup.dispose()); + validMeshes.forEach((m) => m.dispose()); + return resolve(null); } const allToolParts = []; validMeshes.forEach((mesh) => { From 82c34e325c404a15cc9afb9b66d67f05eceed1ab Mon Sep 17 00:00:00 2001 From: Dr Tracy Gardner Date: Sun, 8 Feb 2026 17:00:14 +0000 Subject: [PATCH 5/5] Filter base meshes by material for CSG --- api/csg.js | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/api/csg.js b/api/csg.js index 5ed7e74d..435354a8 100644 --- a/api/csg.js +++ b/api/csg.js @@ -142,9 +142,10 @@ function prepareMeshForCSG(mesh) { * @param {BABYLON.Mesh} mesh - The mesh to inspect * @returns {BABYLON.Mesh[]} - Meshes suitable for CSG operations */ -function prepareMeshesForCSG(mesh) { +function prepareMeshesForCSG(mesh, options = {}) { if (!mesh) return []; + const { requireMaterial = false } = options; const meshes = []; const queue = [mesh]; @@ -175,7 +176,7 @@ function prepareMeshesForCSG(mesh) { current.getIndices() && current.getIndices().length > 0; - if (hasValidGeometry) { + if (hasValidGeometry && (!requireMaterial || current.material)) { meshes.push(current); } @@ -431,7 +432,12 @@ export const flockCSG = { let actualBase = baseMesh; // Ensure base mesh has valid geometry for CSG - const baseMeshes = prepareMeshesForCSG(actualBase); + let baseMeshes = prepareMeshesForCSG(actualBase, { + requireMaterial: true, + }); + if (!baseMeshes.length) { + baseMeshes = prepareMeshesForCSG(actualBase); + } console.debug( `[subtractMeshes] Base mesh candidates: ${baseMeshes.length}`, ); @@ -678,7 +684,12 @@ export const flockCSG = { let actualBase = baseMesh; // Ensure base mesh has valid geometry for CSG - const baseMeshes = prepareMeshesForCSG(actualBase); + let baseMeshes = prepareMeshesForCSG(actualBase, { + requireMaterial: true, + }); + if (!baseMeshes.length) { + baseMeshes = prepareMeshesForCSG(actualBase); + } console.debug( `[subtractMeshesMerge] Base mesh candidates: ${baseMeshes.length}`, ); @@ -865,7 +876,12 @@ export const flockCSG = { let actualBase = baseMesh; // Ensure base mesh has valid geometry for CSG - const baseMeshes = prepareMeshesForCSG(actualBase); + let baseMeshes = prepareMeshesForCSG(actualBase, { + requireMaterial: true, + }); + if (!baseMeshes.length) { + baseMeshes = prepareMeshesForCSG(actualBase); + } console.debug( `[subtractMeshesIndividual] Base mesh candidates: ${baseMeshes.length}`, );