Skip to content
Merged
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
235 changes: 158 additions & 77 deletions ui/gizmos.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,22 +28,32 @@ import {
stopCanvasKeyboardMode,
} from "./canvas-utils.js";
import { createAxisKeyboardHandler } from "./axis-keyboard.js";
import { cleanup } from "manifold-3d/lib/animation.js";
export let gizmoManager;

// Enable debug messages
const DEBUG = true;

const blueColor = flock.BABYLON.Color3.FromHexString("#0072B2"); // Colour for X-axis
const greenColor = flock.BABYLON.Color3.FromHexString("#009E73"); // Colour for Y-axis
const orangeColor = flock.BABYLON.Color3.FromHexString("#D55E00"); // Colour for Z-axis

const FAST_CURSOR = 1; // Step for moving KB cursor quickly
const DEFAULT_CURSOR = 0.1; // Step for moving KB cursor slowly (default)

window.selectedColor = "#ffffff"; // Default color
let colorPicker = null;

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

// Track state
let cameraMode = "play";
let activeDuplicatePickHandler = null; // Are they in the middle of a duplication?
let stopAxisKeyboard = null; // Are they transforming?
let activePick = null; // [Select mesh?]
let activeDuplicatePickHandler = null; // [Clone mesh?]
let stopAxisKeyboard = null; // Axis keyboard active?
let positionGizmoObserver = null; // [Move mesh?]

// Track DO sections and their associated blocks for cleanup
const gizmoCreatedBlocks = new Map(); // blockId -> { parentId, createdDoSection, timestamp }
Expand Down Expand Up @@ -495,6 +505,107 @@ function getScaledSize(mesh) {
};
}

// Clean up gizmo state if aborted
function exitGizmoState() {
cleanupScenePick(); // Stop picking

// Properly clean up if duplicating
if (activeDuplicatePickHandler) {
window.removeEventListener("click", activeDuplicatePickHandler);
activeDuplicatePickHandler = null;
}

// Stop the axis keyboard
stopAxisKeyboard?.();
stopAxisKeyboard = null;
// Remove position observer
if (positionGizmoObserver) {
gizmoManager.onAttachedToMeshObservable.remove(positionGizmoObserver);
positionGizmoObserver = null;
}
// Remove active class from all buttons
document
.querySelectorAll(".gizmo-button")
.forEach((btn) => btn.classList.remove("active"));
disableGizmos();
document.body.style.cursor = "default";
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Start the keyboard handler for moving a mesh
function startMoveKeyboardHandler(mesh) {
document.body.style.cursor = "default";
stopAxisKeyboard?.();
stopAxisKeyboard = null;
setTimeout(() => {
stopAxisKeyboard = createAxisKeyboardHandler({
onMove: (dx, dy, dz) => {
mesh.position.x += dx;
mesh.position.y += dy;
mesh.position.z += dz;
mesh.computeWorldMatrix(true);
const block = meshMap[mesh?.metadata?.blockKey];
if (block) {
const pos = flock.getBlockPositionFromMesh(mesh);
setBlockXYZ(block, pos.x, pos.y, pos.z);
}
},
onConfirm: () => {
exitGizmoState();
document.getElementById("positionButton")?.focus();
},
onCancel: () => {
exitGizmoState();
// Deselect so you get [select mesh] for next tool
gizmoManager.attachToMesh(null);
document.getElementById("positionButton")?.focus();
},
stepNormal: DEFAULT_CURSOR,
stepFast: FAST_CURSOR,
});
}, 0);
}

// Pick a mesh (used by multiple gizmos)
function pickMeshFromScene(onPicked) {
cleanupScenePick(); // Stop picking
resetAttachedMesh();

const pointerObservable = flock.scene.onPointerObservable;
const pointerObserver = pointerObservable.add((event) => {
if (event.type === flock.BABYLON.PointerEventTypes.POINTERPICK) {
cleanupScenePick();
onPicked(event.pickInfo.pickedMesh, event.pickInfo.pickedPoint);
}
});

activePick = { pointerObservable, pointerObserver };

setTimeout(() => {
startCanvasKeyboardMode(
(x, y) => {
const pick = flock.scene.pick(x, y);
cleanupScenePick();
onPicked(pick?.pickedMesh, pick?.pickedPoint);
},
false,
(x, y) =>
!!flock.scene.pick(x, y, (m) => m.isPickable && m.name !== "ground")
?.hit,
);
document.body.style.cursor = "crosshair";
}, 0);
}

// Clean up after picking
function cleanupScenePick() {
if (activePick) {
activePick.pointerObservable.remove(activePick.pointerObserver);
activePick = null;
}
stopCanvasKeyboardMode();
document.body.style.cursor = "default";
}

export function disableGizmos() {
if (!gizmoManager) return;
// Disable all gizmos
Expand All @@ -507,16 +618,22 @@ export function disableGizmos() {

// Toggle which Gizmo is being used
export function toggleGizmo(gizmoType) {
// Is this gizmo already active? If so, toggle it off
const button = document.getElementById(`${gizmoType}Button`);
if (button?.classList.contains("active")) {
exitGizmoState();
return;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// No buttons should be highlighted
document
.querySelectorAll(".gizmo-button")
.forEach((btn) => btn.classList.remove("active"));

// If they abandoned a duplicate half way, remove listener
if (activeDuplicatePickHandler) {
window.removeEventListener("click", activeDuplicatePickHandler);
activeDuplicatePickHandler = null;
if (gizmoType === "duplicate") return;
if (gizmoType === "duplicate" && activeDuplicatePickHandler) {
exitGizmoState();
return;
}

// If they were mid-transform, clean up
Expand Down Expand Up @@ -958,44 +1075,42 @@ function handleRotationGizmo() {
function handlePositionGizmo() {
configurePositionGizmo(gizmoManager);

// Highlight the move button
const positionButton = document.getElementById("positionButton");
positionButton.classList.add("active");

const mesh = gizmoManager.attachedMesh;
if (mesh) {
const original = mesh.position.clone();
setTimeout(() => {
stopAxisKeyboard = createAxisKeyboardHandler({
onMove: (dx, dy, dz) => {
mesh.position.x += dx;
mesh.position.y += dy;
mesh.position.z += dz;
},
onConfirm: () => {
mesh.computeWorldMatrix(true);
const block = meshMap[mesh?.metadata?.blockKey];
if (block) {
const pos = flock.getBlockPositionFromMesh(mesh);
setBlockXYZ(block, pos.x, pos.y, pos.z);
}
disableGizmos();
},
onCancel: () => {
mesh.position.copyFrom(original);
disableGizmos();
},
stepNormal: 0.1,
stepFast: 1,
});
}, 0);
startMoveKeyboardHandler(mesh);
} else {
pickMeshFromScene((pickedMesh) => {
if (!pickedMesh || pickedMesh.name === "ground") return;
if (pickedMesh.parent) pickedMesh = getRootMesh(pickedMesh.parent);
gizmoManager.attachToMesh(pickedMesh);
});
}

gizmoManager.onAttachedToMeshObservable.add((mesh) => {
if (!mesh) return;
// Don't attach to multiple meshes
if (positionGizmoObserver) {
gizmoManager.onAttachedToMeshObservable.remove(positionGizmoObserver);
}

const blockKey = mesh?.metadata?.blockKey;
const blockId = blockKey ? meshMap[blockKey] : null;
if (!blockId) return;
positionGizmoObserver = gizmoManager.onAttachedToMeshObservable.add(
(mesh) => {
if (!mesh) {
exitGizmoState();
return;
}

highlightBlockById(Blockly.getMainWorkspace(), blockId);
});
startMoveKeyboardHandler(mesh); // Reattach

const blockKey = mesh?.metadata?.blockKey;
const blockId = blockKey ? meshMap[blockKey] : null;
if (!blockId) return;

highlightBlockById(Blockly.getMainWorkspace(), blockId);
},
);

gizmoManager.gizmos.positionGizmo.onDragStartObservable.add(() => {
const mesh = gizmoManager.attachedMesh;
Expand Down Expand Up @@ -1076,7 +1191,6 @@ function handleBoundsGizmo() {

// Select: Allow the user to select a mesh by clicking on it
function handleSelectGizmo() {
let blockKey;
gizmoManager.selectGizmoEnabled = true;

function applySelection(pickedMesh, pickedPoint) {
Expand Down Expand Up @@ -1121,44 +1235,8 @@ function handleSelectGizmo() {
}
}

// Wait until the click has propagated otherwise
// the keyboard mode gets cancelled immediately
setTimeout(() => {
startCanvasKeyboardMode(
(x, y) => {
if (gizmoManager.attachedMesh) {
resetAttachedMesh();
blockKey = findParentWithBlockId(gizmoManager.attachedMesh)?.metadata
?.blockKey;
}
const pick = flock.scene.pick(x, y);
applySelection(pick?.pickedMesh, pick?.pickedPoint);
stopCanvasKeyboardMode();
},
false,
(x, y) =>
!!flock.scene.pick(x, y, (m) => m.isPickable && m.name !== "ground")
?.hit,
);
}, 0);

// Store the pointer observable
const pointerObservable = flock.scene.onPointerObservable;

// Add the observer
const pointerObserver = pointerObservable.add((event) => {
if (event.type === flock.BABYLON.PointerEventTypes.POINTERPICK) {
if (gizmoManager.attachedMesh) {
resetAttachedMesh();
blockKey = findParentWithBlockId(gizmoManager.attachedMesh)?.metadata
?.blockKey;
}

applySelection(event.pickInfo.pickedMesh, event.pickInfo.pickedPoint);

pointerObservable.remove(pointerObserver);
}
});
// Use helper function to pick the mesh
pickMeshFromScene(applySelection);
}

// Duplicate: Create a copy of the selected mesh and its corresponding block,
Expand Down Expand Up @@ -1656,3 +1734,6 @@ export function configureScaleGizmo(
// Export functions for global access
window.toggleGizmo = toggleGizmo;
window.turnOffAllGizmos = turnOffAllGizmos;
if (DEBUG) {
window._debugPick = () => flock.scene.onPointerObservable._observers.length;
}
Loading