Files
Mujoco_WASM/wasm/web_platform/src/viewer/OrientationGizmo.ts
T

46 lines
3.7 KiB
TypeScript

import * as THREE from 'three';
const SVG_NS='http://www.w3.org/2000/svg';
interface AxisElements {axis:THREE.Vector3;line:SVGLineElement;negative:SVGCircleElement;positive:SVGCircleElement;label:SVGTextElement;}
function svgElement<K extends keyof SVGElementTagNameMap>(name:K):SVGElementTagNameMap[K]{return document.createElementNS(SVG_NS,name);}
/** 固定在视口右下角、随相机旋转的 XYZ 方向示意器。 */
export class OrientationGizmo {
readonly element:SVGSVGElement;
private readonly axes:AxisElements[]=[];
private readonly inverseQuaternion=new THREE.Quaternion();
private readonly projected=new THREE.Vector3();
private readonly center:SVGCircleElement;
constructor(host:HTMLElement){
this.element=svgElement('svg');this.element.setAttribute('viewBox','0 0 100 100');this.element.setAttribute('role','img');this.element.setAttribute('aria-label','XYZ 方向指示器');
Object.assign(this.element.style,{position:'absolute',right:'14px',bottom:'14px',width:'92px',height:'92px',pointerEvents:'none',border:'1px solid rgba(100,116,139,.45)',borderRadius:'10px',background:'rgba(15,23,42,.72)',backdropFilter:'blur(3px)'});
const definitions:[string,number,THREE.Vector3][]=[['X',0xef4444,new THREE.Vector3(1,0,0)],['Y',0x22c55e,new THREE.Vector3(0,1,0)],['Z',0x3b82f6,new THREE.Vector3(0,0,1)]];
for(const [name,colorValue,axis] of definitions){
const color=`#${colorValue.toString(16).padStart(6,'0')}`;const group=svgElement('g');const line=svgElement('line');line.setAttribute('stroke',color);line.setAttribute('stroke-width','2.5');line.setAttribute('stroke-linecap','round');
const negative=svgElement('circle');negative.setAttribute('r','4');negative.setAttribute('fill',color);negative.setAttribute('fill-opacity','.7');
const positive=svgElement('circle');positive.setAttribute('r','8');positive.setAttribute('fill',color);
const label=svgElement('text');label.textContent=name;label.setAttribute('fill','#0f172a');label.setAttribute('font-size','10');label.setAttribute('font-weight','700');label.setAttribute('text-anchor','middle');label.setAttribute('dominant-baseline','central');
group.append(line,negative,positive,label);this.element.append(group);this.axes.push({axis,line,negative,positive,label});
}
this.center=svgElement('circle');this.center.setAttribute('cx','50');this.center.setAttribute('cy','50');this.center.setAttribute('r','3.5');this.center.setAttribute('fill','#cbd5e1');this.element.append(this.center);host.append(this.element);
}
update(camera:THREE.Camera):void {
this.inverseQuaternion.copy(camera.quaternion).invert();
for(const item of this.axes){
this.projected.copy(item.axis).applyQuaternion(this.inverseQuaternion);const x=50+this.projected.x*30,y=50-this.projected.y*30;const nx=50-this.projected.x*30,ny=50+this.projected.y*30;
item.line.setAttribute('x1',String(nx));item.line.setAttribute('y1',String(ny));item.line.setAttribute('x2',String(x));item.line.setAttribute('y2',String(y));
item.negative.setAttribute('cx',String(nx));item.negative.setAttribute('cy',String(ny));item.positive.setAttribute('cx',String(x));item.positive.setAttribute('cy',String(y));item.label.setAttribute('x',String(x));item.label.setAttribute('y',String(y+.5));
const opacity=String(.65+.35*Math.max(0,this.projected.z));item.positive.setAttribute('fill-opacity',opacity);item.label.setAttribute('fill-opacity',opacity);
}
}
setTheme(theme:'light'|'dark'):void {
const light=theme==='light';this.element.style.background=light?'rgba(248,250,252,.82)':'rgba(15,23,42,.72)';this.element.style.borderColor=light?'rgba(100,116,139,.35)':'rgba(100,116,139,.45)';this.center.setAttribute('fill',light?'#475569':'#cbd5e1');
}
dispose():void {this.element.remove();}
}