All files / adapters/three orientationIndicator.ts

100% Statements 58/58
100% Branches 0/0
100% Functions 0/0
100% Lines 58/58

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131        38x                                                 39x 39x 39x 39x 39x 39x 39x 39x 39x 39x 39x 39x 39x 39x   39x               39x 39x 39x 39x 39x         13x 13x   13x     39x 39x 39x         39x 39x 39x       39x 39x 39x 39x 39x 39x 39x 39x 117x   117x 78x 78x 78x 78x       39x 39x 39x         39x     13x               24x 72x 72x 216x   216x 144x 144x     72x 72x      
import * as THREE from 'three';
 
type ColorMaterial = THREE.Material & { color?: THREE.Color };
 
const axes = [
  // In the cube's right-handed local frame, +X/+Y/+Z point through R/U/F.
  {
    name: 'x',
    direction: new THREE.Vector3(1, 0, 0),
    labelOffset: new THREE.Vector3(0, 0.13, -0.1),
    color: 0xd9493f,
  },
  {
    name: 'y',
    direction: new THREE.Vector3(0, 1, 0),
    labelOffset: new THREE.Vector3(0.13, 0, -0.1),
    color: 0xf4f4f4,
  },
  {
    name: 'z',
    direction: new THREE.Vector3(0, 0, 1),
    labelOffset: new THREE.Vector3(-0.2, 0.14, 0),
    color: 0x4caf67,
  },
] as const;
 
export type OrientationIndicatorColors = Record<(typeof axes)[number]['name'], number>;
 
function label(axis: string, color: number, position: THREE.Vector3): THREE.Sprite | undefined {
  if (typeof document === 'undefined') return undefined;
  const canvas = document.createElement('canvas');
  canvas.width = 96;
  canvas.height = 96;
  const context = canvas.getContext('2d');
  if (!context) return undefined;
  context.strokeStyle = '#10192c';
  context.lineWidth = 8;
  context.fillStyle = '#ffffff';
  context.font = 'bold 54px Arial';
  context.textAlign = 'center';
  context.textBaseline = 'middle';
  context.strokeText(axis.toUpperCase(), 48, 50);
  context.fillText(axis.toUpperCase(), 48, 50);
 
  const sprite = new THREE.Sprite(
    new THREE.SpriteMaterial({
      map: new THREE.CanvasTexture(canvas),
      color,
      depthTest: false,
      depthWrite: false,
    }),
  );
  sprite.name = `orientation-label-${axis}`;
  sprite.position.copy(position);
  sprite.scale.setScalar(0.34);
  sprite.renderOrder = 1001;
  return sprite;
}
 
/** A compact, labelled X/Y/Z triad in cube-local coordinates. */
export function createOrientationIndicator(includeLabels = true): THREE.Group {
  const indicator = new THREE.Group();
  indicator.name = 'orientation-indicator';
 
  for (const axis of axes) {
    // LineBasicMaterial width is ignored by WebGL on most platforms. Model
    // each arrow with meshes so all three axes stay legible at any DPI.
    const arrow = new THREE.Group();
    arrow.name = `orientation-axis-${axis.name}`;
    const material = new THREE.MeshBasicMaterial({
      color: axis.color,
      depthTest: false,
      depthWrite: false,
    });
    const shaft = new THREE.Mesh(new THREE.CylinderGeometry(0.032, 0.032, 0.62, 10), material);
    const tip = new THREE.Mesh(new THREE.ConeGeometry(0.1, 0.2, 10), material);
    const rotation = new THREE.Quaternion().setFromUnitVectors(
      new THREE.Vector3(0, 1, 0),
      axis.direction,
    );
    shaft.quaternion.copy(rotation);
    tip.quaternion.copy(rotation);
    shaft.position.copy(axis.direction).multiplyScalar(0.31);
    tip.position.copy(axis.direction).multiplyScalar(0.69);
    shaft.renderOrder = 1000;
    tip.renderOrder = 1000;
    arrow.add(shaft, tip);
    arrow.traverse((object) => {
      const material = (object as THREE.Mesh | THREE.Line).material as
        ColorMaterial | ColorMaterial[];
      if (material) {
        const materials = Array.isArray(material) ? material : [material];
        materials.forEach((value) => {
          value.depthTest = false;
          value.depthWrite = false;
        });
      }
    });
    indicator.add(arrow);
    if (includeLabels) {
      const axisLabel = label(
        axis.name,
        axis.color,
        axis.direction.clone().multiplyScalar(0.98).add(axis.labelOffset),
      );
      if (axisLabel) indicator.add(axisLabel);
    }
  }
  return indicator;
}
 
/** Recolor logical X/Y/Z axes to match their current physical R/U/F facelets. */
export function setOrientationIndicatorColors(
  indicator: THREE.Group,
  colors: OrientationIndicatorColors,
): void {
  for (const axis of axes) {
    const color = colors[axis.name];
    indicator.getObjectByName(`orientation-axis-${axis.name}`)?.traverse((object) => {
      const material = (object as THREE.Mesh | THREE.Line).material as
        ColorMaterial | ColorMaterial[];
      if (!material) return;
      (Array.isArray(material) ? material : [material]).forEach((value) =>
        value.color?.setHex(color),
      );
    });
    const axisLabel = indicator.getObjectByName(`orientation-label-${axis.name}`);
    if (axisLabel instanceof THREE.Sprite) axisLabel.material.color.setHex(color);
  }
}