Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 62 additions & 6 deletions api/shapes.js
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,7 @@ export const flockShapes = {
text,
font,
color = "#FFFFFF",
alpha = 1,
size = 50,
depth = 1.0,
position = { x: 0, y: 0, z: 0 },
Expand All @@ -721,6 +722,24 @@ export const flockShapes = {
depth = toDim(depth, 1);
const { x, y, z } = position;

let blockKey = modelId;
let meshId = modelId;
if (modelId.includes("__")) {
[meshId, blockKey] = modelId.split("__");
}

if (flock.scene.getMeshByName(meshId)) {
meshId = meshId + "_" + flock.scene.getUniqueId();
}

flock._recycleOldestByKey(blockKey);

// Guard against overlapping builds for the same blockKey: stamp this build
// with a unique token and drop the result if a newer build supersedes it.
if (!flock._pendingTextBuilds) flock._pendingTextBuilds = new Map();
const buildToken = Symbol();
flock._pendingTextBuilds.set(blockKey, buildToken);

// Create the loading promise
const loadPromise = new Promise(async (resolve, reject) => {
try {
Expand All @@ -746,7 +765,7 @@ export const flockShapes = {
});

// Create Babylon.js mesh from manifold data
mesh = new flock.BABYLON.Mesh(modelId, flock.scene);
mesh = new flock.BABYLON.Mesh(meshId, flock.scene);
const vertexData = new flock.BABYLON.VertexData();

vertexData.positions = meshData.positions;
Expand Down Expand Up @@ -806,7 +825,7 @@ export const flockShapes = {
const fontData = await (await fetch(font)).json();

mesh = flock.BABYLON.MeshBuilder.CreateText(
modelId,
meshId,
text,
fontData,
{
Expand All @@ -823,9 +842,12 @@ export const flockShapes = {
return;
}

mesh.metadata = mesh.metadata || {};
mesh.metadata.blockKey = blockKey;

mesh.position.set(x, y, z);
const material = new flock.BABYLON.StandardMaterial(
"textMaterial_" + modelId,
"textMaterial_" + meshId,
flock.scene,
);

Expand All @@ -834,31 +856,65 @@ export const flockShapes = {
);
material.backFaceCulling = false;
material.emissiveColor = material.diffuseColor.scale(0.2);
material.alpha = toAlpha(alpha);

mesh.material = material;

mesh.computeWorldMatrix(true);
mesh.refreshBoundingInfo();

// Normalize mesh so bounding box height equals the size parameter.
// Font cap-height is typically ~70% of em-height, causing a mismatch
// between SIZE in the block and the visual height. Baking a uniform XY
// scale here ensures SIZE always equals the rendered height, which
// prevents a visual jump when the scale gizmo is released.
{
const bbExt = mesh.getBoundingInfo().boundingBox.extendSize;
const bbHeight = bbExt.y * 2;
if (bbHeight > 0 && Math.abs(bbHeight - size) > 0.001) {
const normScale = size / bbHeight;
const savedPos = mesh.position.clone();
mesh.position = flock.BABYLON.Vector3.Zero();
mesh.scaling.x = normScale;
mesh.scaling.y = normScale;
mesh.bakeCurrentTransformIntoVertices();
mesh.scaling = flock.BABYLON.Vector3.One();
mesh.position = savedPos;
mesh.computeWorldMatrix(true);
mesh.refreshBoundingInfo();
}
}

mesh.setEnabled(true);
mesh.visibility = 1;

const textShape = new flock.BABYLON.PhysicsShapeMesh(mesh, flock.scene);
flock.applyPhysics(mesh, textShape);

// Drop stale result if a newer build for this blockKey was started
if (flock._pendingTextBuilds.get(blockKey) !== buildToken) {
mesh.dispose();
resolve();
return;
}
flock._pendingTextBuilds.delete(blockKey);

flock._registerInstance(blockKey, mesh.name);

if (callback) {
requestAnimationFrame(callback);
}

resolve();
} catch (error) {
console.error(`Error creating 3D text '${modelId}':`, error);
console.error(`Error creating 3D text '${meshId}':`, error);
reject(error);
}
});

// Store promise for whenModelReady coordination
flock.modelReadyPromises.set(modelId, loadPromise);
flock.modelReadyPromises.set(meshId, loadPromise);

return modelId;
return meshId;
},
};
8 changes: 2 additions & 6 deletions blocks/text.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
getHelpUrlFor,
nextVariableIndexes,
handleBlockCreateEvent,
handleBlockChange,
registerBlockHandler,
} from "./blocks.js";
import {
Expand Down Expand Up @@ -456,12 +457,7 @@ export function defineTextBlocks() {
this.setStyle("text_blocks");

registerBlockHandler(this, (changeEvent) =>
handleBlockCreateEvent(
this,
changeEvent,
variableNamePrefix,
nextVariableIndexes,
),
handleBlockChange(this, changeEvent, variableNamePrefix),
);
},
};
Expand Down
13 changes: 7 additions & 6 deletions generators/generators-text.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
getFieldValue,
sanitizeForCode,
emitSafeTextArg,
getVariableInfo,
} from "./generators-utilities.js";

export function registerTextGenerators(javascriptGenerator) {
Expand Down Expand Up @@ -361,9 +362,9 @@ export function registerTextGenerators(javascriptGenerator) {

// Add 3D text --------------------------------------------
javascriptGenerator.forBlock["create_3d_text"] = function (block) {
const variableName = javascriptGenerator.nameDB_.getName(
block.getFieldValue("ID_VAR"),
Blockly.Names.NameType.VARIABLE,
const { generatedName: variableName, userVariableName } = getVariableInfo(
block,
"ID_VAR",
);

let rawText = getFieldValue(block, "TEXT", "Hello World");
Expand All @@ -383,9 +384,9 @@ export function registerTextGenerators(javascriptGenerator) {
if (fontKey === "__fonts_FreeSans_Bold_json")
font = "./fonts/FreeSans_Bold.json";

const meshId = "text_" + generateUniqueId();
meshMap[meshId] = block;
meshBlockIdMap[meshId] = block.id;
const meshId = `${userVariableName}__${block.id}`;
meshMap[block.id] = block;
meshBlockIdMap[block.id] = block.id;

let doCode = "";
if (block.getInput("DO")) {
Expand Down
30 changes: 29 additions & 1 deletion ui/addmeshes.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import * as Blockly from "blockly";
import {
meshMap,
meshBlockIdMap,
generateUniqueId,
} from "../generators/generators.js";
import { flock } from "../flock.js";
import {
Expand All @@ -25,6 +24,7 @@ export function createMeshOnCanvas(block) {
"create_cylinder",
"create_capsule",
"create_plane",
"create_3d_text",
].includes(block.type);

if (isShape) {
Expand Down Expand Up @@ -628,6 +628,34 @@ function createShapeInternal(block) {
});
break;

case "create_3d_text": {
({ colorOrMaterial: color, alpha } = resolveColorOrMaterial("#FFFFFF"));

const textInput = block.getInput("TEXT");
const textTarget = textInput?.connection?.targetBlock?.();
const textValue = textTarget
? textTarget.getFieldValue("TEXT") ?? textTarget.getFieldValue("NUM") ?? "Hello World"
: "Hello World";

const fontSize = parseFloat(getConnectedFieldValue("SIZE", "NUM", "50"));
const textDepth = parseFloat(getConnectedFieldValue("DEPTH", "NUM", "1"));

meshMap[block.id] = block;
meshBlockIdMap[block.id] = block.id;

newMesh = flock.create3DText({
text: String(textValue),
font: "fonts/FreeSansBold.ttf",
color,
alpha,
size: fontSize,
depth: textDepth,
position: { x: position.x, y: position.y, z: position.z },
modelId: `3dtext__${block.id}`,
});
break;
}

default:
return;
}
Expand Down
22 changes: 22 additions & 0 deletions ui/blockmesh.js
Original file line number Diff line number Diff line change
Expand Up @@ -1074,6 +1074,28 @@ function handlePrimitiveGeometryChange(mesh, block, changed) {
}
break;
}

case "create_3d_text": {
if (["SIZE", "DEPTH"].includes(changed)) {
const newSize = parseFloat(
block.getInput("SIZE").connection.targetBlock().getFieldValue("NUM"),
);
const newDepth = parseFloat(
block.getInput("DEPTH").connection.targetBlock().getFieldValue("NUM"),
);

mesh.computeWorldMatrix(true);
mesh.refreshBoundingInfo();
const ext = mesh.getBoundingInfo().boundingBox.extendSize;
const currentW = ext.x * 2 * mesh.scaling.x;
const currentH = ext.y * 2 * mesh.scaling.y;
const newW = currentH > 0 ? currentW * (newSize / currentH) : currentW;

setAbsoluteSize(mesh, newW, newSize, newDepth);
repositionPrimitiveFromBlock();
}
break;
}
}
}

Expand Down
58 changes: 54 additions & 4 deletions ui/gizmos.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ const orangeColor = flock.BABYLON.Color3.FromHexString("#D55E00"); // Colour for
window.selectedColor = "#ffffff"; // Default color
let colorPicker = null;

// 3D text scale gizmo axis tracking
let textScaleAxis = null;
let textOrigScaleZ = 1;

// Color picking keyboard mode variables
let colorPickingKeyboardMode = false;
let colorPickingCallback = null;
Expand Down Expand Up @@ -648,8 +652,8 @@ export function toggleGizmo(gizmoType) {
if (event.type === flock.BABYLON.PointerEventTypes.POINTERPICK) {
if (gizmoManager.attachedMesh) {
resetAttachedMesh();
blockKey = findParentWithBlockId(gizmoManager.attachedMesh)?.metadata
?.blockKey;
blockKey = findParentWithBlockId(gizmoManager.attachedMesh)
?.metadata?.blockKey;
}
let pickedMesh = event.pickInfo.pickedMesh;

Expand Down Expand Up @@ -933,6 +937,24 @@ export function toggleGizmo(gizmoType) {

case "scale":
configureScaleGizmo(gizmoManager);
{
const sg = gizmoManager.gizmos.scaleGizmo;
if (!sg._textAxisObserversRegistered) {
sg.xGizmo.dragBehavior.onDragStartObservable.add(
() => (textScaleAxis = "x"),
);
sg.yGizmo.dragBehavior.onDragStartObservable.add(
() => (textScaleAxis = "y"),
);
sg.zGizmo.dragBehavior.onDragStartObservable.add(
() => (textScaleAxis = "z"),
);
sg.uniformScaleGizmo.dragBehavior.onDragStartObservable.add(
() => (textScaleAxis = "uniform"),
);
sg._textAxisObserversRegistered = true;
}
}
gizmoManager.onAttachedToMeshObservable.add((mesh) => {
if (!mesh) return;

Expand Down Expand Up @@ -965,6 +987,21 @@ export function toggleGizmo(gizmoType) {
case "create_cylinder":
mesh.scaling.z = mesh.scaling.x;
break;
case "create_3d_text":
if (textScaleAxis === "z") {
// Z handle: depth only — lock X and Y
mesh.scaling.x = 1;
mesh.scaling.y = 1;
} else if (textScaleAxis === "x" || textScaleAxis === "uniform") {
// X or uniform: size only — keep Y = X, lock Z
mesh.scaling.y = mesh.scaling.x;
mesh.scaling.z = textOrigScaleZ;
} else if (textScaleAxis === "y") {
// Y handle: size only — keep X = Y, lock Z
mesh.scaling.x = mesh.scaling.y;
mesh.scaling.z = textOrigScaleZ;
}
break;
}
}
});
Expand All @@ -975,6 +1012,8 @@ export function toggleGizmo(gizmoType) {
mesh.computeWorldMatrix(true);
mesh.refreshBoundingInfo();
originalBottomY = mesh.getBoundingInfo().boundingBox.minimumWorld.y;
textOrigScaleZ = mesh.scaling.z;
textScaleAxis = null;

const motionType = mesh.physics?.getMotionType();
mesh.savedMotionType = motionType;
Expand All @@ -995,6 +1034,7 @@ export function toggleGizmo(gizmoType) {
gizmoManager.gizmos.scaleGizmo.onDragEndObservable.add(() => {
const mesh = gizmoManager.attachedMesh;
const block = meshMap[mesh?.metadata?.blockKey];
textScaleAxis = null;

if (mesh.savedMotionType != null) {
mesh.physics.setMotionType(mesh.savedMotionType);
Expand Down Expand Up @@ -1075,6 +1115,16 @@ export function toggleGizmo(gizmoType) {
});
break;

case "create_3d_text": {
const currentSize = getNumberInput(block, "SIZE");
const currentDepth = getNumberInput(block, "DEPTH");
setNumberInputs(block, {
SIZE: currentSize * mesh.scaling.y,
DEPTH: currentDepth * mesh.scaling.z,
});
break;
}

case "load_model":
case "load_multi_object":
case "load_object":
Expand Down Expand Up @@ -1410,8 +1460,8 @@ export function setGizmoManager(value) {
// KeyCode for 'Delete' key is 46
// Handle delete action

const blockKey = findParentWithBlockId(gizmoManager.attachedMesh)?.metadata
?.blockKey;
const blockKey = findParentWithBlockId(gizmoManager.attachedMesh)
?.metadata?.blockKey;
const blockId = meshBlockIdMap[blockKey];

deleteBlockWithUndo(blockId);
Expand Down
Loading