













(function () {
  'use strict';

  var RAD2DEG = 180 / Math.PI;

  var DEFAULTS = {
    NEAR_PX: 140,      
    HOLD_MS: 3000,     
    FADE_MS: 400       
  };

  var Support = {
    NEAR_PX: DEFAULTS.NEAR_PX,
    HOLD_MS: DEFAULTS.HOLD_MS,
    FADE_MS: DEFAULTS.FADE_MS
  };

  Support.install = function (proto) {

    
    proto._plane = function () {
      this.root.updateMatrixWorld(true);
      var origin = new THREE.Vector3().setFromMatrixPosition(this.root.matrixWorld);
      var normal = new THREE.Vector3(0, 0, 1)
        .applyQuaternion(this.root.getWorldQuaternion(new THREE.Quaternion()))
        .normalize();
      return {
        plane: new THREE.Plane().setFromNormalAndCoplanarPoint(normal, origin),
        origin: origin
      };
    };

    
    proto._azimuthAt = function (raycaster) {
      var p = this._plane();
      var hit = raycaster.ray.intersectPlane(p.plane, new THREE.Vector3());
      if (!hit) return null;
      var local = this.root.worldToLocal(hit.clone());
      if (local.length() < 1e-6) return null;
      return {
        deg: Math.atan2(local.y, local.x) * RAD2DEG,
        radius: Math.sqrt(local.x * local.x + local.y * local.y)
      };
    };

    
    proto._planeHitDistance = function (raycaster) {
      var p = this._plane();
      var hit = raycaster.ray.intersectPlane(p.plane, new THREE.Vector3());
      return hit ? raycaster.ray.origin.distanceTo(hit) : null;
    };

    
    proto.reveal = function (ms) {
      this._visibleUntil = performance.now() + (ms || Support.HOLD_MS);
    };

    
    proto._updateProximity = function (px, py, w, h) {
      this.root.updateMatrixWorld(true);
      var v = new THREE.Vector3().setFromMatrixPosition(this.root.matrixWorld);
      v.project(this.scene.camera);
      var sx = (v.x * 0.5 + 0.5) * w;
      var sy = (-v.y * 0.5 + 0.5) * h;
      var d = Math.sqrt((px - sx) * (px - sx) + (py - sy) * (py - sy));
      if (d <= Support.NEAR_PX) this.reveal(Support.HOLD_MS);
    };

    
    proto._stepFade = function (t) {
      var target = (t < this._visibleUntil) ? 1 : 0;
      if (this._opacity !== target) {
        var step = 16 / Support.FADE_MS;
        this._opacity += (target > this._opacity) ? step * 3 : -step;  
        this._opacity = Math.max(0, Math.min(1, this._opacity));
      }
      this.root.visible = this._opacity > 0.01;
      if (!this.root.visible) return false;
      var k = this._opacity;
      this._materials.forEach(function (m) { m.opacity = m.__baseOpacity * k; });
      return true;
    };

    



    proto._registerClaim = function () {
      var self = this;
      this._pointerOwned = true;   
      this._claim = function (raycaster) { return self._hitDistance(raycaster); };
      if (window.Gizmo && Gizmo.Arbiter) {
        Gizmo.Arbiter.addClaimant(this._claim, function (owned) {
          self._pointerOwned = !!owned;
        });
      }
    };

    



    proto._claimOwned = function () { return this._pointerOwned !== false; };

    proto._unregisterClaim = function () {
      if (this._claim && window.Gizmo && Gizmo.Arbiter) {
        Gizmo.Arbiter.removeClaimant(this._claim);
      }
    };

    
    proto._disposeRoot = function () {
      this._unregisterClaim();
      if (this.root.parent) this.root.parent.remove(this.root);
      this.root.traverse(function (o) {
        if (o.geometry) o.geometry.dispose();
        if (o.material) {
          if (o.material.map) o.material.map.dispose();
          o.material.dispose();
        }
      });
    };
  };

  

  Support.makeLabel = function (text, sizeM) {
    var S = 64;
    var cv = document.createElement('canvas');
    cv.width = cv.height = S;
    var ctx = cv.getContext('2d');
    ctx.clearRect(0, 0, S, S);
    ctx.fillStyle = '#d7dade';
    ctx.font = 'bold 30px system-ui, "Segoe UI", sans-serif';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillText(text, S / 2, S / 2);
    var tex = new THREE.CanvasTexture(cv);
    var mat = new THREE.SpriteMaterial({ map: tex, transparent: true, opacity: 0.95 });
    mat.__baseOpacity = 0.95;
    var sp = new THREE.Sprite(mat);
    var s = sizeM || 0.022;
    sp.scale.set(s, s, 1);
    return sp;
  };

  
  Support.lineMaterial = function (color, opacity) {
    var m = new THREE.LineBasicMaterial({
      color: color, transparent: true, opacity: opacity
    });
    m.__baseOpacity = opacity;
    return m;
  };

  window.DialSupport = Support;
})();
