Files
Mujoco_WASM/web_platform/src/viewer/OrientationGizmo.ts
T
chenlin 60d3a6d68c
web-platform-ci / TypeScript, lint, unit, build (push) Has been cancelled
web-platform-ci / Playwright E2E (push) Has been cancelled
chore(web-platform): release V0.6.1 工程质量优化
2026-08-28 15:38:10 +08:00

115 lines
4.3 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 + 0.5));
const opacity = String(0.65 + 0.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();
}
}