315 lines
No EOL
9.7 KiB
JavaScript
315 lines
No EOL
9.7 KiB
JavaScript
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
|
|
|
import { THREE } from '../ext/three.js';
|
|
import { broker } from '../moto/broker.js';
|
|
import { space as motoSpace } from '../moto/space.js';
|
|
import { api as meshApi } from './api.js';
|
|
import { util as meshUtil } from './util.js';
|
|
|
|
const {
|
|
BoxGeometry,
|
|
EdgesGeometry,
|
|
LineSegments,
|
|
MeshBasicMaterial,
|
|
LineBasicMaterial,
|
|
Group,
|
|
Vector3,
|
|
Quaternion,
|
|
Mesh
|
|
} = THREE;
|
|
|
|
class TransformTool {
|
|
constructor() {
|
|
this.group = new Group();
|
|
this.handles = [];
|
|
this.lines = []; // Add array to track connecting lines
|
|
this.mode = '2d'; // or '3d'
|
|
this.bounds = null;
|
|
this.handleSize = 2;
|
|
this.handleColor = 0x0088ff;
|
|
this.handleOpacity = 0.5;
|
|
this.enabled = true; // persisted state
|
|
this.visible = false; // selection state
|
|
|
|
// Create materials
|
|
this.handleMaterial = new MeshBasicMaterial({
|
|
color: this.handleColor,
|
|
transparent: true,
|
|
opacity: this.handleOpacity
|
|
});
|
|
|
|
this.edgeMaterial = new LineBasicMaterial({
|
|
color: this.handleColor,
|
|
transparent: true,
|
|
opacity: 0.8
|
|
});
|
|
|
|
// Add to scene
|
|
motoSpace.world.add(this.group);
|
|
this.group.visible = false;
|
|
|
|
// Listen for selection changes
|
|
broker.listeners({
|
|
selection_update: () => this.update(),
|
|
sketch_selections: () => this.update(),
|
|
selection_drag: () => this.update(),
|
|
selection_move: () => this.update(),
|
|
selection_scale: () => this.update(),
|
|
selection_rotate: () => this.update(),
|
|
selection_qrotate: () => this.update(),
|
|
sketch_render: () => this.update(),
|
|
history_undo: () => this.update(),
|
|
history_redo: () => this.update(),
|
|
move: () => this.update(),
|
|
rotate: () => this.update(),
|
|
qrotate: () => this.update(),
|
|
scale: () => this.update(),
|
|
});
|
|
}
|
|
|
|
setEnabled(enabled) {
|
|
this.enabled = enabled;
|
|
if (meshApi && meshApi.prefs) {
|
|
meshApi.prefs.map.space.bounds = enabled;
|
|
meshApi.prefs.save();
|
|
}
|
|
this.updateVisibility();
|
|
}
|
|
|
|
setVisible(visible) {
|
|
this.visible = visible;
|
|
this.updateVisibility();
|
|
}
|
|
|
|
show() {
|
|
this.setVisible(true);
|
|
}
|
|
|
|
hide() {
|
|
this.setVisible(false);
|
|
}
|
|
|
|
toggleBounds() {
|
|
this.setEnabled(!this.enabled);
|
|
}
|
|
|
|
updateVisibility() {
|
|
this.group.visible = this.enabled && this.visible;
|
|
}
|
|
|
|
update() {
|
|
// Get current selection
|
|
const selection = meshApi.selection;
|
|
const sketch = selection.sketch();
|
|
const models = selection.models();
|
|
|
|
// Hide if no selection
|
|
if (!sketch && !models.length) {
|
|
this.hide();
|
|
return;
|
|
}
|
|
|
|
// Show transform tool
|
|
this.show();
|
|
|
|
// Get bounds based on selection type
|
|
let bounds;
|
|
if (sketch) {
|
|
// For sketches, use 2D mode
|
|
this.mode = '2d';
|
|
bounds = sketch.bounds;
|
|
} else {
|
|
// For models/groups, use 3D mode
|
|
this.mode = '3d';
|
|
const objects = models;
|
|
bounds = meshUtil.bounds(objects);
|
|
}
|
|
|
|
this.setBounds(bounds);
|
|
}
|
|
|
|
setBounds(bounds, mode = '3d') {
|
|
this.mode = mode;
|
|
this.bounds = bounds;
|
|
this.updateHandles();
|
|
}
|
|
|
|
updateHandles() {
|
|
// Clear existing handles and lines
|
|
this.handles.forEach(handle => this.group.remove(handle));
|
|
this.lines.forEach(line => this.group.remove(line));
|
|
this.handles = [];
|
|
this.lines = [];
|
|
|
|
if (!this.bounds) return;
|
|
|
|
const { min, max } = this.bounds;
|
|
const corners = this.getCorners(min, max);
|
|
|
|
// Create handles for each corner
|
|
corners.forEach(corner => {
|
|
const handle = this.createHandle();
|
|
handle.position.copy(corner);
|
|
this.handles.push(handle);
|
|
this.group.add(handle);
|
|
});
|
|
|
|
// Add connecting lines between handles
|
|
if (this.mode === '2d') {
|
|
// For 2D mode, connect handles with horizontal and vertical lines
|
|
const lineGeometry = new THREE.BufferGeometry();
|
|
const linePositions = [];
|
|
|
|
// Horizontal lines
|
|
linePositions.push(
|
|
corners[0].x, corners[0].y, corners[0].z, // 0 to 1
|
|
corners[1].x, corners[1].y, corners[1].z,
|
|
corners[2].x, corners[2].y, corners[2].z, // 2 to 3
|
|
corners[3].x, corners[3].y, corners[3].z
|
|
);
|
|
|
|
// Vertical lines
|
|
linePositions.push(
|
|
corners[0].x, corners[0].y, corners[0].z, // 0 to 3
|
|
corners[3].x, corners[3].y, corners[3].z,
|
|
corners[1].x, corners[1].y, corners[1].z, // 1 to 2
|
|
corners[2].x, corners[2].y, corners[2].z
|
|
);
|
|
|
|
lineGeometry.setAttribute('position', new THREE.Float32BufferAttribute(linePositions, 3));
|
|
const line = new THREE.LineSegments(lineGeometry, this.edgeMaterial);
|
|
this.lines.push(line);
|
|
this.group.add(line);
|
|
} else {
|
|
// For 3D mode, connect handles with axis-aligned lines
|
|
const lineGeometry = new THREE.BufferGeometry();
|
|
const linePositions = [];
|
|
|
|
// Bottom face (horizontal lines)
|
|
for (let i = 0; i < 4; i++) {
|
|
linePositions.push(
|
|
corners[i].x, corners[i].y, corners[i].z,
|
|
corners[(i + 1) % 4].x, corners[(i + 1) % 4].y, corners[(i + 1) % 4].z
|
|
);
|
|
}
|
|
|
|
// Top face (horizontal lines)
|
|
for (let i = 4; i < 8; i++) {
|
|
linePositions.push(
|
|
corners[i].x, corners[i].y, corners[i].z,
|
|
corners[4 + ((i - 3) % 4)].x, corners[4 + ((i - 3) % 4)].y, corners[4 + ((i - 3) % 4)].z
|
|
);
|
|
}
|
|
|
|
// Vertical edges
|
|
for (let i = 0; i < 4; i++) {
|
|
linePositions.push(
|
|
corners[i].x, corners[i].y, corners[i].z,
|
|
corners[i + 4].x, corners[i + 4].y, corners[i + 4].z
|
|
);
|
|
}
|
|
|
|
lineGeometry.setAttribute('position', new THREE.Float32BufferAttribute(linePositions, 3));
|
|
const line = new THREE.LineSegments(lineGeometry, this.edgeMaterial);
|
|
this.lines.push(line);
|
|
this.group.add(line);
|
|
}
|
|
}
|
|
|
|
getCorners(min, max) {
|
|
if (this.mode === '2d') {
|
|
// 2D mode - 4 corners in XY plane at z midline
|
|
const zMid = (min.z + max.z) / 2;
|
|
return [
|
|
new Vector3(min.x, min.y, zMid),
|
|
new Vector3(max.x, min.y, zMid),
|
|
new Vector3(max.x, max.y, zMid),
|
|
new Vector3(min.x, max.y, zMid)
|
|
];
|
|
} else {
|
|
// 3D mode - 8 corners of bounding box
|
|
return [
|
|
new Vector3(min.x, min.y, min.z),
|
|
new Vector3(max.x, min.y, min.z),
|
|
new Vector3(max.x, max.y, min.z),
|
|
new Vector3(min.x, max.y, min.z),
|
|
new Vector3(min.x, min.y, max.z),
|
|
new Vector3(max.x, min.y, max.z),
|
|
new Vector3(max.x, max.y, max.z),
|
|
new Vector3(min.x, max.y, max.z)
|
|
];
|
|
}
|
|
}
|
|
|
|
createHandle() {
|
|
// Create a small box for the handle
|
|
const geometry = new BoxGeometry(
|
|
this.handleSize,
|
|
this.handleSize,
|
|
this.handleSize
|
|
);
|
|
|
|
// Create the handle mesh
|
|
const handle = new Mesh(geometry, this.handleMaterial);
|
|
|
|
// Add wireframe edges
|
|
const edges = new EdgesGeometry(geometry);
|
|
const line = new LineSegments(edges, this.edgeMaterial);
|
|
handle.add(line);
|
|
|
|
return handle;
|
|
}
|
|
|
|
get object() {
|
|
return this.group;
|
|
}
|
|
|
|
activeHandles() {
|
|
if (!this.enabled) return [];
|
|
// Return an array of objects wrapping each handle
|
|
// Each object: { mesh, index, position }
|
|
return this.handles.map((mesh, index) => ({
|
|
mesh,
|
|
index,
|
|
position: mesh.position.clone(),
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Given a handle (from activeHandles) and a drag {delta, offset},
|
|
* compute the new center and size for the bounding box.
|
|
* Returns: { center: Vector3, size: Vector3 }
|
|
*/
|
|
computeDelta(handleObj, { delta, offset }) {
|
|
// Get current bounds
|
|
if (!this.bounds) return null;
|
|
const { min, max } = this.bounds;
|
|
const center = min.clone().add(max).multiplyScalar(0.5);
|
|
const size = max.clone().sub(min);
|
|
|
|
// Which handle is being dragged?
|
|
const handleIndex = handleObj.index;
|
|
// Get all corners
|
|
const corners = this.getCorners(min, max);
|
|
// Copy corners so we can mutate
|
|
const newCorners = corners.map(c => c.clone());
|
|
// Move the dragged corner by delta
|
|
newCorners[handleIndex].add(delta);
|
|
|
|
// Compute new min/max from moved corners
|
|
let newMin = newCorners[0].clone();
|
|
let newMax = newCorners[0].clone();
|
|
for (let c of newCorners) {
|
|
newMin.min(c);
|
|
newMax.max(c);
|
|
}
|
|
const newCenter = newMin.clone().add(newMax).multiplyScalar(0.5);
|
|
const newSize = newMax.clone().sub(newMin);
|
|
return { center: newCenter, size: newSize };
|
|
}
|
|
}
|
|
|
|
// Create singleton instance
|
|
const handles = new TransformTool();
|
|
|
|
export { handles };
|