From e3bdbc7cdaa7b9fd7f4200a84cdbbb07e9237e78 Mon Sep 17 00:00:00 2001 From: Dr Tracy Gardner Date: Tue, 23 Dec 2025 13:38:32 +0000 Subject: [PATCH 01/20] Add material tile size block --- api/material.js | 47 ++++++++++++++++++++++++++++++++++++++++ blocks/materials.js | 28 ++++++++++++++++++++++++ generators/generators.js | 17 +++++++++++++++ locale/de.js | 4 ++++ locale/en.js | 3 +++ locale/es.js | 4 ++++ locale/fr.js | 4 ++++ locale/it.js | 4 ++++ locale/pl.js | 4 ++++ locale/pt.js | 4 ++++ locale/sv.js | 4 ++++ toolbox.js | 14 ++++++++++++ 12 files changed, 137 insertions(+) diff --git a/api/material.js b/api/material.js index d375473b..12b07c2f 100644 --- a/api/material.js +++ b/api/material.js @@ -4,6 +4,27 @@ export function setFlockReference(ref) { flock = ref; } +const PRIMITIVE_TILE_SCALE = 1; +const IMPORTED_TILE_SCALE = 0.5; + +function resolveTextureTileScale(mesh, fallbackScale = null) { + const meta = mesh.metadata || (mesh.metadata = {}); + if (Number.isFinite(meta.textureTileScale)) return meta.textureTileScale; + + const isPrimitive = !!meta.shapeType; + const isImported = !!meta.modelName; + const inferredScale = + fallbackScale != null + ? fallbackScale + : isPrimitive + ? PRIMITIVE_TILE_SCALE + : isImported + ? IMPORTED_TILE_SCALE + : PRIMITIVE_TILE_SCALE; + meta.textureTileScale = inferredScale; + return inferredScale; +} + export const flockMaterial = { adjustMaterialTilingToMesh(mesh, material, unitsPerTile = null) { if (!mesh || !material) return; @@ -320,6 +341,32 @@ export const flockMaterial = { }); }); }, + setMaterialTileSize(meshName, tileUnits) { + const requestedSize = Number(tileUnits); + return flock.whenModelReady(meshName, (mesh) => { + if (!Number.isFinite(requestedSize) || requestedSize <= 0) return; + + const targets = [mesh, ...(mesh.getDescendants?.() || [])]; + const rootScale = resolveTextureTileScale(mesh); + + targets.forEach((target) => { + const scale = resolveTextureTileScale(target, rootScale); + const effectiveTileSize = requestedSize * scale; + target.metadata.textureTileBaseSize = requestedSize; + target.metadata.textureTileSize = effectiveTileSize; + + const material = + target.material || + (target.getClassName?.() === "InstancedMesh" + ? target.sourceMesh?.material + : null); + + if (material) { + flock.adjustMaterialTilingToMesh(target, material, effectiveTileSize); + } + }); + }); + }, clearEffects(meshName) { return flock.whenModelReady(meshName, (mesh) => { if (flock.materialsDebug) console.log(`Clear effects from ${meshName}:`); diff --git a/blocks/materials.js b/blocks/materials.js index caa1450e..54d43e9c 100644 --- a/blocks/materials.js +++ b/blocks/materials.js @@ -748,4 +748,32 @@ export function defineMaterialsBlocks() { attachSetMaterialOnChange(this); }, }; + + Blockly.Blocks["set_material_tile_size"] = { + init: function () { + this.jsonInit({ + type: "set_material_tile_size", + message0: translate("material_tile_size"), + args0: [ + { + type: "field_variable", + name: "MESH", + variable: window.currentMesh, + }, + { + type: "input_value", + name: "TILE_SIZE", + check: "Number", + }, + ], + previousStatement: null, + nextStatement: null, + inputsInline: true, + colour: categoryColours["Materials"], + tooltip: getTooltip("material_tile_size"), + }); + this.setHelpUrl(getHelpUrlFor(this.type)); + this.setStyle("materials_blocks"); + }, + }; } diff --git a/generators/generators.js b/generators/generators.js index f706490a..b3fbf369 100644 --- a/generators/generators.js +++ b/generators/generators.js @@ -3334,6 +3334,23 @@ export function defineGenerators() { return code;*/ }; + javascriptGenerator.forBlock["set_material_tile_size"] = function ( + block, + ) { + const meshVar = javascriptGenerator.nameDB_.getName( + block.getFieldValue("MESH"), + Blockly.Names.NameType.VARIABLE, + ); + + const tileSize = javascriptGenerator.valueToCode( + block, + "TILE_SIZE", + javascriptGenerator.ORDER_ATOMIC, + ); + + return `await setMaterialTileSize(${meshVar}, ${tileSize});\n`; + }; + javascriptGenerator.forBlock["skin_colour"] = function (block) { const colour = block.getFieldValue("COLOR"); const code = `"${colour}"`; diff --git a/locale/de.js b/locale/de.js index 9a30c66d..f6bd5634 100644 --- a/locale/de.js +++ b/locale/de.js @@ -241,6 +241,8 @@ export default { material: "Material %1 %2 Alpha %3", gradient_material: "Material %1 Alpha %2", set_material: "Material von %1 auf %2 setzen", + material_tile_size: + "Material-Kachelgröße für %1 auf %2 setzen", // Physics blocks add_physics: "Physik hinzufügen %1 Typ %2", @@ -633,6 +635,8 @@ export default { material_tooltip: "Definiere Materialeigenschaften", gradient_material_tooltip: "Definiere Materialeigenschaften mit Verlauf", set_material_tooltip: "Setze das angegebene Material auf das Objekt", + material_tile_size_tooltip: + "Lege fest, wie viele Welteinheiten jede Texturkachel umfasst. Importierte Meshes nutzen die halbe Größe von primitiven Formen. Tut nichts, wenn das Material keine Textur hat.\nSchlüsselwort: Kachelgröße", // Physics tooltips add_physics_tooltip: diff --git a/locale/en.js b/locale/en.js index ec45065f..d8d0a85e 100644 --- a/locale/en.js +++ b/locale/en.js @@ -238,6 +238,7 @@ export default { material: "material %1 %2 alpha %3", gradient_material: "material %1 alpha %2", set_material: "set material of %1 to %2", + material_tile_size: "set material tile size of %1 to %2", // Custom block translations - Physics blocks add_physics: "add physics %1 type %2", @@ -498,6 +499,8 @@ export default { material_tooltip: "Define material properties", gradient_material_tooltip: "Define material properties", set_material_tooltip: "Set the specified material on the given mesh.", + material_tile_size_tooltip: + "Set how many world units each texture tile spans. Imported meshes use half the size of primitive shapes. Does nothing if the material has no texture.\nKeyword: tile size", // Tooltip translations - Physics blocks add_physics_tooltip: diff --git a/locale/es.js b/locale/es.js index b7e6848d..344df5af 100644 --- a/locale/es.js +++ b/locale/es.js @@ -235,6 +235,8 @@ export default { material: "material %1 %2 alfa %3", // human gradient_material: "material %1 alfa %2", // human set_material: "establecer material de %1 a %2", // human + material_tile_size: + "establecer el tamaño de mosaico del material de %1 a %2", // human // Custom block translations - Physics blocks add_physics: "añadir física a %1 tipo %2", // human @@ -504,6 +506,8 @@ export default { material_tooltip: "Define propiedades del material", // human gradient_material_tooltip: "Define propiedades del material (gradiente)", // human set_material_tooltip: "Establecer el material especificado a la malla indicada.", // human + material_tile_size_tooltip: + "Define cuántas unidades del mundo ocupa cada mosaico de textura. Las mallas importadas usan la mitad del tamaño de las formas primitivas. No hace nada si el material no tiene textura.\nPalabra clave: tamaño de mosaico", // human // Tooltip translations - Physics blocks add_physics_tooltip: diff --git a/locale/fr.js b/locale/fr.js index 6ebdfa1f..ad4457b7 100644 --- a/locale/fr.js +++ b/locale/fr.js @@ -236,6 +236,8 @@ export default { material: "matériau %1 %2 opacité %3", gradient_material: "matériau %1 opacité %2", set_material: "définir le matériau de %1 à %2", + material_tile_size: + "définir la taille de tuile du matériau de %1 à %2", // Custom block translations - Physics blocks add_physics: "ajouter physique %1 type %2", @@ -511,6 +513,8 @@ export default { material_tooltip: "Définit les propriétés du matériau", gradient_material_tooltip: "Définit les propriétés du matériau", set_material_tooltip: "Définit le matériau spécifié sur le maillage donné.", + material_tile_size_tooltip: + "Définit combien d’unités du monde chaque tuile de texture couvre. Les maillages importés utilisent la moitié de la taille des formes primitives. Ne fait rien si le matériau n’a pas de texture.\nMot-clé: taille de tuile", // Tooltip translations - Physics blocks add_physics_tooltip: diff --git a/locale/it.js b/locale/it.js index c4c0ea37..3f53d925 100644 --- a/locale/it.js +++ b/locale/it.js @@ -240,6 +240,8 @@ export default { material: "materiale %1 %2 alpha %3", gradient_material: "materiale %1 alpha %2", set_material: "imposta materiale di %1 a %2", + material_tile_size: + "imposta la dimensione della piastrella del materiale di %1 a %2", // Custom block translations - Physics blocks add_physics: "aggiungi fisica %1 tipo %2", @@ -509,6 +511,8 @@ export default { material_tooltip: "Definisci le proprietà del materiale", gradient_material_tooltip: "Definisci le proprietà del materiale", set_material_tooltip: "Imposta il materiale specificato sulla mesh indicata.", + material_tile_size_tooltip: + "Imposta quante unità del mondo occupa ogni piastrella della texture. Le mesh importate usano metà della dimensione delle forme primitive. Non fa nulla se il materiale non ha texture.\nParola chiave: dimensione piastrella", // Tooltip translations - Physics blocks add_physics_tooltip: diff --git a/locale/pl.js b/locale/pl.js index 80b17a52..6609bc95 100644 --- a/locale/pl.js +++ b/locale/pl.js @@ -237,6 +237,8 @@ export default { material: "materiał %1 %2 przezroczystość: %3", gradient_material: "gradientowy materiał %1 przezroczystość: %2", set_material: "ustaw materiał %2 na %1", + material_tile_size: + "ustaw rozmiar kafelka materiału dla %1 na %2", // Custom block translations - Physics blocks add_physics: "dodaj fizykę do %1 typ: %2", @@ -505,6 +507,8 @@ export default { material_tooltip: "Zdefiniuj właściwości materiału", gradient_material_tooltip: "Zdefiniuj właściwości materiału z gradientem", set_material_tooltip: "Ustaw podany materiał na wybranym obiekcie.", + material_tile_size_tooltip: + "Ustaw ile jednostek świata zajmuje każdy kafelek tekstury. Importowane siatki używają połowy rozmiaru form prymitywnych. Nic nie robi, jeśli materiał nie ma tekstury.\nSłowo kluczowe: rozmiar kafelka", // Tooltip translations - Physics blocks add_physics_tooltip: diff --git a/locale/pt.js b/locale/pt.js index 0e0d8ee7..77838978 100644 --- a/locale/pt.js +++ b/locale/pt.js @@ -235,6 +235,8 @@ export default { material: "material %1 %2 opacidade %3", gradient_material: "material %1 opacidade %2", set_material: "definir material de %1 para %2", + material_tile_size: + "definir tamanho de ladrilho do material de %1 para %2", // Custom block translations - Physics blocks add_physics: "adicionar física %1 tipo %2", @@ -501,6 +503,8 @@ export default { material_tooltip: "Define propriedades de material", gradient_material_tooltip: "Define propriedades de material com gradiente", set_material_tooltip: "Define o material especificado no mesh indicado.", + material_tile_size_tooltip: + "Define quantas unidades do mundo cada ladrilho de textura cobre. Meshes importados usam metade do tamanho das formas primitivas. Não faz nada se o material não tiver textura.\nPalavra-chave: tamanho do ladrilho", // Tooltip translations - Physics blocks add_physics_tooltip: diff --git a/locale/sv.js b/locale/sv.js index 35f9bba2..f1835671 100644 --- a/locale/sv.js +++ b/locale/sv.js @@ -235,6 +235,8 @@ export default { material: "material %1 %2 alfa %3", gradient_material: "material %1 alfa %2", set_material: "ställ in material för %1 till %2", + material_tile_size: + "ställ in materialets plattstorlek för %1 till %2", // Custom block translations - Physics blocks add_physics: "lägg till fysik %1 typ %2", @@ -500,6 +502,8 @@ export default { gradient_material_tooltip: "Definiera materialegenskaper", set_material_tooltip: "Ange det angivna materialet på det valda mesh-objektet.", + material_tile_size_tooltip: + "Ställ in hur många världs-enheter varje texturplatta täcker. Importerade mesh använder halva storleken av primitiva former. Gör inget om materialet saknar textur.\nKeyword: plattstorlek", // Tooltip translations - Physics blocks add_physics_tooltip: diff --git a/toolbox.js b/toolbox.js index d5337b1e..09be698c 100644 --- a/toolbox.js +++ b/toolbox.js @@ -3086,6 +3086,20 @@ const toolboxMaterials = { }, }, }, + { + kind: "block", + type: "set_material_tile_size", + inputs: { + TILE_SIZE: { + shadow: { + type: "math_number", + fields: { + NUM: 4, + }, + }, + }, + }, + }, ], }; From f74529400b62f878b0835319c37c914aa5ce219c Mon Sep 17 00:00:00 2001 From: Dr Tracy Gardner Date: Tue, 23 Dec 2025 13:47:03 +0000 Subject: [PATCH 02/20] Fix AI marker for Spanish material tile translation --- locale/es.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locale/es.js b/locale/es.js index 344df5af..d737939e 100644 --- a/locale/es.js +++ b/locale/es.js @@ -236,7 +236,7 @@ export default { gradient_material: "material %1 alfa %2", // human set_material: "establecer material de %1 a %2", // human material_tile_size: - "establecer el tamaño de mosaico del material de %1 a %2", // human + "establecer el tamaño de mosaico del material de %1 a %2", // ai // Custom block translations - Physics blocks add_physics: "añadir física a %1 tipo %2", // human From c848db6205d2650b5122ccb381dd67fadb8c49ef Mon Sep 17 00:00:00 2001 From: Dr Tracy Gardner Date: Tue, 23 Dec 2025 13:47:09 +0000 Subject: [PATCH 03/20] Let tile size block retile primitive shapes when requested --- api/material.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/api/material.js b/api/material.js index 12b07c2f..0a12d0c4 100644 --- a/api/material.js +++ b/api/material.js @@ -33,7 +33,9 @@ export const flockMaterial = { const shapeType = mesh?.metadata?.shapeType; const bakedShapes = new Set(["Box", "Sphere", "Cylinder", "Capsule", "Plane"]); - if (shapeType && bakedShapes.has(shapeType)) return; + if (shapeType && bakedShapes.has(shapeType) && !Number.isFinite(unitsPerTile)) { + return; + } const tex = material.diffuseTexture || From d1aaa1ff66dab22ec25f198317855cd91d408e11 Mon Sep 17 00:00:00 2001 From: Dr Tracy Gardner Date: Tue, 23 Dec 2025 13:51:55 +0000 Subject: [PATCH 04/20] Apply saved tile size to retile baked shapes --- api/material.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/api/material.js b/api/material.js index 0a12d0c4..388b65da 100644 --- a/api/material.js +++ b/api/material.js @@ -31,9 +31,15 @@ export const flockMaterial = { if (mesh.metadata?.skipAutoTiling) return; + const existingTile = mesh.metadata?.textureTileSize; const shapeType = mesh?.metadata?.shapeType; const bakedShapes = new Set(["Box", "Sphere", "Cylinder", "Capsule", "Plane"]); - if (shapeType && bakedShapes.has(shapeType) && !Number.isFinite(unitsPerTile)) { + if ( + shapeType && + bakedShapes.has(shapeType) && + !Number.isFinite(unitsPerTile) && + !Number.isFinite(existingTile) + ) { return; } @@ -56,7 +62,6 @@ export const flockMaterial = { const extend = mesh.getBoundingInfo?.()?.boundingBox?.extendSizeWorld; if (!extend) return; - const existingTile = mesh.metadata?.textureTileSize; const tile = Number.isFinite(unitsPerTile) && unitsPerTile > 0 ? unitsPerTile From ecda72eea93b4e6d50fbeca663a7f8e144d19e3f Mon Sep 17 00:00:00 2001 From: Dr Tracy Gardner Date: Tue, 23 Dec 2025 14:15:48 +0000 Subject: [PATCH 05/20] Retile primitive meshes when material tile size changes --- api/material.js | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/api/material.js b/api/material.js index 388b65da..456c027d 100644 --- a/api/material.js +++ b/api/material.js @@ -25,6 +25,39 @@ function resolveTextureTileScale(mesh, fallbackScale = null) { return inferredScale; } +function retilePrimitiveMesh(mesh, tileSize) { + const shapeType = mesh?.metadata?.shapeType; + if (!shapeType || !Number.isFinite(tileSize) || tileSize <= 0) return; + + const ext = mesh.getBoundingInfo?.()?.boundingBox?.extendSizeWorld; + if (!ext) return; + + const width = ext.x * 2; + const height = ext.y * 2; + const depth = ext.z * 2; + const largestDiameter = Math.max(width, height, depth); + + switch (shapeType) { + case "Box": + flock.setSizeBasedBoxUVs(mesh, width, height, depth, tileSize); + break; + case "Sphere": + flock.setSphereUVs(mesh, largestDiameter, tileSize); + break; + case "Cylinder": + flock.setSizeBasedCylinderUVs(mesh, height, width, width, tileSize); + break; + case "Capsule": + flock.setCapsuleUVs(mesh, width / 2, height, tileSize); + break; + case "Plane": + flock.setSizeBasedPlaneUVs(mesh, width, depth, tileSize); + break; + default: + break; + } +} + export const flockMaterial = { adjustMaterialTilingToMesh(mesh, material, unitsPerTile = null) { if (!mesh || !material) return; @@ -70,6 +103,11 @@ export const flockMaterial = { : 2; mesh.metadata = mesh.metadata || {}; mesh.metadata.textureTileSize = tile; + + if (shapeType && bakedShapes.has(shapeType)) { + retilePrimitiveMesh(mesh, tile); + } + const worldWidth = extend.x * 2; const worldHeight = extend.y * 2; const worldDepth = extend.z * 2; @@ -369,6 +407,7 @@ export const flockMaterial = { : null); if (material) { + retilePrimitiveMesh(target, effectiveTileSize); flock.adjustMaterialTilingToMesh(target, material, effectiveTileSize); } }); From 823cd6406a459c89ef42489b93e4543eea2dc4a9 Mon Sep 17 00:00:00 2001 From: Dr Tracy Gardner Date: Tue, 23 Dec 2025 14:20:58 +0000 Subject: [PATCH 06/20] Allow setMaterial tileSize option and reuse in tile block --- api/material.js | 160 +++++++++++++++++++++++++----------------------- 1 file changed, 82 insertions(+), 78 deletions(-) diff --git a/api/material.js b/api/material.js index 456c027d..b85360ec 100644 --- a/api/material.js +++ b/api/material.js @@ -387,31 +387,7 @@ export const flockMaterial = { }); }, setMaterialTileSize(meshName, tileUnits) { - const requestedSize = Number(tileUnits); - return flock.whenModelReady(meshName, (mesh) => { - if (!Number.isFinite(requestedSize) || requestedSize <= 0) return; - - const targets = [mesh, ...(mesh.getDescendants?.() || [])]; - const rootScale = resolveTextureTileScale(mesh); - - targets.forEach((target) => { - const scale = resolveTextureTileScale(target, rootScale); - const effectiveTileSize = requestedSize * scale; - target.metadata.textureTileBaseSize = requestedSize; - target.metadata.textureTileSize = effectiveTileSize; - - const material = - target.material || - (target.getClassName?.() === "InstancedMesh" - ? target.sourceMesh?.material - : null); - - if (material) { - retilePrimitiveMesh(target, effectiveTileSize); - flock.adjustMaterialTilingToMesh(target, material, effectiveTileSize); - } - }); - }); + return flock.setMaterial(meshName, null, { tileSize: tileUnits }); }, clearEffects(meshName) { return flock.whenModelReady(meshName, (mesh) => { @@ -861,76 +837,104 @@ export const flockMaterial = { return material; }, - setMaterial(meshName, materials) { - materials = materials.map((material) => { - if (material instanceof flock.BABYLON.Material) { - return material; - } else { - material = flock.createMaterial(material); - material.metadata = material.metadata || {}; - material.metadata.internal = true; - return material; - } - }); + setMaterial(meshName, materials, options = {}) { + const normalizedMaterials = Array.isArray(materials) + ? materials.map((material) => { + if (material instanceof flock.BABYLON.Material) { + return material; + } else { + material = flock.createMaterial(material); + material.metadata = material.metadata || {}; + material.metadata.internal = true; + return material; + } + }) + : []; - flock.setMaterialInternal(meshName, materials); + flock.setMaterialInternal(meshName, normalizedMaterials, options); flock.whenModelReady(meshName, (mesh) => { if (flock.materialsDebug) console.log(mesh.metadata.clones); mesh.metadata?.clones?.forEach((cloneName) => { - flock.setMaterialInternal(cloneName, materials); + flock.setMaterialInternal(cloneName, normalizedMaterials, options); }); }); }, - setMaterialInternal(meshName, materials) { + setMaterialInternal(meshName, materials, { tileSize = null } = {}) { return flock.whenModelReady(meshName, (mesh) => { const allMeshes = [mesh].concat(mesh.getDescendants()); - allMeshes.forEach((part) => { - if (part.material?.metadata?.internal) { - part.material.dispose(); - } - }); + const hasMaterials = Array.isArray(materials) && materials.length > 0; - if (flock.materialsDebug) - console.log(`Setting material of ${meshName} to ${materials}:`); - const validMeshes = allMeshes.filter( - (part) => part instanceof flock.BABYLON.Mesh, - ); + if (hasMaterials) { + allMeshes.forEach((part) => { + if (part.material?.metadata?.internal) { + part.material.dispose(); + } + }); - // Sort meshes alphabetically by name - const sortedMeshes = validMeshes.sort((a, b) => - a.name.localeCompare(b.name), - ); + if (flock.materialsDebug) + console.log(`Setting material of ${meshName} to ${materials}:`); + const validMeshes = allMeshes.filter( + (part) => part instanceof flock.BABYLON.Mesh, + ); - if (flock.materialsDebug) - console.log(`Setting material of ${sortedMeshes.length} meshes`); - sortedMeshes.forEach((part, index) => { - const material = Array.isArray(materials) - ? materials[index % materials.length] - : materials; + // Sort meshes alphabetically by name + const sortedMeshes = validMeshes.sort((a, b) => + a.name.localeCompare(b.name), + ); - if (material instanceof flock.GradientMaterial) { - mesh.computeWorldMatrix(true); + if (flock.materialsDebug) + console.log(`Setting material of ${sortedMeshes.length} meshes`); + sortedMeshes.forEach((part, index) => { + const material = Array.isArray(materials) + ? materials[index % materials.length] + : materials; - const boundingInfo = mesh.getBoundingInfo(); + if (material instanceof flock.GradientMaterial) { + mesh.computeWorldMatrix(true); - const yDimension = boundingInfo.boundingBox.extendSizeWorld.y; + const boundingInfo = mesh.getBoundingInfo(); - material.scale = yDimension > 0 ? 1 / yDimension : 1; - } - if (!(material instanceof flock.BABYLON.Material)) { - console.error( - `Invalid material provided for mesh ${part.name}:`, - material, - ); - return; - } + const yDimension = boundingInfo.boundingBox.extendSizeWorld.y; - if (flock.materialsDebug) - console.log(`Setting material of ${part.name} to ${material.name}`); - // Apply the material to the mesh - part.material = material; - flock.adjustMaterialTilingToMesh(part, material); - }); + material.scale = yDimension > 0 ? 1 / yDimension : 1; + } + if (!(material instanceof flock.BABYLON.Material)) { + console.error( + `Invalid material provided for mesh ${part.name}:`, + material, + ); + return; + } + + if (flock.materialsDebug) + console.log(`Setting material of ${part.name} to ${material.name}`); + // Apply the material to the mesh + part.material = material; + flock.adjustMaterialTilingToMesh(part, material); + }); + } + + if (Number.isFinite(tileSize) && tileSize > 0) { + const targets = allMeshes.filter( + (part) => part instanceof flock.BABYLON.Mesh, + ); + const rootScale = resolveTextureTileScale(mesh); + targets.forEach((part) => { + const scale = resolveTextureTileScale(part, rootScale); + const effectiveTileSize = tileSize * scale; + part.metadata.textureTileBaseSize = tileSize; + part.metadata.textureTileSize = effectiveTileSize; + retilePrimitiveMesh(part, effectiveTileSize); + const material = + part.material || + (part.getClassName?.() === "InstancedMesh" + ? part.sourceMesh?.material + : null); + if (material) { + flock.adjustMaterialTilingToMesh(part, material, effectiveTileSize); + } + }); + } if (mesh.metadata?.glow) { flock.glowMesh(mesh); From cdf5b47fa6984e77a3d075bf2993d05d91ccf933 Mon Sep 17 00:00:00 2001 From: Dr Tracy Gardner Date: Tue, 23 Dec 2025 14:26:24 +0000 Subject: [PATCH 07/20] Return promises from material tiling and reuse setMaterial for tile block --- api/material.js | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/api/material.js b/api/material.js index b85360ec..ea3ab67c 100644 --- a/api/material.js +++ b/api/material.js @@ -387,7 +387,10 @@ export const flockMaterial = { }); }, setMaterialTileSize(meshName, tileUnits) { - return flock.setMaterial(meshName, null, { tileSize: tileUnits }); + const requestedSize = Number(tileUnits); + if (!Number.isFinite(requestedSize) || requestedSize <= 0) + return Promise.resolve(); + return flock.setMaterial(meshName, [], { tileSize: requestedSize }); }, clearEffects(meshName) { return flock.whenModelReady(meshName, (mesh) => { @@ -851,15 +854,23 @@ export const flockMaterial = { }) : []; - flock.setMaterialInternal(meshName, normalizedMaterials, options); - flock.whenModelReady(meshName, (mesh) => { - if (flock.materialsDebug) console.log(mesh.metadata.clones); - mesh.metadata?.clones?.forEach((cloneName) => { - flock.setMaterialInternal(cloneName, normalizedMaterials, options); - }); - }); + const tasks = []; + tasks.push(flock.setMaterialInternal(meshName, normalizedMaterials, options)); + + tasks.push( + flock.whenModelReady(meshName, (mesh) => { + if (flock.materialsDebug) console.log(mesh.metadata.clones); + const clonePromises = + mesh.metadata?.clones?.map((cloneName) => + flock.setMaterialInternal(cloneName, normalizedMaterials, options), + ) || []; + return Promise.all(clonePromises); + }), + ); + + return Promise.all(tasks); }, - setMaterialInternal(meshName, materials, { tileSize = null } = {}) { + setMaterialInternal(meshName, materials = [], { tileSize = null } = {}) { return flock.whenModelReady(meshName, (mesh) => { const allMeshes = [mesh].concat(mesh.getDescendants()); const hasMaterials = Array.isArray(materials) && materials.length > 0; From bce99edb29ba56743c2e3c7455ecb41524e8f569 Mon Sep 17 00:00:00 2001 From: Dr Tracy Gardner Date: Tue, 23 Dec 2025 14:37:32 +0000 Subject: [PATCH 08/20] Expose setMaterialTileSize to user scripts --- flock.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flock.js b/flock.js index cdf800d5..033a20a2 100644 --- a/flock.js +++ b/flock.js @@ -1015,6 +1015,8 @@ export const flock = { changeColorMesh: this.changeColorMesh?.bind(this), changeMaterial: this.changeMaterial?.bind(this), setMaterial: this.setMaterial?.bind(this), + setMaterialTileSize: + this.setMaterialTileSize?.bind(this), createMaterial: this.createMaterial?.bind(this), moveForward: this.moveForward?.bind(this), moveSideways: this.moveSideways?.bind(this), From 9c2ae5a3c7ad1fc06f35bf4f67ebfa36e9b5ac9b Mon Sep 17 00:00:00 2001 From: Dr Tracy Gardner Date: Tue, 23 Dec 2025 14:44:56 +0000 Subject: [PATCH 09/20] Avoid double-scaling when retiling primitives --- api/material.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/api/material.js b/api/material.js index ea3ab67c..4707b766 100644 --- a/api/material.js +++ b/api/material.js @@ -106,6 +106,9 @@ export const flockMaterial = { if (shapeType && bakedShapes.has(shapeType)) { retilePrimitiveMesh(mesh, tile); + tex.uScale = 1; + tex.vScale = 1; + return; } const worldWidth = extend.x * 2; From 528aa5c87746469269d7523dc2b46d1547c892ad Mon Sep 17 00:00:00 2001 From: Dr Tracy Gardner Date: Tue, 23 Dec 2025 14:56:24 +0000 Subject: [PATCH 10/20] Preserve base tile size across setMaterial calls --- api/material.js | 55 +++++++++++++++++++++++++++++-------------------- 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/api/material.js b/api/material.js index 4707b766..86d1dc96 100644 --- a/api/material.js +++ b/api/material.js @@ -58,6 +58,12 @@ function retilePrimitiveMesh(mesh, tileSize) { } } +function computeEffectiveTile(mesh, baseTile, fallbackScale = null) { + if (!Number.isFinite(baseTile) || baseTile <= 0) return { base: null, effective: null }; + const scale = resolveTextureTileScale(mesh, fallbackScale); + return { base: baseTile, effective: baseTile * scale }; +} + export const flockMaterial = { adjustMaterialTilingToMesh(mesh, material, unitsPerTile = null) { if (!mesh || !material) return; @@ -877,6 +883,15 @@ export const flockMaterial = { return flock.whenModelReady(meshName, (mesh) => { const allMeshes = [mesh].concat(mesh.getDescendants()); const hasMaterials = Array.isArray(materials) && materials.length > 0; + const rootScale = resolveTextureTileScale(mesh); + const baseTile = + Number.isFinite(tileSize) && tileSize > 0 + ? tileSize + : Number.isFinite(mesh.metadata?.textureTileBaseSize) + ? mesh.metadata.textureTileBaseSize + : Number.isFinite(mesh.metadata?.textureTileSize) + ? mesh.metadata.textureTileSize + : null; if (hasMaterials) { allMeshes.forEach((part) => { @@ -924,31 +939,27 @@ export const flockMaterial = { console.log(`Setting material of ${part.name} to ${material.name}`); // Apply the material to the mesh part.material = material; - flock.adjustMaterialTilingToMesh(part, material); + const { effective } = computeEffectiveTile(part, baseTile, rootScale); + flock.adjustMaterialTilingToMesh(part, material, effective); }); } - if (Number.isFinite(tileSize) && tileSize > 0) { - const targets = allMeshes.filter( - (part) => part instanceof flock.BABYLON.Mesh, - ); - const rootScale = resolveTextureTileScale(mesh); - targets.forEach((part) => { - const scale = resolveTextureTileScale(part, rootScale); - const effectiveTileSize = tileSize * scale; - part.metadata.textureTileBaseSize = tileSize; - part.metadata.textureTileSize = effectiveTileSize; - retilePrimitiveMesh(part, effectiveTileSize); - const material = - part.material || - (part.getClassName?.() === "InstancedMesh" - ? part.sourceMesh?.material - : null); - if (material) { - flock.adjustMaterialTilingToMesh(part, material, effectiveTileSize); - } - }); - } + const targets = allMeshes.filter((part) => part instanceof flock.BABYLON.Mesh); + targets.forEach((part) => { + const { base, effective } = computeEffectiveTile(part, baseTile, rootScale); + if (!base || !effective) return; + part.metadata.textureTileBaseSize = base; + part.metadata.textureTileSize = effective; + retilePrimitiveMesh(part, effective); + const material = + part.material || + (part.getClassName?.() === "InstancedMesh" + ? part.sourceMesh?.material + : null); + if (material) { + flock.adjustMaterialTilingToMesh(part, material, effective); + } + }); if (mesh.metadata?.glow) { flock.glowMesh(mesh); From ae2b1b71b45993af624212fe679348d9dd96be0c Mon Sep 17 00:00:00 2001 From: Dr Tracy Gardner Date: Tue, 23 Dec 2025 15:02:26 +0000 Subject: [PATCH 11/20] Keep existing texture scaling when no tile size is set --- api/material.js | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/api/material.js b/api/material.js index 86d1dc96..7bbb5369 100644 --- a/api/material.js +++ b/api/material.js @@ -70,18 +70,6 @@ export const flockMaterial = { if (mesh.metadata?.skipAutoTiling) return; - const existingTile = mesh.metadata?.textureTileSize; - const shapeType = mesh?.metadata?.shapeType; - const bakedShapes = new Set(["Box", "Sphere", "Cylinder", "Capsule", "Plane"]); - if ( - shapeType && - bakedShapes.has(shapeType) && - !Number.isFinite(unitsPerTile) && - !Number.isFinite(existingTile) - ) { - return; - } - const tex = material.diffuseTexture || material.albedoTexture || @@ -101,6 +89,19 @@ export const flockMaterial = { const extend = mesh.getBoundingInfo?.()?.boundingBox?.extendSizeWorld; if (!extend) return; + const existingTile = mesh.metadata?.textureTileSize; + const shapeType = mesh?.metadata?.shapeType; + const bakedShapes = new Set(["Box", "Sphere", "Cylinder", "Capsule", "Plane"]); + + if ( + !Number.isFinite(unitsPerTile) && + !Number.isFinite(existingTile) && + (!shapeType || !bakedShapes.has(shapeType)) + ) { + // No explicit tile size and no stored tile size -> keep current texture scaling. + return; + } + const tile = Number.isFinite(unitsPerTile) && unitsPerTile > 0 ? unitsPerTile From a6a23a69411b5a7edb63a9f89c640c2843cd00ee Mon Sep 17 00:00:00 2001 From: Dr Tracy Gardner Date: Tue, 23 Dec 2025 15:07:28 +0000 Subject: [PATCH 12/20] Use 4-unit default tile size and store base tile --- api/material.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/api/material.js b/api/material.js index 7bbb5369..a8ce5720 100644 --- a/api/material.js +++ b/api/material.js @@ -6,6 +6,7 @@ export function setFlockReference(ref) { const PRIMITIVE_TILE_SCALE = 1; const IMPORTED_TILE_SCALE = 0.5; +const DEFAULT_TILE_UNITS = 4; function resolveTextureTileScale(mesh, fallbackScale = null) { const meta = mesh.metadata || (mesh.metadata = {}); @@ -107,9 +108,12 @@ export const flockMaterial = { ? unitsPerTile : Number.isFinite(existingTile) && existingTile > 0 ? existingTile - : 2; + : DEFAULT_TILE_UNITS; mesh.metadata = mesh.metadata || {}; mesh.metadata.textureTileSize = tile; + if (!Number.isFinite(mesh.metadata.textureTileBaseSize)) { + mesh.metadata.textureTileBaseSize = tile; + } if (shapeType && bakedShapes.has(shapeType)) { retilePrimitiveMesh(mesh, tile); @@ -892,7 +896,7 @@ export const flockMaterial = { ? mesh.metadata.textureTileBaseSize : Number.isFinite(mesh.metadata?.textureTileSize) ? mesh.metadata.textureTileSize - : null; + : DEFAULT_TILE_UNITS; if (hasMaterials) { allMeshes.forEach((part) => { From b5b6802379cbd4274cb5efb82c558e2462fd1ddb Mon Sep 17 00:00:00 2001 From: Dr Tracy Gardner Date: Tue, 23 Dec 2025 15:20:21 +0000 Subject: [PATCH 13/20] Tune default tile size to 3 units for objects --- api/material.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/material.js b/api/material.js index a8ce5720..2e19bc78 100644 --- a/api/material.js +++ b/api/material.js @@ -6,7 +6,7 @@ export function setFlockReference(ref) { const PRIMITIVE_TILE_SCALE = 1; const IMPORTED_TILE_SCALE = 0.5; -const DEFAULT_TILE_UNITS = 4; +const DEFAULT_TILE_UNITS = 3; function resolveTextureTileScale(mesh, fallbackScale = null) { const meta = mesh.metadata || (mesh.metadata = {}); From cc282a3584667f9e6fca6d673bca10842f034540 Mon Sep 17 00:00:00 2001 From: Dr Tracy Gardner Date: Tue, 23 Dec 2025 15:24:52 +0000 Subject: [PATCH 14/20] Scale default tile size using mesh-specific factor --- api/material.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/api/material.js b/api/material.js index 2e19bc78..70f599ca 100644 --- a/api/material.js +++ b/api/material.js @@ -109,14 +109,21 @@ export const flockMaterial = { : Number.isFinite(existingTile) && existingTile > 0 ? existingTile : DEFAULT_TILE_UNITS; + mesh.metadata = mesh.metadata || {}; - mesh.metadata.textureTileSize = tile; + if (!Number.isFinite(mesh.metadata.textureTileBaseSize)) { + mesh.metadata.textureTileBaseSize = tile; + } + + const scale = resolveTextureTileScale(mesh); + const effectiveTile = tile * scale; + mesh.metadata.textureTileSize = effectiveTile; if (!Number.isFinite(mesh.metadata.textureTileBaseSize)) { mesh.metadata.textureTileBaseSize = tile; } if (shapeType && bakedShapes.has(shapeType)) { - retilePrimitiveMesh(mesh, tile); + retilePrimitiveMesh(mesh, effectiveTile); tex.uScale = 1; tex.vScale = 1; return; @@ -126,8 +133,8 @@ export const flockMaterial = { const worldHeight = extend.y * 2; const worldDepth = extend.z * 2; - const newUScale = worldWidth / tile; - const newVScale = Math.max(worldHeight, worldDepth) / tile; + const newUScale = worldWidth / effectiveTile; + const newVScale = Math.max(worldHeight, worldDepth) / effectiveTile; if (Number.isFinite(newUScale) && newUScale > 0) tex.uScale = newUScale; if (Number.isFinite(newVScale) && newVScale > 0) tex.vScale = newVScale; From 4ad561fb08f0e8d2e3ae445cbd56f4e0b8676f8d Mon Sep 17 00:00:00 2001 From: Dr Tracy Gardner Date: Tue, 23 Dec 2025 15:34:38 +0000 Subject: [PATCH 15/20] Align default tile to 4 units so block matches initial tiling --- api/material.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/material.js b/api/material.js index 70f599ca..2dec6aed 100644 --- a/api/material.js +++ b/api/material.js @@ -6,7 +6,7 @@ export function setFlockReference(ref) { const PRIMITIVE_TILE_SCALE = 1; const IMPORTED_TILE_SCALE = 0.5; -const DEFAULT_TILE_UNITS = 3; +const DEFAULT_TILE_UNITS = 4; function resolveTextureTileScale(mesh, fallbackScale = null) { const meta = mesh.metadata || (mesh.metadata = {}); From 929a5176e414bc545b85b9ca876102761f926b07 Mon Sep 17 00:00:00 2001 From: Dr Tracy Gardner Date: Tue, 23 Dec 2025 15:40:06 +0000 Subject: [PATCH 16/20] Use base tile and neutral scale when an explicit size is provided --- api/material.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/api/material.js b/api/material.js index 2dec6aed..2a0cff4f 100644 --- a/api/material.js +++ b/api/material.js @@ -90,7 +90,7 @@ export const flockMaterial = { const extend = mesh.getBoundingInfo?.()?.boundingBox?.extendSizeWorld; if (!extend) return; - const existingTile = mesh.metadata?.textureTileSize; + const existingTile = mesh.metadata?.textureTileBaseSize ?? mesh.metadata?.textureTileSize; const shapeType = mesh?.metadata?.shapeType; const bakedShapes = new Set(["Box", "Sphere", "Cylinder", "Capsule", "Plane"]); @@ -103,7 +103,7 @@ export const flockMaterial = { return; } - const tile = + const baseTile = Number.isFinite(unitsPerTile) && unitsPerTile > 0 ? unitsPerTile : Number.isFinite(existingTile) && existingTile > 0 @@ -112,11 +112,12 @@ export const flockMaterial = { mesh.metadata = mesh.metadata || {}; if (!Number.isFinite(mesh.metadata.textureTileBaseSize)) { - mesh.metadata.textureTileBaseSize = tile; + mesh.metadata.textureTileBaseSize = baseTile; } - const scale = resolveTextureTileScale(mesh); - const effectiveTile = tile * scale; + const neutralScale = Number.isFinite(unitsPerTile); + const effectiveTile = + neutralScale ? baseTile : baseTile * resolveTextureTileScale(mesh); mesh.metadata.textureTileSize = effectiveTile; if (!Number.isFinite(mesh.metadata.textureTileBaseSize)) { mesh.metadata.textureTileBaseSize = tile; From 42aff9f1ec4943c481851db421daaa689b09b013 Mon Sep 17 00:00:00 2001 From: Dr Tracy Gardner Date: Tue, 23 Dec 2025 15:46:00 +0000 Subject: [PATCH 17/20] Neutral-scale explicit tile size for consistent object tiling --- api/material.js | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/api/material.js b/api/material.js index 2a0cff4f..250f5a5f 100644 --- a/api/material.js +++ b/api/material.js @@ -59,9 +59,9 @@ function retilePrimitiveMesh(mesh, tileSize) { } } -function computeEffectiveTile(mesh, baseTile, fallbackScale = null) { +function computeEffectiveTile(mesh, baseTile, fallbackScale = null, { neutralScale = false } = {}) { if (!Number.isFinite(baseTile) || baseTile <= 0) return { base: null, effective: null }; - const scale = resolveTextureTileScale(mesh, fallbackScale); + const scale = neutralScale ? 1 : resolveTextureTileScale(mesh, fallbackScale); return { base: baseTile, effective: baseTile * scale }; } @@ -115,13 +115,8 @@ export const flockMaterial = { mesh.metadata.textureTileBaseSize = baseTile; } - const neutralScale = Number.isFinite(unitsPerTile); - const effectiveTile = - neutralScale ? baseTile : baseTile * resolveTextureTileScale(mesh); + const effectiveTile = baseTile; mesh.metadata.textureTileSize = effectiveTile; - if (!Number.isFinite(mesh.metadata.textureTileBaseSize)) { - mesh.metadata.textureTileBaseSize = tile; - } if (shapeType && bakedShapes.has(shapeType)) { retilePrimitiveMesh(mesh, effectiveTile); @@ -952,14 +947,18 @@ export const flockMaterial = { console.log(`Setting material of ${part.name} to ${material.name}`); // Apply the material to the mesh part.material = material; - const { effective } = computeEffectiveTile(part, baseTile, rootScale); + const { effective } = computeEffectiveTile(part, baseTile, rootScale, { + neutralScale: Number.isFinite(tileSize), + }); flock.adjustMaterialTilingToMesh(part, material, effective); }); } const targets = allMeshes.filter((part) => part instanceof flock.BABYLON.Mesh); targets.forEach((part) => { - const { base, effective } = computeEffectiveTile(part, baseTile, rootScale); + const { base, effective } = computeEffectiveTile(part, baseTile, rootScale, { + neutralScale: Number.isFinite(tileSize), + }); if (!base || !effective) return; part.metadata.textureTileBaseSize = base; part.metadata.textureTileSize = effective; From e41d1413eb6e3741afdfab56f04b7ec51b4f11f2 Mon Sep 17 00:00:00 2001 From: Dr Tracy Gardner Date: Tue, 23 Dec 2025 15:56:44 +0000 Subject: [PATCH 18/20] Apply mesh scaling only for primitives when tile is explicit --- api/material.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/api/material.js b/api/material.js index 250f5a5f..6a7b7ac6 100644 --- a/api/material.js +++ b/api/material.js @@ -115,7 +115,10 @@ export const flockMaterial = { mesh.metadata.textureTileBaseSize = baseTile; } - const effectiveTile = baseTile; + const { effective } = computeEffectiveTile(mesh, baseTile, null, { + neutralScale: !!shapeType, + }); + const effectiveTile = effective ?? baseTile; mesh.metadata.textureTileSize = effectiveTile; if (shapeType && bakedShapes.has(shapeType)) { @@ -948,7 +951,7 @@ export const flockMaterial = { // Apply the material to the mesh part.material = material; const { effective } = computeEffectiveTile(part, baseTile, rootScale, { - neutralScale: Number.isFinite(tileSize), + neutralScale: !!part.metadata?.shapeType && Number.isFinite(tileSize), }); flock.adjustMaterialTilingToMesh(part, material, effective); }); @@ -956,9 +959,12 @@ export const flockMaterial = { const targets = allMeshes.filter((part) => part instanceof flock.BABYLON.Mesh); targets.forEach((part) => { - const { base, effective } = computeEffectiveTile(part, baseTile, rootScale, { - neutralScale: Number.isFinite(tileSize), - }); + const { base, effective } = computeEffectiveTile( + part, + baseTile, + rootScale, + { neutralScale: !!part.metadata?.shapeType && Number.isFinite(tileSize) }, + ); if (!base || !effective) return; part.metadata.textureTileBaseSize = base; part.metadata.textureTileSize = effective; From 805b8450abd548944027c5fc89e98f2c50b7d380 Mon Sep 17 00:00:00 2001 From: Dr Tracy Gardner Date: Tue, 23 Dec 2025 19:13:46 +0000 Subject: [PATCH 19/20] Scale defaults for imports so 4 matches box tiling --- api/material.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/api/material.js b/api/material.js index 6a7b7ac6..48204f60 100644 --- a/api/material.js +++ b/api/material.js @@ -116,7 +116,7 @@ export const flockMaterial = { } const { effective } = computeEffectiveTile(mesh, baseTile, null, { - neutralScale: !!shapeType, + neutralScale: !!shapeType, // primitives neutral, imports scaled }); const effectiveTile = effective ?? baseTile; mesh.metadata.textureTileSize = effectiveTile; @@ -959,12 +959,9 @@ export const flockMaterial = { const targets = allMeshes.filter((part) => part instanceof flock.BABYLON.Mesh); targets.forEach((part) => { - const { base, effective } = computeEffectiveTile( - part, - baseTile, - rootScale, - { neutralScale: !!part.metadata?.shapeType && Number.isFinite(tileSize) }, - ); + const { base, effective } = computeEffectiveTile(part, baseTile, rootScale, { + neutralScale: !!part.metadata?.shapeType && Number.isFinite(tileSize), + }); if (!base || !effective) return; part.metadata.textureTileBaseSize = base; part.metadata.textureTileSize = effective; From c0a519755b648674369fb01ba63a185d06b1a7e7 Mon Sep 17 00:00:00 2001 From: Dr Tracy Gardner Date: Tue, 23 Dec 2025 20:08:02 +0000 Subject: [PATCH 20/20] Align imported mesh tiling default with material updates --- api/material.js | 44 +++++++++++++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/api/material.js b/api/material.js index 48204f60..b02d82cb 100644 --- a/api/material.js +++ b/api/material.js @@ -5,7 +5,7 @@ export function setFlockReference(ref) { } const PRIMITIVE_TILE_SCALE = 1; -const IMPORTED_TILE_SCALE = 0.5; +const IMPORTED_TILE_SCALE = 0.25; const DEFAULT_TILE_UNITS = 4; function resolveTextureTileScale(mesh, fallbackScale = null) { @@ -66,7 +66,12 @@ function computeEffectiveTile(mesh, baseTile, fallbackScale = null, { neutralSca } export const flockMaterial = { - adjustMaterialTilingToMesh(mesh, material, unitsPerTile = null) { + adjustMaterialTilingToMesh( + mesh, + material, + unitsPerTile = null, + { unitsAlreadyScaled = false, baseTileOverride = null } = {}, + ) { if (!mesh || !material) return; if (mesh.metadata?.skipAutoTiling) return; @@ -103,22 +108,29 @@ export const flockMaterial = { return; } + const providedTile = + Number.isFinite(unitsPerTile) && unitsPerTile > 0 ? unitsPerTile : null; + const baseTile = - Number.isFinite(unitsPerTile) && unitsPerTile > 0 - ? unitsPerTile - : Number.isFinite(existingTile) && existingTile > 0 - ? existingTile - : DEFAULT_TILE_UNITS; + Number.isFinite(baseTileOverride) && baseTileOverride > 0 + ? baseTileOverride + : providedTile ?? + (Number.isFinite(existingTile) && existingTile > 0 + ? existingTile + : DEFAULT_TILE_UNITS); mesh.metadata = mesh.metadata || {}; - if (!Number.isFinite(mesh.metadata.textureTileBaseSize)) { + if (Number.isFinite(baseTileOverride) && baseTileOverride > 0) { + mesh.metadata.textureTileBaseSize = baseTileOverride; + } else if (!Number.isFinite(mesh.metadata.textureTileBaseSize)) { mesh.metadata.textureTileBaseSize = baseTile; } const { effective } = computeEffectiveTile(mesh, baseTile, null, { - neutralScale: !!shapeType, // primitives neutral, imports scaled + neutralScale: unitsAlreadyScaled || !!shapeType, // primitives neutral, imports scaled }); - const effectiveTile = effective ?? baseTile; + const effectiveTile = + unitsAlreadyScaled && providedTile != null ? providedTile : effective ?? baseTile; mesh.metadata.textureTileSize = effectiveTile; if (shapeType && bakedShapes.has(shapeType)) { @@ -950,10 +962,13 @@ export const flockMaterial = { console.log(`Setting material of ${part.name} to ${material.name}`); // Apply the material to the mesh part.material = material; - const { effective } = computeEffectiveTile(part, baseTile, rootScale, { + const { base, effective } = computeEffectiveTile(part, baseTile, rootScale, { neutralScale: !!part.metadata?.shapeType && Number.isFinite(tileSize), }); - flock.adjustMaterialTilingToMesh(part, material, effective); + flock.adjustMaterialTilingToMesh(part, material, effective, { + unitsAlreadyScaled: true, + baseTileOverride: base, + }); }); } @@ -972,7 +987,10 @@ export const flockMaterial = { ? part.sourceMesh?.material : null); if (material) { - flock.adjustMaterialTilingToMesh(part, material, effective); + flock.adjustMaterialTilingToMesh(part, material, effective, { + unitsAlreadyScaled: true, + baseTileOverride: base, + }); } });