diff --git a/api/mesh.js b/api/mesh.js index f43b23b4..9b537c1f 100644 --- a/api/mesh.js +++ b/api/mesh.js @@ -266,7 +266,7 @@ export const flockMesh = { ? flock.getGroundLevelAt(px, pz) : py; - mesh.position = new flock.BABYLON.Vector3(px, resolvedY, pz); + flock.setBlockPositionOnMesh(mesh, { x: px, y: resolvedY, z: pz, useY: true }); mesh.metadata = { ...(mesh.metadata || {}), shapeType }; mesh.metadata.blockKey = mesh.name; @@ -293,7 +293,7 @@ export const flockMesh = { if (shouldResolveGroundLevel && !flock.ground) { flock.waitForGroundReady().then(() => { const groundY = flock.getGroundLevelAt(px, pz); - mesh.position.y = groundY; + flock.setBlockPositionOnMesh(mesh, { x: px, y: groundY, z: pz, useY: true }); if (mesh.physics) { mesh.physics.setTargetTransform( mesh.position, diff --git a/api/shapes.js b/api/shapes.js index 200f3763..f567e1e5 100644 --- a/api/shapes.js +++ b/api/shapes.js @@ -276,7 +276,6 @@ export const flockShapes = { // Initialise the mesh with position, color, and other properties flock.initializeMesh(newBox, position, color, "Box", alpha); - newBox.position.y += height / 2; // Middle of the box newBox.metadata = newBox.metadata || {}; newBox.metadata.blockKey = blockKey; @@ -349,7 +348,6 @@ export const flockShapes = { // Initialise the mesh with position, color, and other properties flock.initializeMesh(newSphere, position, color, "Sphere", alpha); - newSphere.position.y += diameterY / 2; newSphere.metadata = newSphere.metadata || {}; newSphere.metadata.blockKey = blockKey; @@ -433,7 +431,6 @@ export const flockShapes = { // Initialise the mesh with position, color, and other properties flock.initializeMesh(newCylinder, position, color, "Cylinder", alpha); - newCylinder.position.y += height / 2; // Initialise the mesh with position, color, and other properties newCylinder.metadata = newCylinder.metadata || {}; @@ -504,7 +501,6 @@ export const flockShapes = { // Initialise the mesh with position, color, and other properties flock.initializeMesh(newCapsule, position, color, "Capsule", alpha); - newCapsule.position.y += height / 2; flock.setCapsuleUVs(newCapsule, radius, height, 1); // Adjust texturePhysicalSize as needed @@ -576,11 +572,13 @@ export const flockShapes = { newPlane.metadata.shape = "plane"; newPlane.metadata.blockKey = blockKey; - newPlane.position = new flock.BABYLON.Vector3( - position[0], - position[1] + height / 2, - position[2], - ); + flock.setBlockPositionOnMesh(newPlane, { + x: position[0], + y: position[1], + z: position[2], + useY: true, + meshName: newPlane.name, + }); const planeBody = new flock.BABYLON.PhysicsBody( newPlane, diff --git a/api/transform.js b/api/transform.js index d21a06fc..63ee2900 100644 --- a/api/transform.js +++ b/api/transform.js @@ -1,10 +1,89 @@ let flock; +function resolvePositionInputs( + mesh, + { x = 0, y = 0, z = 0, useY = true, meshName = "" } = {}, +) { + const nextX = x ?? mesh.position.x; + const nextY = y ?? mesh.position.y; + const nextZ = z ?? mesh.position.z; + + return { + x: nextX, + y: nextY, + z: nextZ, + useY, + isCamera: meshName === "__active_camera__", + }; +} + +function applyPositionWithCurrentBaseRule( + mesh, + { x = 0, y = 0, z = 0, useY = true, meshName = "" } = {}, +) { + const { x: nextX, y: nextY, z: nextZ, isCamera } = resolvePositionInputs( + mesh, + { + x, + y, + z, + useY, + meshName, + }, + ); + + mesh.position.set(nextX, useY ? nextY : mesh.position.y, nextZ); + + if (useY && !isCamera && typeof mesh.getBoundingInfo === "function") { + mesh.computeWorldMatrix(true); + mesh.refreshBoundingInfo?.(); + const boundingInfo = mesh.getBoundingInfo(); + const minWorldY = boundingInfo?.boundingBox?.minimumWorld?.y; + + if (Number.isFinite(minWorldY)) { + const deltaY = nextY - minWorldY; + if (Math.abs(deltaY) > 1e-6) { + mesh.position.y += deltaY; + } + } + } + + mesh.computeWorldMatrix(true); + + return { + x: mesh.position.x, + y: mesh.position.y, + z: mesh.position.z, + }; +} + export function setFlockReference(ref) { flock = ref; } export const flockTransform = { + async setBlockPositionOnMesh( + mesh, + { x = 0, y = 0, z = 0, useY = true, meshName = "" } = {}, + ) { + if (!mesh) return; + + let nextY = y; + const groundLevelSentinel = -999999; + const numericY = typeof nextY === "string" ? Number(nextY) : nextY; + if (nextY === "__ground__level__" || numericY === groundLevelSentinel) { + await flock.waitForGroundReady(); + nextY = flock.getGroundLevelAt(x, z); + } + + applyPositionWithCurrentBaseRule(mesh, { + x, + y: nextY, + z, + useY, + meshName: meshName || mesh.name || "", + }); + }, positionAt(meshName, { x = 0, y = 0, z = 0, useY = true } = {}) { return new Promise((resolve, reject) => { flock.whenModelReady(meshName, async (mesh) => { @@ -17,13 +96,6 @@ export const flockTransform = { y ??= mesh.position.y; z ??= mesh.position.z; - const groundLevelSentinel = -999999; - const numericY = typeof y === "string" ? Number(y) : y; - if (y === "__ground__level__" || numericY === groundLevelSentinel) { - await flock.waitForGroundReady(); - y = flock.getGroundLevelAt(x, z); - } - if (mesh.physics) { if ( mesh.physics.getMotionType() !== @@ -35,31 +107,13 @@ export const flockTransform = { } } - // Use a consistent placement rule: requested Y is mesh base (minimumWorld.y) - // so imported models and primitives share the same semantics. - mesh.position.set( + await this.setBlockPositionOnMesh(mesh, { x, - useY ? y : mesh.position.y, + y, z, - ); - - if ( - useY && - meshName !== "__active_camera__" && - typeof mesh.getBoundingInfo === "function" - ) { - mesh.computeWorldMatrix(true); - mesh.refreshBoundingInfo?.(); - const boundingInfo = mesh.getBoundingInfo(); - const minWorldY = boundingInfo?.boundingBox?.minimumWorld?.y; - - if (Number.isFinite(minWorldY)) { - const deltaY = y - minWorldY; - if (Math.abs(deltaY) > 1e-6) { - mesh.position.y += deltaY; - } - } - } + useY, + meshName, + }); // Update physics and world matrix if (mesh.physics) { @@ -837,6 +891,22 @@ export const flockTransform = { }); }); }, + + getBlockPositionFromMesh(mesh) { + if (!mesh) return { x: 0, y: 0, z: 0 }; + + mesh.computeWorldMatrix?.(true); + mesh.refreshBoundingInfo?.(); + + const boundingInfo = mesh.getBoundingInfo?.(); + const minY = boundingInfo?.boundingBox?.minimumWorld?.y; + + return { + x: mesh.position?.x ?? 0, + y: Number.isFinite(minY) ? minY : (mesh.position?.y ?? 0), + z: mesh.position?.z ?? 0, + }; + }, _getAnchor(mesh) { if (!mesh) return null; diff --git a/ui/gizmos.js b/ui/gizmos.js index 07b1efa6..a05fff85 100644 --- a/ui/gizmos.js +++ b/ui/gizmos.js @@ -13,7 +13,6 @@ import { setBlockXYZ, duplicateBlockAndInsert, findParentWithBlockId, - calculateYPosition, setNumberInputs, getNumberInput, } from "./blocklyutil.js"; @@ -732,8 +731,13 @@ export function toggleGizmo(gizmoType) { const block = meshMap[mesh.metadata.blockKey]; if (block) { - const meshY = calculateYPosition(mesh, block); - setBlockXYZ(block, mesh.position.x, meshY, mesh.position.z); + const blockPosition = flock.getBlockPositionFromMesh(mesh); + setBlockXYZ( + block, + blockPosition.x, + blockPosition.y, + blockPosition.z, + ); } }); @@ -779,8 +783,13 @@ export function toggleGizmo(gizmoType) { const block = meshMap[mesh.metadata.blockKey]; if (block) { - const meshY = calculateYPosition(mesh, block); - setBlockXYZ(block, mesh.position.x, meshY, mesh.position.z); + const blockPosition = flock.getBlockPositionFromMesh(mesh); + setBlockXYZ( + block, + blockPosition.x, + blockPosition.y, + blockPosition.z, + ); } });