分享图
动画工坊
引擎就绪
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>TRIZ 理想最终解 (IFR) - 主动悬挂与行星越障</title>
    <style>
        :root {
            --bg-color: #0b0f19;
            --grid-color: rgba(30, 41, 59, 0.6);
            --terrain-color: #1e293b;
            --terrain-stroke: #334155;
            
            /* 机械核心色 */
            --mech-base: #475569;
            --mech-accent: #94a3b8;
            --mech-dark: #1e293b;
            
            /* IFR 亮点色 (主动悬挂 & 稳定载荷) */
            --ifr-glow: #0ea5e9;
            --ifr-extend: #38bdf8;
            --ifr-compress: #f59e0b;
            --payload-base: #fbbf24;
            --payload-glow: rgba(251, 191, 36, 0.2);
            --stable-green: #10b981;
            
            /* 字体 */
            --font-mono: 'JetBrains Mono', 'SF Mono', Consolas, 'Courier New', monospace;
        }

        body {
            margin: 0;
            padding: 0;
            background-color: var(--bg-color);
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            overflow: hidden;
            font-family: var(--font-mono);
            color: #f8fafc;
        }

        #canvas-container {
            position: relative;
            width: 100%;
            max-width: 1600px;
            aspect-ratio: 16 / 9;
            box-shadow: 0 0 100px rgba(0, 0, 0, 0.8) inset;
            border: 1px solid rgba(255, 255, 255, 0.05);
            background: radial-gradient(circle at center, #111827 0%, #030712 100%);
        }

        svg {
            width: 100%;
            height: 100%;
            display: block;
        }

        /* 动画与特效类 */
        .glow-cyan { filter: drop-shadow(0 0 8px var(--ifr-glow)); }
        .glow-green { filter: drop-shadow(0 0 8px var(--stable-green)); }
        .glow-amber { filter: drop-shadow(0 0 12px var(--payload-base)); }
        
        .laser-beam {
            stroke: var(--stable-green);
            stroke-width: 2;
            stroke-dasharray: 10 5;
            animation: dash 1s linear infinite;
            opacity: 0.8;
        }

        .data-text {
            font-size: 14px;
            fill: #94a3b8;
            letter-spacing: 1px;
        }

        .data-value {
            font-weight: bold;
            fill: #f8fafc;
        }

        .data-highlight {
            fill: var(--stable-green);
            font-weight: bold;
            filter: drop-shadow(0 0 4px rgba(16, 185, 129, 0.5));
        }

        @keyframes dash {
            to { stroke-dashoffset: -15; }
        }

        /* 控制面板覆盖层 */
        #hud {
            position: absolute;
            top: 30px;
            left: 30px;
            pointer-events: none;
        }

        .hud-panel {
            background: rgba(15, 23, 42, 0.7);
            border: 1px solid rgba(56, 189, 248, 0.3);
            border-left: 4px solid var(--ifr-glow);
            padding: 20px;
            border-radius: 4px;
            backdrop-filter: blur(10px);
        }

        .hud-title {
            font-size: 12px;
            text-transform: uppercase;
            color: var(--ifr-glow);
            margin: 0 0 15px 0;
            letter-spacing: 2px;
        }

        .hud-row {
            display: flex;
            justify-content: space-between;
            margin-bottom: 8px;
            font-size: 14px;
            width: 250px;
        }

        .hud-label { color: #64748b; }
        .hud-val { color: #f8fafc; font-weight: bold; font-variant-numeric: tabular-nums; }
        
        .status-badge {
            display: inline-block;
            padding: 2px 8px;
            background: rgba(16, 185, 129, 0.2);
            color: var(--stable-green);
            border-radius: 12px;
            font-size: 12px;
            border: 1px solid rgba(16, 185, 129, 0.5);
            animation: pulse-green 2s infinite;
        }

        @keyframes pulse-green {
            0% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.4); }
            70% { box-shadow: 0 0 0 6px rgba(16, 185, 129, 0); }
            100% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); }
        }
        
        #controls {
            position: absolute;
            bottom: 30px;
            left: 50%;
            transform: translateX(-50%);
            background: rgba(15, 23, 42, 0.8);
            padding: 10px 20px;
            border-radius: 30px;
            border: 1px solid rgba(255,255,255,0.1);
            display: flex;
            gap: 20px;
            backdrop-filter: blur(5px);
        }
        
        .control-group {
            display: flex;
            align-items: center;
            gap: 10px;
            font-size: 12px;
            color: #94a3b8;
        }
        
        input[type=range] {
            accent-color: var(--ifr-glow);
            width: 100px;
        }
    </style>
</head>
<body>

<div id="canvas-container">
    <div id="hud">
        <div class="hud-panel">
            <h2 class="hud-title">System Telemetry // IFR</h2>
            <div class="hud-row">
                <span class="hud-label">Chassis Pitch:</span>
                <span class="hud-val" id="val-pitch">0.0°</span>
            </div>
            <div class="hud-row">
                <span class="hud-label">Payload Posture:</span>
                <span class="hud-val" style="color: var(--stable-green);">0.0°</span>
            </div>
            <div class="hud-row">
                <span class="hud-label">Actuator L Stroke:</span>
                <span class="hud-val" id="val-actL">0.0 mm</span>
            </div>
            <div class="hud-row">
                <span class="hud-label">Actuator R Stroke:</span>
                <span class="hud-val" id="val-actR">0.0 mm</span>
            </div>
            <div class="hud-row" style="margin-top: 15px;">
                <span class="hud-label">Gyro Response:</span>
                <span class="hud-val" style="color: var(--ifr-glow);">4.2 ms</span>
            </div>
            <div style="margin-top: 15px;">
                <span class="status-badge">IFR TARGET LOCKED</span>
            </div>
        </div>
    </div>
    
    <div id="controls">
        <div class="control-group">
            <label>Environment Speed</label>
            <input type="range" id="speedCtrl" min="0" max="10" value="4">
        </div>
    </div>

    <svg id="scene" viewBox="0 0 1600 900" preserveAspectRatio="xMidYMid meet">
        <!-- 定义背景网格与滤镜 -->
        <defs>
            <pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
                <rect width="40" height="40" fill="none" stroke="var(--grid-color)" stroke-width="0.5" />
                <circle cx="40" cy="40" r="1" fill="#334155" />
            </pattern>
            <pattern id="hatch" width="10" height="10" patternTransform="rotate(45)" patternUnits="userSpaceOnUse">
                <line x1="0" y1="0" x2="0" y2="10" stroke="#fbbf24" stroke-width="2" opacity="0.3"/>
            </pattern>
            <!-- 渐变遮罩边缘 -->
            <linearGradient id="fadeEdge" x1="0" y1="0" x2="1" y2="0">
                <stop offset="0%" stop-color="rgba(11, 15, 25, 1)"/>
                <stop offset="10%" stop-color="rgba(11, 15, 25, 0)"/>
                <stop offset="90%" stop-color="rgba(11, 15, 25, 0)"/>
                <stop offset="100%" stop-color="rgba(11, 15, 25, 1)"/>
            </linearGradient>
        </defs>

        <!-- 背景 -->
        <rect width="1600" height="900" fill="url(#grid)" />
        
        <!-- 环境基准线 -->
        <line x1="0" y1="650" x2="1600" y2="650" stroke="#1e293b" stroke-width="2" stroke-dasharray="10 10"/>
        <text x="50" y="640" class="data-text" fill="#475569">BASE ELEVATION ZERO</text>

        <!-- 动态地形 (跑步机原理) -->
        <g id="terrain-group">
            <path id="terrain-path" d="" fill="var(--terrain-color)" stroke="var(--terrain-stroke)" stroke-width="6" stroke-linejoin="round"/>
            <path id="terrain-path-glow" d="" fill="none" stroke="var(--ifr-glow)" stroke-width="2" opacity="0.3" filter="blur(4px)"/>
        </g>
        
        <!-- 边缘遮罩以隐藏地形生成和消失 -->
        <rect width="1600" height="900" fill="url(#fadeEdge)" pointer-events="none" />

        <!-- 机器人总成 (固定在屏幕中央) -->
        <g id="robot" transform="translate(800, 0)">
            
            <!-- ====== 底层:产生问题的部分 (深色系) ====== -->
            
            <!-- 后轮总成 -->
            <g id="rear-wheel-group">
                <!-- 悬挂臂 -->
                <line id="rear-arm" x1="0" y1="0" x2="-180" y2="0" stroke="var(--mech-base)" stroke-width="12" stroke-linecap="round" />
                <circle id="rear-wheel" cx="-180" cy="0" r="45" fill="var(--mech-dark)" stroke="var(--mech-accent)" stroke-width="6" />
                <circle id="rear-wheel-hub" cx="-180" cy="0" r="15" fill="var(--mech-base)" />
                <line id="rear-wheel-spoke" x1="-180" y1="0" x2="-180" y2="-45" stroke="var(--mech-accent)" stroke-width="4" />
            </g>

            <!-- 前轮:行星轮越障机构 -->
            <g id="front-wheel-group">
                <!-- 悬挂臂 -->
                <line id="front-arm" x1="0" y1="0" x2="180" y2="0" stroke="var(--mech-base)" stroke-width="12" stroke-linecap="round" />
                
                <!-- 行星架中心 -->
                <g id="planetary-hub" transform="translate(180, 0)">
                    <circle cx="0" cy="0" r="60" fill="none" stroke="#334155" stroke-width="2" stroke-dasharray="8 4"/>
                    <!-- 三臂支架 -->
                    <polygon points="0,-45 38.97,22.5 -38.97,22.5" fill="var(--mech-base)" stroke="var(--mech-accent)" stroke-width="4" stroke-linejoin="round"/>
                    <circle cx="0" cy="0" r="15" fill="var(--mech-dark)" stroke="var(--mech-accent)" stroke-width="3"/>
                    
                    <!-- 三个子轮 -->
                    <g transform="translate(0, -45)">
                        <circle cx="0" cy="0" r="25" fill="var(--mech-dark)" stroke="var(--mech-accent)" stroke-width="4"/>
                        <line x1="0" y1="-25" x2="0" y2="25" stroke="var(--mech-accent)" stroke-width="2"/>
                        <line x1="-25" y1="0" x2="25" y2="0" stroke="var(--mech-accent)" stroke-width="2"/>
                    </g>
                    <g transform="translate(38.97, 22.5)">
                        <circle cx="0" cy="0" r="25" fill="var(--mech-dark)" stroke="var(--mech-accent)" stroke-width="4"/>
                        <line x1="0" y1="-25" x2="0" y2="25" stroke="var(--mech-accent)" stroke-width="2"/>
                        <line x1="-25" y1="0" x2="25" y2="0" stroke="var(--mech-accent)" stroke-width="2"/>
                    </g>
                    <g transform="translate(-38.97, 22.5)">
                        <circle cx="0" cy="0" r="25" fill="var(--mech-dark)" stroke="var(--mech-accent)" stroke-width="4"/>
                        <line x1="0" y1="-25" x2="0" y2="25" stroke="var(--mech-accent)" stroke-width="2"/>
                        <line x1="-25" y1="0" x2="25" y2="0" stroke="var(--mech-accent)" stroke-width="2"/>
                    </g>
                </g>
            </g>

            <!-- 刚性底盘主梁 -->
            <g id="chassis-beam">
                <rect x="-220" y="-15" width="440" height="30" rx="10" fill="var(--mech-base)" stroke="var(--mech-accent)" stroke-width="3" />
                <circle cx="-180" cy="0" r="8" fill="var(--mech-dark)" />
                <circle cx="180" cy="0" r="8" fill="var(--mech-dark)" />
                <text x="0" y="5" font-size="12" fill="#94a3b8" text-anchor="middle" dominant-baseline="middle" font-weight="bold">RIGID CHASSIS BASE</text>
            </g>


            <!-- ====== IFR 核心创新:主动悬挂 (亮色系) ====== -->
            
            <!-- 后置主动推杆 (Actuator L) -->
            <g id="actuator-l-group">
                <!-- 外筒 (固定在底盘) -->
                <line id="act-l-base" x1="-120" y1="-15" x2="-120" y2="-100" stroke="var(--mech-accent)" stroke-width="16" stroke-linecap="round"/>
                <!-- 内杆 (连接平台) -->
                <line id="act-l-rod" x1="-120" y1="-60" x2="-120" y2="-180" stroke="var(--ifr-glow)" stroke-width="8" stroke-linecap="round" class="glow-cyan"/>
                <!-- 关节连接点 -->
                <circle id="act-l-joint-bottom" cx="-120" cy="-15" r="6" fill="#f8fafc" />
                <circle id="act-l-joint-top" cx="-120" cy="-180" r="6" fill="#f8fafc" />
            </g>

            <!-- 前置主动推杆 (Actuator R) -->
            <g id="actuator-r-group">
                <line id="act-r-base" x1="120" y1="-15" x2="120" y2="-100" stroke="var(--mech-accent)" stroke-width="16" stroke-linecap="round"/>
                <line id="act-r-rod" x1="120" y1="-60" x2="120" y2="-180" stroke="var(--ifr-glow)" stroke-width="8" stroke-linecap="round" class="glow-cyan"/>
                <circle id="act-r-joint-bottom" cx="120" cy="-15" r="6" fill="#f8fafc" />
                <circle id="act-r-joint-top" cx="120" cy="-180" r="6" fill="#f8fafc" />
            </g>

            <!-- ====== 最终理想状态 (水平载荷平台) ====== -->
            
            <!-- 载货平台 -->
            <g id="payload-platform" transform="translate(0, -180)">
                <!-- 平台基座 -->
                <rect x="-160" y="-10" width="320" height="20" rx="5" fill="#334155" stroke="var(--ifr-glow)" stroke-width="2" class="glow-cyan"/>
                
                <!-- 货物箱 -->
                <path d="M -120 -10 L -120 -120 L 120 -120 L 120 -10 Z" fill="url(#hatch)" />
                <rect x="-120" y="-120" width="240" height="110" rx="8" fill="var(--payload-glow)" stroke="var(--payload-base)" stroke-width="3" class="glow-amber"/>
                
                <!-- 货物标识 -->
                <text x="0" y="-60" font-size="24" fill="var(--payload-base)" text-anchor="middle" font-weight="900" letter-spacing="4">SENSITIVE CARGO</text>
                <circle cx="0" cy="-120" r="4" fill="var(--payload-base)" class="glow-amber"/>
                
                <!-- IFR 绝对水平激光束指示器 -->
                <g class="glow-green">
                    <line x1="-800" y1="-135" x2="800" y2="-135" class="laser-beam" />
                    <!-- 陀螺仪图标 -->
                    <circle cx="0" cy="-135" r="12" fill="#0f172a" stroke="var(--stable-green)" stroke-width="2"/>
                    <path d="M -8 -135 L 8 -135 M 0 -143 L 0 -127" stroke="var(--stable-green)" stroke-width="2"/>
                    <text x="20" y="-145" font-size="12" fill="var(--stable-green)" font-weight="bold">IFR HORIZON LOCKED</text>
                </g>
            </g>
        </g>
    </svg>
</div>

<script>
    /**
     * 核心逻辑与运动学解算
     */
    
    // 配置参数
    const config = {
        wheelbase: 360,          // 前后轮轴距
        wheelRadiusRear: 45,     // 后轮半径
        planetaryRadius: 45,     // 行星轮旋转外切等效半径 (约等于支架长度)
        planetaryArm: 45,        // 行星架臂长
        platformHeight: -220,    // 平台相对于屏幕的固定绝对高度 (实现IFR绝对隔离)
        mountOffset: 120,        // 推杆安装点距中心距离
        baseStrokeLength: 160    // 推杆静止长度
    };

    // 状态变量
    let worldOffset = 0;
    let speedMult = 4;
    let animationFrameId;

    // DOM 元素引用
    const els = {
        terrain: document.getElementById('terrain-path'),
        terrainGlow: document.getElementById('terrain-path-glow'),
        chassis: document.getElementById('chassis-beam'),
        rearArm: document.getElementById('rear-arm'),
        frontArm: document.getElementById('front-arm'),
        rearWheelSpoke: document.getElementById('rear-wheel-spoke'),
        planetaryHub: document.getElementById('planetary-hub'),
        
        actLBase: document.getElementById('act-l-base'),
        actLRod: document.getElementById('act-l-rod'),
        actLJointB: document.getElementById('act-l-joint-bottom'),
        actLJointT: document.getElementById('act-l-joint-top'),
        
        actRBase: document.getElementById('act-r-base'),
        actRRod: document.getElementById('act-r-rod'),
        actRJointB: document.getElementById('act-r-joint-bottom'),
        actRJointT: document.getElementById('act-r-joint-top'),
        
        platform: document.getElementById('payload-platform'),
        
        // HUD
        valPitch: document.getElementById('val-pitch'),
        valActL: document.getElementById('val-actL'),
        valActR: document.getElementById('val-actR'),
        speedCtrl: document.getElementById('speedCtrl')
    };

    // 监听滑块控制速度
    els.speedCtrl.addEventListener('input', (e) => {
        speedMult = parseFloat(e.target.value);
    });

    /**
     * 缓动函数 (生成平滑不规则台阶)
     */
    function smoothstep(edge0, edge1, x) {
        const t = Math.max(0, Math.min(1, (x - edge0) / (edge1 - edge0)));
        return t * t * (3 - 2 * t);
    }

    /**
     * 地形高度生成函数
     * 基于世界坐标 X 返回对应的地面 Y 坐标
     */
    function getTerrainY(worldX) {
        // 循环周期 1600
        const period = 2000;
        let x = ((worldX % period) + period) % period; 
        
        let baseY = 650; // 基础地面高度
        
        // 构建不规则连续台阶 (模拟问题场景)
        // 阶梯 1: 暴力突变
        if (x > 300 && x <= 450) baseY -= smoothstep(300, 380, x) * 120;
        else if (x > 450 && x <= 800) baseY -= 120;
        // 阶梯 2: 继续上升
        else if (x > 800 && x <= 950) baseY -= 120 + smoothstep(800, 880, x) * 80;
        else if (x > 950 && x <= 1300) baseY -= 200;
        // 下坡:断崖式
        else if (x > 1300 && x <= 1500) baseY -= 200 - smoothstep(1300, 1400, x) * 200;
        
        // 叠加一些微小颠簸
        baseY += Math.sin(x * 0.05) * 5;
        
        return baseY;
    }

    /**
     * 颜色映射:根据推杆形变计算颜色
     */
    function getActuatorColor(currentLength) {
        const diff = currentLength - config.baseStrokeLength;
        const maxStrain = 50; // 50mm 形变达到最大颜色深度
        const ratio = Math.max(-1, Math.min(1, diff / maxStrain));
        
        if (ratio > 0) {
            // 拉伸:变蓝
            return `color-mix(in srgb, var(--ifr-extend) ${ratio * 100}%, var(--ifr-glow))`;
        } else {
            // 压缩:变橙
            return `color-mix(in srgb, var(--ifr-compress) ${Math.abs(ratio) * 100}%, var(--ifr-glow))`;
        }
    }

    /**
     * 主渲染循环
     */
    function render() {
        // 1. 更新世界偏移量 (跑步机移动)
        worldOffset += speedMult;
        
        // 2. 绘制可视地形
        let pathD = `M -100 ${getTerrainY(-100 + worldOffset)} `;
        for (let screenX = -50; screenX <= 1650; screenX += 20) {
            let wx = screenX + worldOffset;
            pathD += `L ${screenX} ${getTerrainY(wx)} `;
        }
        pathD += `L 1650 1000 L -100 1000 Z`;
        els.terrain.setAttribute('d', pathD);
        els.terrainGlow.setAttribute('d', pathD);

        // 3. 机器人运动学解算 (Robot 固定在 screenX = 800)
        const centerScreenX = 0; // 相对机器人 Group 的内部坐标系系
        const frontWorldX = 800 + (config.wheelbase / 2) + worldOffset;
        const rearWorldX  = 800 - (config.wheelbase / 2) + worldOffset;

        // 获取前后轮与地面的接触高度
        const frontY = getTerrainY(frontWorldX) - config.planetaryRadius;
        const rearY  = getTerrainY(rearWorldX) - config.wheelRadiusRear;

        // 计算底盘倾角 (弧度)
        // dx = wheelbase, dy = frontY - rearY
        const chassisAngleRad = Math.atan2(frontY - rearY, config.wheelbase);
        const chassisAngleDeg = chassisAngleRad * (180 / Math.PI);

        // 计算底盘中心绝对高度
        const chassisCenterY = (frontY + rearY) / 2;

        // --- 应用底盘变换 (展现问题:剧烈颠簸) ---
        // 悬挂臂作为刚体连接,通过改变 Y 坐标和旋转来体现
        els.chassis.setAttribute('transform', `translate(0, ${chassisCenterY}) rotate(${chassisAngleDeg})`);
        els.rearArm.setAttribute('y2', rearY - chassisCenterY); // 连接中心到后轮
        els.frontArm.setAttribute('y2', frontY - chassisCenterY); // 连接中心到前轮
        
        document.getElementById('rear-wheel').setAttribute('cy', rearY);
        document.getElementById('rear-wheel-hub').setAttribute('cy', rearY);
        document.getElementById('rear-wheel-spoke').setAttribute('transform', `translate(-180, ${rearY}) rotate(${(worldOffset * 2) % 360}) translate(180, ${-rearY})`);
        
        // 行星轮越障动画逻辑:当遇到陡坡时,模拟行星架翻转
        // 简单计算地形导数来决定是否触发翻转
        const slope = getTerrainY(frontWorldX + 10) - getTerrainY(frontWorldX - 10);
        let extraPlanetaryRot = 0;
        if (slope < -10) { // 上坡
            extraPlanetaryRot = slope * -5;
        }
        const baseSpin = (worldOffset * 1.5) % 360;
        els.planetaryHub.setAttribute('transform', `translate(180, ${frontY}) rotate(${baseSpin + extraPlanetaryRot})`);


        // --- 应用核心 IFR 方案:主动悬挂反向补偿 ---
        
        // 目标:平台保持绝对高度和 0 度倾角 (即不随底盘翻转或上下颠簸)
        // 在 SVG 的 Robot 局部坐标系中,平台中心坐标固定为 (0, config.platformHeight)
        const platformY_local = config.platformHeight;
        els.platform.setAttribute('transform', `translate(0, ${platformY_local})`);

        // 解算左右液压杆形态
        // 底盘上的安装点 (随底盘中心高度和倾角变化)
        const A = chassisAngleRad;
        const L_baseX_local = -config.mountOffset * Math.cos(A);
        const L_baseY_local = chassisCenterY - config.mountOffset * Math.sin(A);
        const R_baseX_local = config.mountOffset * Math.cos(A);
        const R_baseY_local = chassisCenterY + config.mountOffset * Math.sin(A);

        // 平台上的安装点 (恒定水平,固定于平台)
        const L_topX_local = -config.mountOffset;
        const L_topY_local = platformY_local;
        const R_topX_local = config.mountOffset;
        const R_topY_local = platformY_local;

        // 计算当前推杆长度
        const strokeL = Math.sqrt(Math.pow(L_baseX_local - L_topX_local, 2) + Math.pow(L_baseY_local - L_topY_local, 2));
        const strokeR = Math.sqrt(Math.pow(R_baseX_local - R_topX_local, 2) + Math.pow(R_baseY_local - R_topY_local, 2));

        // 更新左侧推杆 SVG 坐标
        els.actLJointB.setAttribute('cx', L_baseX_local); els.actLJointB.setAttribute('cy', L_baseY_local);
        els.actLJointT.setAttribute('cx', L_topX_local);  els.actLJointT.setAttribute('cy', L_topY_local);
        els.actLBase.setAttribute('x1', L_baseX_local);   els.actLBase.setAttribute('y1', L_baseY_local);
        els.actLBase.setAttribute('x2', L_baseX_local + (L_topX_local - L_baseX_local)*0.5); 
        els.actLBase.setAttribute('y2', L_baseY_local + (L_topY_local - L_baseY_local)*0.5); // 外筒指向
        els.actLRod.setAttribute('x1', L_baseX_local);    els.actLRod.setAttribute('y1', L_baseY_local);
        els.actLRod.setAttribute('x2', L_topX_local);     els.actLRod.setAttribute('y2', L_topY_local);
        
        // 更新右侧推杆 SVG 坐标
        els.actRJointB.setAttribute('cx', R_baseX_local); els.actRJointB.setAttribute('cy', R_baseY_local);
        els.actRJointT.setAttribute('cx', R_topX_local);  els.actRJointT.setAttribute('cy', R_topY_local);
        els.actRBase.setAttribute('x1', R_baseX_local);   els.actRBase.setAttribute('y1', R_baseY_local);
        els.actRBase.setAttribute('x2', R_baseX_local + (R_topX_local - R_baseX_local)*0.5); 
        els.actRBase.setAttribute('y2', R_baseY_local + (R_topY_local - R_baseY_local)*0.5);
        els.actRRod.setAttribute('x1', R_baseX_local);    els.actRRod.setAttribute('y1', R_baseY_local);
        els.actRRod.setAttribute('x2', R_topX_local);     els.actRRod.setAttribute('y2', R_topY_local);

        // 动态颜色反馈应力
        els.actLRod.style.stroke = getActuatorColor(strokeL);
        els.actRRod.style.stroke = getActuatorColor(strokeR);

        // 4. 更新 UI 遥测数据
        // 底盘因地势引起的俯仰 (负值为仰头,此处转为正负直观展示)
        els.valPitch.textContent = (-chassisAngleDeg).toFixed(1) + '°';
        // 警示色:当倾角过大时底盘数据标红,但载荷保持绿
        els.valPitch.style.color = Math.abs(chassisAngleDeg) > 15 ? '#ef4444' : '#f8fafc';
        
        // 推杆动作量 (相对静止长度的形变)
        const deltaL = config.baseStrokeLength - strokeL; 
        const deltaR = config.baseStrokeLength - strokeR;
        els.valActL.textContent = (deltaL > 0 ? '+' : '') + deltaL.toFixed(1) + ' mm';
        els.valActR.textContent = (deltaR > 0 ? '+' : '') + deltaR.toFixed(1) + ' mm';

        // 循环请求下一帧
        animationFrameId = requestAnimationFrame(render);
    }

    // 页面加载完成后自动启动动画,无须用户交互
    window.addEventListener('DOMContentLoaded', () => {
        render();
    });

    // 确保 iframe 重载时也能正确触发
    window.addEventListener('load', () => {
        if(!animationFrameId) {
            render();
        }
    });
</script>
</body>
</html>
积分规则:第一轮对话扣减8分,后续每轮扣6分