示例
基础模板
const canvas = document.querySelector("#canvas");
const engine = new Engine(canvas, true);
const scene = new Scene(engine);
// 添加灯光
// 添加一些其它逻辑
engine.runRenderLoop(() => {
scene.render();
});
window.addEventListener("resize", () => {
engine.resize();
});
- 添加摄像机
配置一个 ArcRotateCamera
,然后将视线固定到中心点,然后加上交互,就能围绕中心点拖动交互查看了。
private createCamera() {
const { centerPositions, cubeletSize } = this._options;
const camera = new ArcRotateCamera(
"camera1",
Tools.ToRadians(-75),
Tools.ToRadians(75),
cubeletSize * 10,
new Vector3(centerPositions[0], centerPositions[1], centerPositions[2]),
this._scene
);
camera.attachControl();
camera.setTarget(Vector3.Zero());
}
- 添加灯光
如果一个场景中没有灯光,则无法查看到效果。 在正方体的相对的两个角设置点光源,另外多设置一个直射光源加强光照。
private createLight() {
const { centerPositions, cubeletSize } = this._options;
// 各个面都创建光源
const lights = [
[1, 1, 1],
[-1, -1, -1],
];
const lightDistance = cubeletSize * 1.5 + 2;
lights.reverse().forEach((lt, index) => {
const light = new PointLight(
`light-${index}`,
new Vector3(
centerPositions[0] + lt[0] * lightDistance,
centerPositions[1] + lt[1] * lightDistance,
centerPositions[2] + lt[2] * lightDistance
),
this._scene
);
light.intensity = 1;
const dlight = new DirectionalLight(
"d-light",
new Vector3(lt[0], lt[1], lt[2]),
this._scene
);
dlight.intensity = 0.5;
});
}
创建模型
魔方其实就是一个包含 26 个正方体的集合,所以只需要按照一定方法创建 26 个正方体就行。 画了立方体后也需要确定每一个立方体的具体位置。 先确定一个中心,添加配置centerPositions: [0, 0, 0]
,表示魔方的中心就是 xyz 轴的中心。 让后使用-1,1,0
排列表示每个魔方相对中心点的位置,这个位置需要乘以魔方每个小块的大小cubeletSize
。
- 计算绘制位置
魔方需要绘制 26 个小方块,这些方块的配置位置可以使用1,-1,0
排列表示。
function permute(arr: number[], stack: number[], result: number[][]) {
if (stack.length === arr.length) {
result.push(stack.slice());
return;
}
for (let i = 0; i < arr.length; i++) {
stack.push(arr[i]);
permute(arr, stack, result);
stack.pop();
}
}
const cubelets: number[][] = [];
// 全排列,构成26块位置
permute([-1, 1, 0], [], cubelets);
cubelets.pop(); // 不要0 0 0的项
查看计算结果
[[-1,-1,-1],[-1,-1,1],[-1,-1,0],[-1,1,-1],[-1,1,1],[-1,1,0],[-1,0,-1],[-1,0,1],[-1,0,0],[1,-1,-1],[1,-1,1],[1,-1,0],[1,1,-1],[1,1,1],[1,1,0],[1,0,-1],[1,0,1],[1,0,0],[0,-1,-1],[0,-1,1],[0,-1,0],[0,1,-1],[0,1,1],[0,1,0],[0,0,-1],[0,0,1]]
- 然后根据计算的相对位置数据,绘制魔方方块。
设置六个面的颜色。
private getColors() {
return [
new Color4(1, 1, 1, 1), // 白色
new Color4(1, 1, 0, 1), // 黄色
new Color4(0, 0, 1, 1), // 蓝色
new Color4(0, 1, 0, 1), // 绿色
new Color4(1, 0.5, 0, 1), // 橙色
new Color4(1, 0, 0, 1), // 红色
];
}
绘制方块,每个方块的各个面分别都设置有颜色。
cubelets.forEach((pos) => {
const cubeletBox = MeshBuilder.CreateBox(
`cubelet-${this._id}-${pos[0]}-${pos[1]}-${pos[2]}`,
{
width: this._cubeletSize,
height: this._cubeletSize,
depth: this._cubeletSize,
faceColors: colors,
},
this._scene
);
cubeletBox.scaling = new Vector3(0.98, 0.98, 0.98);
cubeletBox.metadata = {
originPos: pos.slice(), // 纪录排列位置
currentPos: pos.slice(),
};
this.calcRealPosition(cubeletBox);
this._cubelets.push(cubeletBox);
});
根据参数计算方块实际位置。
private calcRealPosition(cubelet: Mesh) {
const currentPos = cubelet.metadata.currentPos.slice();
const x = currentPos[0] * this._cubeletSize + this._centerPosition.x;
const y = currentPos[1] * this._cubeletSize + this._centerPosition.y;
const z = currentPos[2] * this._cubeletSize + this._centerPosition.z;
cubelet.position = new Vector3(x, y, z);
}
旋转动画
旋转的时候需要同时旋转一个面的所有方块,为了方便计算,创建一个空的节点,将需要旋转的方块全部绑定到这个空的节点上,然后旋转这个节点,就能实现旋转。
public rotateCustomFace(
faceCubelets: Mesh[],
rotationQuaternion: Quaternion
) {
// 定义一个空节点,用于旋转
const axisNode = new TransformNode("axis", this._scene);
faceCubelets.forEach((item) => {
item.parent = axisNode;
});
const frameRate = 60;
// 定义绕世界Y轴旋转的动画
const rotationAnimation = new Animation(
"rotationAnimation",
"rotationQuaternion",
frameRate,
Animation.ANIMATIONTYPE_QUATERNION,
Animation.ANIMATIONLOOPMODE_CONSTANT
);
const keys = [
{ frame: 0, value: Quaternion.Identity() },
{ frame: frameRate, value: rotationQuaternion },
];
rotationAnimation.setKeys(keys);
axisNode.animations = [rotationAnimation];
return new Promise((resolve, reject) => {
if (!this._scene) {
reject(new Error("cannot find scene!"));
return;
}
try {
// 开始动画
this._scene.beginAnimation(axisNode, 0, frameRate, false, 1, () => {
faceCubelets.forEach((item) => {
// 解绑和重新计算模块位置
this.calcCurrentPos(item);
this.calcRealPosition(item);
item.parent = null;
item.rotationQuaternion = rotationQuaternion.multiply(
item.rotationQuaternion ?? Quaternion.Identity()
);
});
axisNode.dispose();
resolve(true);
});
} catch (error) {
reject(error);
}
});
}
旋转后因为会将空的节点删除掉,所以旋转后的节点为了保持位置还需重新计算。
faceCubelets.forEach((item) => {
// 解绑和重新计算模块位置
this.calcCurrentPos(item);
this.calcRealPosition(item);
item.parent = null;
item.rotationQuaternion = rotationQuaternion.multiply(
item.rotationQuaternion ?? Quaternion.Identity()
);
});
axisNode.dispose();
拖动交互
为了实现拖动旋转的功能,需要全局监听拖动事件,纪录下拖动方块的法线和旋转方向。 在鼠标点击的时候纪录点击的方块坐标和法线等信息,移动的时候纪录下在相同方块移动的最后位置,最后根据坐标和法线计算旋转角度,并旋转。
private attachDrag() {
let pickedMeshNormal: Vector3 | undefined;
let pickedMesh: Mesh | undefined;
let pickedStartPoint: Vector3 | undefined;
let pickedEndPoint: Vector3 | undefined;
let moving = false; // 动画正在运行
this._scene?.onPointerObservable.add((pointerInfo) => {
if (moving) return;
if (pointerInfo.type === PointerEventTypes.POINTERUP) {
this._scene?.activeCamera?.attachControl();
if (
!pickedMesh ||
!pickedStartPoint ||
!pickedEndPoint ||
!pickedMeshNormal
) {
pickedMeshNormal = undefined;
pickedMesh = undefined;
pickedStartPoint = undefined;
pickedEndPoint = undefined;
return;
}
const moveVecotor = pickedEndPoint.subtract(pickedStartPoint);
const moveDirection = this.moveVectorToAxis(moveVecotor);
// 需要旋转的cubelets
const cubelets = this.getFaceCubeletsByNormalAndDirect(
pickedMesh,
pickedMeshNormal,
moveDirection
);
const rotationQueration = this.getRotationQueration(
pickedMeshNormal,
moveDirection
);
moving = true;
this.rotateCustomFace(cubelets, rotationQueration).finally(
() => (moving = false)
);
pickedMeshNormal = undefined;
pickedMesh = undefined;
pickedStartPoint = undefined;
pickedEndPoint = undefined;
}
if (pointerInfo.type === PointerEventTypes.POINTERMOVE) {
if (pointerInfo.pickInfo) {
const pickResult = this._scene?.pickWithRay(
pointerInfo.pickInfo.ray!,
(m) => {
return m.metadata?.originPos;
}
);
if (
pickResult?.pickedPoint &&
pickResult.pickedMesh === pickedMesh &&
pickedMeshNormal
) {
const normal = pickResult.getNormal(true);
if (normal) {
const realNormal = new Vector3(
Math.round(normal.x),
Math.round(normal.y),
Math.round(normal.z)
).normalize();
if (realNormal.equals(pickedMeshNormal)) {
// 更新end信息
pickedEndPoint = pickResult.pickedPoint;
}
}
}
}
}
if (pointerInfo.type === PointerEventTypes.POINTERDOWN) {
if (
!pointerInfo.pickInfo?.hit ||
!pointerInfo.pickInfo?.pickedMesh?.metadata.originPos
)
return;
this._scene?.activeCamera?.detachControl();
// 使用场景的pick方法来获取被点击的对象
const normal = pointerInfo.pickInfo.getNormal(true);
// 说明不是魔方的方块
if (!normal) return;
if (!pointerInfo.pickInfo.pickedPoint) return;
const realNormal = new Vector3(
Math.round(normal.x),
Math.round(normal.y),
Math.round(normal.z)
).normalize();
pickedMeshNormal = realNormal;
pickedMesh = pointerInfo.pickInfo.pickedMesh as Mesh;
pickedStartPoint = pointerInfo.pickInfo.pickedPoint;
}
});
}
根据法线和移动方向还有点击的方块信息,能计算出最终需要旋转的所有方块。 有法线和移动方向,能很方便的通过叉积计算旋转轴。 然后根据之间纪录的在节点 metadata 中的 currentPos 信息,就能过滤出所有需要旋转的方块。
private getFaceCubeletsByNormalAndDirect(
pointedMesh: Mesh,
normal: Vector3,
direct: Vector3
) {
// 计算两个向量的叉积(得到旋转轴)
const rotationAxis = normal.cross(direct).normalize();
const axis = rotationAxis.x !== 0 ? "x" : rotationAxis.y !== 0 ? "y" : "z";
return this._cubelets.filter((item) => {
const currentPos: number[] = item.metadata.currentPos;
const pointCurrentPos: number[] = pointedMesh.metadata.currentPos;
if (axis === "x") {
return currentPos[0] === pointCurrentPos[0];
} else if (axis === "y") {
return currentPos[1] === pointCurrentPos[1];
} else if (axis === "z") {
return currentPos[2] === pointCurrentPos[2];
}
return false;
});
}
根据法线和鼠标移动方向也能很好的计算出旋转方向。
// 根据法线和移动方向计算旋转量
private getRotationQueration(normal: Vector3, direct: Vector3) {
// 计算两个向量的点积
const dotProduct = normal.dot(direct);
// 计算两个向量的叉积(得到旋转轴)
const rotationAxis = normal.cross(direct);
// 计算夹角(弧度制)
let angle = Math.acos(dotProduct / (normal.length() * direct.length()));
// 如果向量A和向量B方向相反,需要特殊处理
if (dotProduct < 0) {
rotationAxis.scaleInPlace(-1);
angle = Math.PI - angle;
}
// 构造旋转四元数
const rotationQuaternion = Quaternion.RotationAxis(rotationAxis, angle);
return rotationQuaternion;
}
旋转角度使用Quaternion
四元数是为了更精准的表示旋转方向,能够明确的表示最后的旋转状态。
在旋转魔方的时候也需要关闭摄像机的旋转交互,不然摄像机会跟着旋转。
this._scene?.activeCamera?.detachControl();
旋转完成再次开启摄像机交互。
this._scene?.activeCamera?.attachControl();
源码
import { onMounted } from "vue";
import { renderCube } from "@/examples/html/3D/魔方/cubeBoxSpace.ts";
onMounted(() => {
renderCube("#canvas1");
});
import {
ArcRotateCamera,
AxesViewer,
DirectionalLight,
Engine,
PointLight,
Scene,
Tools,
Vector3,
} from "@babylonjs/core";
import { CubeBox, CubeFace } from "./cubeBox";
interface CubeBoxSpaceOptions {
cubeletSize: number; // 每一小块的大小
centerPositions: [number, number, number]; // xyz
}
const defaultOptions: CubeBoxSpaceOptions = {
cubeletSize: 1,
centerPositions: [0, 0, 0],
};
class CubeBoxSpace {
private _canvas: HTMLCanvasElement | undefined;
private _engine: Engine | undefined;
private _scene: Scene | undefined;
private _options: CubeBoxSpaceOptions;
private _cube: CubeBox | undefined;
constructor(options?: CubeBoxSpaceOptions) {
this._options = {
...defaultOptions,
...options,
};
}
// #region light
private createLight() {
const { centerPositions, cubeletSize } = this._options;
// 各个面都创建光源
const lights = [
[1, 1, 1],
[-1, -1, -1],
];
const lightDistance = cubeletSize * 1.5 + 2;
lights.reverse().forEach((lt, index) => {
const light = new PointLight(
`light-${index}`,
new Vector3(
centerPositions[0] + lt[0] * lightDistance,
centerPositions[1] + lt[1] * lightDistance,
centerPositions[2] + lt[2] * lightDistance
),
this._scene
);
light.intensity = 1;
const dlight = new DirectionalLight(
"d-light",
new Vector3(lt[0], lt[1], lt[2]),
this._scene
);
dlight.intensity = 0.5;
});
}
// #endregion light
// #region camera
private createCamera() {
const { centerPositions, cubeletSize } = this._options;
const camera = new ArcRotateCamera(
"camera1",
Tools.ToRadians(-75),
Tools.ToRadians(75),
cubeletSize * 10,
new Vector3(centerPositions[0], centerPositions[1], centerPositions[2]),
this._scene
);
camera.attachControl();
camera.setTarget(Vector3.Zero());
}
// #endregion camera
private createScene(engine: Engine) {
const scene = new Scene(engine);
this._scene = scene;
}
public mount(canvasDom: HTMLCanvasElement | string) {
const { centerPositions, cubeletSize } = this._options;
if (!canvasDom) throw new Error("canvas is required");
if (typeof canvasDom === "string") {
const canvas = document.querySelector(canvasDom);
if (!canvas) throw new Error("canvas is not found");
this._canvas = canvas as HTMLCanvasElement;
} else {
this._canvas = canvasDom;
}
this._engine = new Engine(this._canvas, true);
this.createScene(this._engine);
this.createCamera();
this.createLight();
this._cube = new CubeBox(
cubeletSize,
new Vector3(centerPositions[0], centerPositions[1], centerPositions[2]),
this._scene
);
// 添加坐标轴
// X 轴指向正方向(+X)通常是红色。
// Y 轴指向正方向(+Y)通常是绿色。
// Z 轴指向正方向(+Z)通常是蓝色。
new AxesViewer(this._scene);
const scene = this._scene!;
this._engine.runRenderLoop(() => {
scene.render();
});
window.addEventListener("resize", () => {
this._engine?.resize();
});
setTimeout(async () => {
// await this._cube?.rotate(CubeFace.Up);
// await this._cube?.rotate(CubeFace.Down);
// await this._cube?.rotate(CubeFace.Right);
// await this._cube?.rotate(CubeFace.Left);
// await this._cube?.rotate(CubeFace.Front);
// await this._cube?.rotate(CubeFace.Back);
// await this._cube?.rotate(CubeFace.Up, false);
// await this._cube?.rotate(CubeFace.Down, false);
// await this._cube?.rotate(CubeFace.Right, false);
// await this._cube?.rotate(CubeFace.Left, false);
// await this._cube?.rotate(CubeFace.Front, false);
// await this._cube?.rotate(CubeFace.Back, false);
}, 1000);
}
}
export function renderCube(canvas: string) {
const cubeBoxSpace = new CubeBoxSpace();
cubeBoxSpace.mount(canvas);
}
import {
Animation,
Axis,
Color4,
EngineStore,
Mesh,
MeshBuilder,
Nullable,
PointerEventTypes,
Quaternion,
Scene,
Tools,
TransformNode,
Vector3,
} from "@babylonjs/core";
// 排列
// #region permute
function permute(arr: number[], stack: number[], result: number[][]) {
if (stack.length === arr.length) {
result.push(stack.slice());
return;
}
for (let i = 0; i < arr.length; i++) {
stack.push(arr[i]);
permute(arr, stack, result);
stack.pop();
}
}
// #endregion permute
export enum CubeFace {
Front = "F",
Back = "B",
Up = "U",
Down = "D",
Left = "L",
Right = "R",
}
const faceGetters = {
[CubeFace.Front]: (pos: number[]) => pos[2] === -1,
[CubeFace.Back]: (pos: number[]) => pos[2] === 1,
[CubeFace.Up]: (pos: number[]) => pos[1] === 1,
[CubeFace.Down]: (pos: number[]) => pos[1] === -1,
[CubeFace.Left]: (pos: number[]) => pos[0] === -1,
[CubeFace.Right]: (pos: number[]) => pos[0] === 1,
};
let id = 0;
export class CubeBox {
private _cubelets: Mesh[] = [];
private _cubeletSize;
private _centerPosition: Vector3;
private _scene: Nullable<Scene>;
private _id: number;
constructor(cubeletSize: number, centerPosition: Vector3, scene?: Scene) {
this._cubeletSize = cubeletSize;
this._centerPosition = centerPosition;
this._scene = scene ?? EngineStore.LastCreatedScene;
this._id = id++;
this.createCube();
this.attachDrag();
}
// 计算移动的方向
private moveVectorToAxis(moveVecotor: Vector3) {
// 计算向量在各个坐标轴上的分量
var xComponent = moveVecotor.x;
var yComponent = moveVecotor.y;
var zComponent = moveVecotor.z;
// 找出绝对值最大的分量
var absComponents = [
Math.abs(xComponent),
Math.abs(yComponent),
Math.abs(zComponent),
];
var maxIndex = absComponents.indexOf(Math.max(...absComponents));
// 构建与坐标轴平行的新向量
switch (maxIndex) {
case 0:
return new Vector3(Math.sign(xComponent), 0, 0);
case 1:
return new Vector3(0, Math.sign(yComponent), 0);
case 2:
default:
return new Vector3(0, 0, Math.sign(zComponent));
}
}
// #region getRotationQueration
// 根据法线和移动方向计算旋转量
private getRotationQueration(normal: Vector3, direct: Vector3) {
// 计算两个向量的点积
const dotProduct = normal.dot(direct);
// 计算两个向量的叉积(得到旋转轴)
const rotationAxis = normal.cross(direct);
// 计算夹角(弧度制)
let angle = Math.acos(dotProduct / (normal.length() * direct.length()));
// 如果向量A和向量B方向相反,需要特殊处理
if (dotProduct < 0) {
rotationAxis.scaleInPlace(-1);
angle = Math.PI - angle;
}
// 构造旋转四元数
const rotationQuaternion = Quaternion.RotationAxis(rotationAxis, angle);
return rotationQuaternion;
}
// #endregion getRotationQueration
// 根据法线和旋转方向,获取一面的方块节点
// #region getFaceCubeletsByNormalAndDirect
private getFaceCubeletsByNormalAndDirect(
pointedMesh: Mesh,
normal: Vector3,
direct: Vector3
) {
// 计算两个向量的叉积(得到旋转轴)
const rotationAxis = normal.cross(direct).normalize();
const axis = rotationAxis.x !== 0 ? "x" : rotationAxis.y !== 0 ? "y" : "z";
return this._cubelets.filter((item) => {
const currentPos: number[] = item.metadata.currentPos;
const pointCurrentPos: number[] = pointedMesh.metadata.currentPos;
if (axis === "x") {
return currentPos[0] === pointCurrentPos[0];
} else if (axis === "y") {
return currentPos[1] === pointCurrentPos[1];
} else if (axis === "z") {
return currentPos[2] === pointCurrentPos[2];
}
return false;
});
}
// #endregion getFaceCubeletsByNormalAndDirect
// 拖动旋转的时候调用
// #region rotateCustomFace
public rotateCustomFace(
faceCubelets: Mesh[],
rotationQuaternion: Quaternion
) {
// 定义一个空节点,用于旋转
const axisNode = new TransformNode("axis", this._scene);
faceCubelets.forEach((item) => {
item.parent = axisNode;
});
const frameRate = 60;
// 定义绕世界Y轴旋转的动画
const rotationAnimation = new Animation(
"rotationAnimation",
"rotationQuaternion",
frameRate,
Animation.ANIMATIONTYPE_QUATERNION,
Animation.ANIMATIONLOOPMODE_CONSTANT
);
const keys = [
{ frame: 0, value: Quaternion.Identity() },
{ frame: frameRate, value: rotationQuaternion },
];
rotationAnimation.setKeys(keys);
axisNode.animations = [rotationAnimation];
return new Promise((resolve, reject) => {
if (!this._scene) {
reject(new Error("cannot find scene!"));
return;
}
try {
// 开始动画
this._scene.beginAnimation(axisNode, 0, frameRate, false, 1, () => {
// #region recalc
faceCubelets.forEach((item) => {
// 解绑和重新计算模块位置
this.calcCurrentPos(item);
this.calcRealPosition(item);
item.parent = null;
item.rotationQuaternion = rotationQuaternion.multiply(
item.rotationQuaternion ?? Quaternion.Identity()
);
});
axisNode.dispose();
// #endregion recalc
resolve(true);
});
} catch (error) {
reject(error);
}
});
}
// #endregion rotateCustomFace
// 开启拖动魔方
// #region attachDrag
private attachDrag() {
let pickedMeshNormal: Vector3 | undefined;
let pickedMesh: Mesh | undefined;
let pickedStartPoint: Vector3 | undefined;
let pickedEndPoint: Vector3 | undefined;
let moving = false; // 动画正在运行
this._scene?.onPointerObservable.add((pointerInfo) => {
if (moving) return;
if (pointerInfo.type === PointerEventTypes.POINTERUP) {
this._scene?.activeCamera?.attachControl();
if (
!pickedMesh ||
!pickedStartPoint ||
!pickedEndPoint ||
!pickedMeshNormal
) {
pickedMeshNormal = undefined;
pickedMesh = undefined;
pickedStartPoint = undefined;
pickedEndPoint = undefined;
return;
}
const moveVecotor = pickedEndPoint.subtract(pickedStartPoint);
const moveDirection = this.moveVectorToAxis(moveVecotor);
// 需要旋转的cubelets
const cubelets = this.getFaceCubeletsByNormalAndDirect(
pickedMesh,
pickedMeshNormal,
moveDirection
);
const rotationQueration = this.getRotationQueration(
pickedMeshNormal,
moveDirection
);
moving = true;
this.rotateCustomFace(cubelets, rotationQueration).finally(
() => (moving = false)
);
pickedMeshNormal = undefined;
pickedMesh = undefined;
pickedStartPoint = undefined;
pickedEndPoint = undefined;
}
if (pointerInfo.type === PointerEventTypes.POINTERMOVE) {
if (pointerInfo.pickInfo) {
const pickResult = this._scene?.pickWithRay(
pointerInfo.pickInfo.ray!,
(m) => {
return m.metadata?.originPos;
}
);
if (
pickResult?.pickedPoint &&
pickResult.pickedMesh === pickedMesh &&
pickedMeshNormal
) {
const normal = pickResult.getNormal(true);
if (normal) {
const realNormal = new Vector3(
Math.round(normal.x),
Math.round(normal.y),
Math.round(normal.z)
).normalize();
if (realNormal.equals(pickedMeshNormal)) {
// 更新end信息
pickedEndPoint = pickResult.pickedPoint;
}
}
}
}
}
if (pointerInfo.type === PointerEventTypes.POINTERDOWN) {
if (
!pointerInfo.pickInfo?.hit ||
!pointerInfo.pickInfo?.pickedMesh?.metadata.originPos
)
return;
this._scene?.activeCamera?.detachControl();
// 使用场景的pick方法来获取被点击的对象
const normal = pointerInfo.pickInfo.getNormal(true);
// 说明不是魔方的方块
if (!normal) return;
if (!pointerInfo.pickInfo.pickedPoint) return;
const realNormal = new Vector3(
Math.round(normal.x),
Math.round(normal.y),
Math.round(normal.z)
).normalize();
pickedMeshNormal = realNormal;
pickedMesh = pointerInfo.pickInfo.pickedMesh as Mesh;
pickedStartPoint = pointerInfo.pickInfo.pickedPoint;
}
});
}
// #endregion attachDrag
// 获取一个面的方块
private getFaceCublets(faceDirection: CubeFace) {
return this._cubelets.filter((item) =>
faceGetters[faceDirection](item.metadata.currentPos)
);
}
// 可以通过CubeFace类型字段控制旋转哪一个面
public rotate(faceDirection: CubeFace, clockwise = true) {
const faceCublets = this.getFaceCublets(faceDirection);
const axis =
faceDirection === CubeFace.Up || faceDirection === CubeFace.Down
? Axis.Y
: faceDirection === CubeFace.Left || faceDirection === CubeFace.Right
? Axis.X
: Axis.Z;
const angle = !(
Number(clockwise) ^
Number(
faceDirection === CubeFace.Up ||
faceDirection === CubeFace.Right ||
faceDirection === CubeFace.Back
)
)
? Tools.ToRadians(90)
: Tools.ToRadians(-90);
const rotationQuaternion = Quaternion.RotationAxis(axis, angle);
return this.rotateCustomFace(faceCublets, rotationQuaternion);
}
// #region color
private getColors() {
return [
new Color4(1, 1, 1, 1), // 白色
new Color4(1, 1, 0, 1), // 黄色
new Color4(0, 0, 1, 1), // 蓝色
new Color4(0, 1, 0, 1), // 绿色
new Color4(1, 0.5, 0, 1), // 橙色
new Color4(1, 0, 0, 1), // 红色
];
}
// #endregion color
private createCube() {
// #region permute-use
const cubelets: number[][] = [];
// 全排列,构成26块位置
permute([-1, 1, 0], [], cubelets);
cubelets.pop(); // 不要0 0 0的项
// #endregion permute-use
this._cubelets = [];
const colors = this.getColors();
// #region createCubelets
cubelets.forEach((pos) => {
const cubeletBox = MeshBuilder.CreateBox(
`cubelet-${this._id}-${pos[0]}-${pos[1]}-${pos[2]}`,
{
width: this._cubeletSize,
height: this._cubeletSize,
depth: this._cubeletSize,
faceColors: colors,
},
this._scene
);
cubeletBox.scaling = new Vector3(0.98, 0.98, 0.98);
cubeletBox.metadata = {
originPos: pos.slice(), // 纪录排列位置
currentPos: pos.slice(),
};
this.calcRealPosition(cubeletBox);
this._cubelets.push(cubeletBox);
});
// #endregion createCubelets
}
// #region calcRealPosition
private calcRealPosition(cubelet: Mesh) {
const currentPos = cubelet.metadata.currentPos.slice();
const x = currentPos[0] * this._cubeletSize + this._centerPosition.x;
const y = currentPos[1] * this._cubeletSize + this._centerPosition.y;
const z = currentPos[2] * this._cubeletSize + this._centerPosition.z;
cubelet.position = new Vector3(x, y, z);
}
// #endregion calcRealPosition
private calcCurrentPos(cubelet: Mesh) {
const newPos = [
Math.round(
(cubelet.getAbsolutePosition().x - this._centerPosition.x) /
this._cubeletSize
),
Math.round(
(cubelet.getAbsolutePosition().y - this._centerPosition.y) /
this._cubeletSize
),
Math.round(
(cubelet.getAbsolutePosition().z - this._centerPosition.z) /
this._cubeletSize
),
];
cubelet.metadata.currentPos = newPos;
}
}