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        39x                                                 48x 48x 48x 48x 48x 48x 48x 48x 48x 48x 48x 48x 48x 48x   48x               48x 48x 48x 48x 48x         16x 16x   16x     48x 48x 48x         48x 48x 48x       48x 48x 48x 48x 48x 48x 48x 48x 144x   144x 96x 96x 96x 96x       48x 48x 48x         48x     16x               28x 84x 84x 252x   252x 168x 168x     84x 84x      
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);
  }
}