优化
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
.venv/
|
||||
.env
|
||||
.env.auth
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
artifacts/runs/
|
||||
tmp/*.log
|
||||
+205
-28
@@ -19,6 +19,8 @@ const state = {
|
||||
result: null,
|
||||
progressTimer: null,
|
||||
seatTwin: null,
|
||||
seatActuatorState: {},
|
||||
availableSeatAxes: null,
|
||||
};
|
||||
|
||||
const $ = (selector, root = document) => root.querySelector(selector);
|
||||
@@ -1629,36 +1631,83 @@ function classifyReferenceMaterial(material) {
|
||||
}
|
||||
|
||||
const TWIN_OUTLINE_STYLE = {
|
||||
body: { color: 0x315d69, lineOpacity: 0.52, fillOpacity: 0.014, renderOrder: 30 },
|
||||
glass: { color: 0x78a7b2, lineOpacity: 0.18, fillOpacity: 0.006, renderOrder: 31 },
|
||||
detail: { color: 0x536f76, lineOpacity: 0.28, fillOpacity: 0.009, renderOrder: 32 },
|
||||
wheel: { color: 0x203d45, lineOpacity: 0.68, fillOpacity: 0.02, renderOrder: 33 },
|
||||
interior: { color: 0x607e83, lineOpacity: 0.24, fillOpacity: 0.008, renderOrder: 36 },
|
||||
light: { color: 0x536f76, lineOpacity: 0.28, fillOpacity: 0.009, renderOrder: 32 },
|
||||
body: {
|
||||
color: 0x315d69,
|
||||
lineOpacity: 0.36,
|
||||
occludedColor: 0xa7b8bb,
|
||||
occludedLineOpacity: 0.12,
|
||||
fillOpacity: 0.11,
|
||||
thresholdAngle: 58,
|
||||
renderOrder: 30,
|
||||
},
|
||||
glass: { color: 0x78a7b2, lineOpacity: 0, fillOpacity: 0.045, renderOrder: 31 },
|
||||
detail: { color: 0x536f76, lineOpacity: 0, fillOpacity: 0.028, renderOrder: 32 },
|
||||
wheel: {
|
||||
color: 0x203d45,
|
||||
lineOpacity: 0.52,
|
||||
occludedColor: 0x9eafb2,
|
||||
occludedLineOpacity: 0.14,
|
||||
fillOpacity: 0.18,
|
||||
thresholdAngle: 50,
|
||||
renderOrder: 33,
|
||||
},
|
||||
interior: { color: 0x607e83, lineOpacity: 0, fillOpacity: 0.012, renderOrder: 36 },
|
||||
light: { color: 0x536f76, lineOpacity: 0, fillOpacity: 0.07, renderOrder: 32 },
|
||||
};
|
||||
|
||||
function addTwinMeshOutline(THREE, mesh, style, thresholdAngle = 30) {
|
||||
if (!mesh.geometry?.attributes?.position || mesh.userData.twinOutline) return null;
|
||||
if (
|
||||
!mesh.geometry?.attributes?.position
|
||||
|| mesh.userData.twinOutline
|
||||
|| style.lineOpacity <= 0
|
||||
) return null;
|
||||
const geometry = new THREE.EdgesGeometry(mesh.geometry, thresholdAngle);
|
||||
if (!geometry.getAttribute("position")?.count) {
|
||||
geometry.dispose();
|
||||
return null;
|
||||
}
|
||||
const material = new THREE.LineBasicMaterial({
|
||||
const visibleMaterial = new THREE.LineBasicMaterial({
|
||||
color: style.color,
|
||||
transparent: true,
|
||||
opacity: style.lineOpacity,
|
||||
depthWrite: false,
|
||||
depthTest: true,
|
||||
depthFunc: THREE.LessEqualDepth,
|
||||
toneMapped: false,
|
||||
});
|
||||
const outline = new THREE.LineSegments(geometry, material);
|
||||
outline.name = `${mesh.name || "mesh"}_outline`;
|
||||
outline.renderOrder = style.renderOrder;
|
||||
outline.frustumCulled = mesh.frustumCulled;
|
||||
mesh.add(outline);
|
||||
mesh.userData.twinOutline = outline;
|
||||
return outline;
|
||||
visibleMaterial.onBeforeCompile = (shader) => {
|
||||
shader.vertexShader = shader.vertexShader.replace(
|
||||
"#include <project_vertex>",
|
||||
"#include <project_vertex>\ngl_Position.z -= 0.0006 * gl_Position.w;",
|
||||
);
|
||||
};
|
||||
visibleMaterial.customProgramCacheKey = () => "twin-visible-line-depth-bias-v1";
|
||||
const visibleOutline = new THREE.LineSegments(geometry, visibleMaterial);
|
||||
visibleOutline.name = `${mesh.name || "mesh"}_outline_visible`;
|
||||
visibleOutline.renderOrder = style.renderOrder + 1;
|
||||
visibleOutline.frustumCulled = mesh.frustumCulled;
|
||||
mesh.add(visibleOutline);
|
||||
mesh.userData.twinOutline = visibleOutline;
|
||||
|
||||
const occludedOpacity = style.occludedLineOpacity ?? 0;
|
||||
if (occludedOpacity > 0) {
|
||||
const occludedMaterial = new THREE.LineBasicMaterial({
|
||||
color: style.occludedColor ?? style.color,
|
||||
transparent: true,
|
||||
opacity: occludedOpacity,
|
||||
depthWrite: false,
|
||||
depthTest: true,
|
||||
depthFunc: THREE.GreaterDepth,
|
||||
toneMapped: false,
|
||||
});
|
||||
const occludedOutline = new THREE.LineSegments(geometry, occludedMaterial);
|
||||
occludedOutline.name = `${mesh.name || "mesh"}_outline_occluded`;
|
||||
occludedOutline.renderOrder = style.renderOrder;
|
||||
occludedOutline.frustumCulled = mesh.frustumCulled;
|
||||
mesh.add(occludedOutline);
|
||||
mesh.userData.twinOccludedOutline = occludedOutline;
|
||||
}
|
||||
return visibleOutline;
|
||||
}
|
||||
|
||||
function applyTwinOutlineStyle(THREE, root, options = {}) {
|
||||
@@ -1694,6 +1743,9 @@ function configureTwinSolidEntity(THREE, roots, options = {}) {
|
||||
root.traverse((object) => {
|
||||
if (!object.isMesh || !object.visible) return;
|
||||
if (object.userData.twinOutline) object.userData.twinOutline.visible = false;
|
||||
if (object.userData.twinOccludedOutline) {
|
||||
object.userData.twinOccludedOutline.visible = false;
|
||||
}
|
||||
const materials = Array.isArray(object.material) ? object.material : [object.material];
|
||||
materials.filter(Boolean).forEach((material) => {
|
||||
material.transparent = Boolean(options.transparent);
|
||||
@@ -1792,12 +1844,20 @@ function configureReferenceVehicleModel(THREE, vehicle) {
|
||||
const material = sourceMaterial.clone();
|
||||
const descriptor = referenceMaterialDescriptor(material);
|
||||
const category = classifyReferenceMaterial(material);
|
||||
const outlineStyle = TWIN_OUTLINE_STYLE[category] || TWIN_OUTLINE_STYLE.detail;
|
||||
const key = `${category}:${material.name || "unnamed"}`;
|
||||
report.materials.set(key, (report.materials.get(key) || 0) + 1);
|
||||
if (material.map) {
|
||||
material.map.colorSpace = THREE.SRGBColorSpace;
|
||||
material.map.anisotropy = 8;
|
||||
}
|
||||
[
|
||||
"map",
|
||||
"aoMap",
|
||||
"bumpMap",
|
||||
"normalMap",
|
||||
"roughnessMap",
|
||||
"metalnessMap",
|
||||
"emissiveMap",
|
||||
].forEach((textureKey) => {
|
||||
if (textureKey in material) material[textureKey] = null;
|
||||
});
|
||||
if (material.emissive) {
|
||||
material.emissive.setHex(0x000000);
|
||||
if ("emissiveIntensity" in material) material.emissiveIntensity = 0;
|
||||
@@ -1832,11 +1892,15 @@ function configureReferenceVehicleModel(THREE, vehicle) {
|
||||
} else if (/chrome|metal|alloy|rim/.test(descriptor) && material.color) {
|
||||
material.color.setHex(0xaeb9bb);
|
||||
}
|
||||
const outlineStyle = TWIN_OUTLINE_STYLE[category] || TWIN_OUTLINE_STYLE.detail;
|
||||
material.transparent = true;
|
||||
material.opacity = outlineStyle.fillOpacity;
|
||||
material.depthWrite = false;
|
||||
material.side = THREE.DoubleSide;
|
||||
material.depthWrite = ["body", "detail", "wheel"].includes(category);
|
||||
material.depthTest = true;
|
||||
material.side = category === "glass" ? THREE.DoubleSide : THREE.FrontSide;
|
||||
material.wireframe = false;
|
||||
if ("flatShading" in material) material.flatShading = false;
|
||||
if ("roughness" in material) material.roughness = category === "body" ? 0.72 : 0.82;
|
||||
if ("metalness" in material) material.metalness = category === "wheel" ? 0.48 : 0.08;
|
||||
if (material.emissive) material.emissive.setHex(0x000000);
|
||||
if ("emissiveIntensity" in material) material.emissiveIntensity = 0;
|
||||
material.needsUpdate = true;
|
||||
@@ -1851,7 +1915,12 @@ function configureReferenceVehicleModel(THREE, vehicle) {
|
||||
const outlineCategory = categoryPriority.find((category) => categories.includes(category)) || "detail";
|
||||
const outlineStyle = TWIN_OUTLINE_STYLE[outlineCategory];
|
||||
object.renderOrder = outlineStyle.renderOrder - 1;
|
||||
addTwinMeshOutline(THREE, object, outlineStyle, 30);
|
||||
addTwinMeshOutline(
|
||||
THREE,
|
||||
object,
|
||||
outlineStyle,
|
||||
outlineStyle.thresholdAngle ?? 60,
|
||||
);
|
||||
});
|
||||
return report;
|
||||
}
|
||||
@@ -2048,6 +2117,7 @@ function initSeatTwin() {
|
||||
|
||||
const THREE = window.THREE;
|
||||
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, powerPreference: "high-performance" });
|
||||
renderer.autoClear = false;
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
||||
if ("outputColorSpace" in renderer) renderer.outputColorSpace = THREE.SRGBColorSpace;
|
||||
else renderer.outputEncoding = THREE.sRGBEncoding;
|
||||
@@ -2058,8 +2128,11 @@ function initSeatTwin() {
|
||||
scene.background = new THREE.Color(0xe8eeec);
|
||||
scene.fog = new THREE.Fog(0xe8eeec, 12, 19);
|
||||
const camera = new THREE.PerspectiveCamera(34, 1, 0.1, 40);
|
||||
scene.add(new THREE.HemisphereLight(0xffffff, 0x7d8b90, 2.2));
|
||||
const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x7d8b90, 2.2);
|
||||
hemisphereLight.layers.enable(1);
|
||||
scene.add(hemisphereLight);
|
||||
const keyLight = new THREE.DirectionalLight(0xffffff, 3.2);
|
||||
keyLight.layers.enable(1);
|
||||
keyLight.position.set(-4, 7, 6);
|
||||
keyLight.castShadow = true;
|
||||
keyLight.shadow.mapSize.set(1024, 1024);
|
||||
@@ -2069,6 +2142,7 @@ function initSeatTwin() {
|
||||
keyLight.shadow.camera.bottom = -4;
|
||||
scene.add(keyLight);
|
||||
const fillLight = new THREE.DirectionalLight(0x9fc7d7, 1.15);
|
||||
fillLight.layers.enable(1);
|
||||
fillLight.position.set(5, 3, -5);
|
||||
scene.add(fillLight);
|
||||
|
||||
@@ -2148,6 +2222,14 @@ function initSeatTwin() {
|
||||
});
|
||||
scene.add(fixedModels.driver.root);
|
||||
vehicleSystems.root.add(fixedModels.rear);
|
||||
[
|
||||
vehicleSystems.root,
|
||||
models.passenger.root,
|
||||
fixedModels.driver.root,
|
||||
fixedModels.rear,
|
||||
].forEach((root) => {
|
||||
root.traverse((object) => object.layers.set(1));
|
||||
});
|
||||
|
||||
const initialDevice = () => ({
|
||||
ventilation: { state: "off", level: 0 },
|
||||
@@ -2300,6 +2382,7 @@ function initSeatTwin() {
|
||||
const anchorX = (projectedPosition.x * 0.5 + 0.5) * width;
|
||||
const anchorY = (-projectedPosition.y * 0.5 + 0.5) * height;
|
||||
const offset = {
|
||||
longitudinal: { x: -104, y: 50 },
|
||||
backrest_recline: { x: -108, y: -28 },
|
||||
cushion_tilt: { x: 106, y: -4 },
|
||||
legrest_height: { x: 112, y: 52 },
|
||||
@@ -2388,7 +2471,16 @@ function initSeatTwin() {
|
||||
);
|
||||
camera.lookAt(cameraTargetX, 1.14, -0.08);
|
||||
positionTwinCallouts();
|
||||
renderer.clear();
|
||||
camera.layers.set(0);
|
||||
renderer.render(scene, camera);
|
||||
renderer.clearDepth();
|
||||
const sceneBackground = scene.background;
|
||||
scene.background = null;
|
||||
camera.layers.set(1);
|
||||
renderer.render(scene, camera);
|
||||
scene.background = sceneBackground;
|
||||
camera.layers.set(0);
|
||||
if (!canvas.dataset.rendered) canvas.dataset.rendered = "true";
|
||||
window.requestAnimationFrame(animate);
|
||||
}
|
||||
@@ -2461,6 +2553,58 @@ function applyTwinAngleRanges(vehicle) {
|
||||
});
|
||||
}
|
||||
|
||||
function applySeatMotorAvailability(vehicle) {
|
||||
const motors = vehicle?.seat?.motors || {};
|
||||
const availableAxes = new Set(Object.keys(motors));
|
||||
state.availableSeatAxes = availableAxes;
|
||||
Object.keys(TWIN_DEFAULT_POSITIONS).forEach((axis) => {
|
||||
const available = availableAxes.has(axis);
|
||||
$$(`[data-axis="${axis}"], [data-twin-axis="${axis}"], [data-callout="${axis}"]`)
|
||||
.forEach((item) => {
|
||||
item.classList.toggle("seat-axis-unavailable", !available);
|
||||
item.setAttribute("aria-hidden", String(!available));
|
||||
});
|
||||
const motorName = motors[axis]?.name || axis;
|
||||
const twinLabel = axis === "longitudinal"
|
||||
? `${motorName}位置`
|
||||
: ["cushion_tilt", "backrest_recline", "legrest_height"].includes(axis)
|
||||
? `${motorName}夹角`
|
||||
: motorName;
|
||||
$$(`[data-twin-axis="${axis}"] > span, [data-callout="${axis}"] > span`)
|
||||
.forEach((label) => { label.textContent = twinLabel; });
|
||||
});
|
||||
const cushionName = motors.cushion_tilt?.name;
|
||||
const backrestName = motors.backrest_recline?.name;
|
||||
const legrestName = motors.legrest_height?.name;
|
||||
const backrestIncludedLabel = $('[data-callout="backrest_included_angle"] > span');
|
||||
const legrestIncludedLabel = $('[data-callout="legrest_included_angle"] > span');
|
||||
if (backrestIncludedLabel && backrestName && cushionName) {
|
||||
backrestIncludedLabel.textContent = `${backrestName}-${cushionName}夹角`;
|
||||
}
|
||||
if (legrestIncludedLabel && cushionName && legrestName) {
|
||||
legrestIncludedLabel.textContent = `${cushionName}-${legrestName}夹角`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderSeatMotorRows(vehicle) {
|
||||
const motors = Object.entries(vehicle?.seat?.motors || {});
|
||||
$$(".seat-card .motion-state-grid").forEach((grid) => {
|
||||
const fragment = document.createDocumentFragment();
|
||||
motors.forEach(([axis, motor]) => {
|
||||
const item = document.createElement("div");
|
||||
item.className = "motion-axis";
|
||||
item.dataset.axis = axis;
|
||||
const label = document.createElement("span");
|
||||
label.textContent = motor?.name || axis;
|
||||
const value = document.createElement("b");
|
||||
value.textContent = "--";
|
||||
item.append(label, value, document.createElement("i"));
|
||||
fragment.append(item);
|
||||
});
|
||||
grid.replaceChildren(fragment);
|
||||
});
|
||||
}
|
||||
|
||||
function selectVehicle(vehicleId, { silent = false } = {}) {
|
||||
const vehicle = findVehicleProfile(vehicleId);
|
||||
if (!vehicle) return;
|
||||
@@ -2468,6 +2612,8 @@ function selectVehicle(vehicleId, { silent = false } = {}) {
|
||||
const select = $("#vehicleSelect");
|
||||
if (select) select.value = vehicle.id;
|
||||
applyTwinAngleRanges(vehicle);
|
||||
renderSeatMotorRows(vehicle);
|
||||
applySeatMotorAvailability(vehicle);
|
||||
const longitudinal = vehicle.seat?.motors?.longitudinal;
|
||||
const backrest = vehicle.seat?.motors?.backrest_recline;
|
||||
$("#vehicleNote").textContent = longitudinal && backrest
|
||||
@@ -2476,7 +2622,7 @@ function selectVehicle(vehicleId, { silent = false } = {}) {
|
||||
if (state.result?.actuator_state) {
|
||||
renderSeatState(state.result.actuator_state);
|
||||
} else if (state.seatTwin?.actuatorState) {
|
||||
updateSeatTwin(state.seatTwin.actuatorState);
|
||||
renderSeatState(state.seatTwin.actuatorState);
|
||||
}
|
||||
if (!silent) showToast(`已切换车型:${vehicle.name}`);
|
||||
}
|
||||
@@ -2767,10 +2913,41 @@ function resetSeatState() {
|
||||
});
|
||||
}
|
||||
|
||||
function mergeSeatActuatorState(previousState, patchState) {
|
||||
const mergedState = { ...(previousState || {}) };
|
||||
Object.entries(patchState || {}).forEach(([seat, functions]) => {
|
||||
const previousSeat = mergedState[seat] || {};
|
||||
const mergedSeat = { ...previousSeat };
|
||||
Object.entries(functions || {}).forEach(([feature, value]) => {
|
||||
if (feature === "position") {
|
||||
const previousPosition = previousSeat.position || {};
|
||||
const mergedPosition = { ...previousPosition };
|
||||
Object.entries(value || {}).forEach(([axis, motion]) => {
|
||||
mergedPosition[axis] = motion && typeof motion === "object"
|
||||
? { ...(previousPosition[axis] || {}), ...motion }
|
||||
: motion;
|
||||
});
|
||||
mergedSeat.position = mergedPosition;
|
||||
return;
|
||||
}
|
||||
mergedSeat[feature] = value && typeof value === "object" && !Array.isArray(value)
|
||||
? { ...(previousSeat[feature] || {}), ...value }
|
||||
: value;
|
||||
});
|
||||
mergedState[seat] = mergedSeat;
|
||||
});
|
||||
return mergedState;
|
||||
}
|
||||
|
||||
function renderSeatState(actuatorState) {
|
||||
resetSeatState();
|
||||
if (!actuatorState) return;
|
||||
Object.entries(actuatorState).forEach(([seat, functions]) => {
|
||||
const mergedActuatorState = mergeSeatActuatorState(
|
||||
state.seatActuatorState,
|
||||
actuatorState,
|
||||
);
|
||||
state.seatActuatorState = mergedActuatorState;
|
||||
resetSeatState();
|
||||
Object.entries(mergedActuatorState).forEach(([seat, functions]) => {
|
||||
Object.entries(functions).forEach(([feature, value]) => {
|
||||
if (feature === "position") {
|
||||
Object.entries(value).forEach(([axis, motion]) => {
|
||||
@@ -2793,7 +2970,7 @@ function renderSeatState(actuatorState) {
|
||||
$("b", item).textContent = value.state === "on" ? `ON · L${value.level}` : "OFF";
|
||||
});
|
||||
});
|
||||
updateSeatTwin(actuatorState);
|
||||
updateSeatTwin(mergedActuatorState);
|
||||
}
|
||||
|
||||
function renderDomainState(domainState) {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<script src="https://unpkg.com/three@0.147.0/build/three.min.js" defer></script>
|
||||
<script src="https://unpkg.com/three@0.147.0/examples/js/libs/fflate.min.js" defer></script>
|
||||
<script src="https://unpkg.com/three@0.147.0/examples/js/loaders/FBXLoader.js" defer></script>
|
||||
<script src="/app.js?v=unified-cabin-13" defer></script>
|
||||
<script src="/app.js?v=unified-cabin-22" defer></script>
|
||||
<script type="module" src="/gesture-control.js?v=gesture-core-3"></script>
|
||||
</head>
|
||||
<body>
|
||||
@@ -289,7 +289,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="twin-stage" id="seatTwin" data-selected-seat="passenger">
|
||||
<canvas id="seatTwinCanvas" data-vehicle-model="byd-tang-2024-reference" data-seat-layout="five-seat" data-active-seat="front-right" aria-label="透明轮廓整车、固定主驾和后排、动态副驾、全景天幕、仪表台空调出风口与副驾手套箱上方氛围灯三维状态"></canvas>
|
||||
<canvas id="seatTwinCanvas" data-vehicle-model="byd-tang-2024-reference" data-seat-layout="five-seat" data-active-seat="front-right" aria-label="具有前后遮挡明暗层次的半透明整车外壳、始终清晰显示的动态副驾、全景天幕、仪表台空调出风口与副驾手套箱上方氛围灯三维状态"></canvas>
|
||||
<div class="vehicle-system-hud" aria-label="整车孪生系统状态">
|
||||
<span data-twin-system="roof"><i data-lucide="panels-top-left"></i><small>全景天幕</small><b id="twinRoofState">透光 100%</b></span>
|
||||
<span data-twin-system="climate"><i data-lucide="wind"></i><small>仪表台 HVAC</small><b id="twinClimateState">关闭</b></span>
|
||||
@@ -348,13 +348,7 @@
|
||||
<span data-function="ventilation"><i data-lucide="wind"></i>通风 <b>--</b></span>
|
||||
<span data-function="heating"><i data-lucide="flame"></i>加热 <b>--</b></span>
|
||||
</div>
|
||||
<div class="motion-state-grid">
|
||||
<div class="motion-axis" data-axis="longitudinal"><span>水平</span><b>--</b><i></i></div>
|
||||
<div class="motion-axis" data-axis="cushion_tilt"><span>坐垫俯仰</span><b>--</b><i></i></div>
|
||||
<div class="motion-axis" data-axis="backrest_recline"><span>靠背</span><b>--</b><i></i></div>
|
||||
<div class="motion-axis" data-axis="legrest_height"><span>腿托升降</span><b>--</b><i></i></div>
|
||||
<div class="motion-axis" data-axis="legrest_extension"><span>腿托伸缩</span><b>--</b><i></i></div>
|
||||
</div>
|
||||
<div class="motion-state-grid" aria-live="polite"></div>
|
||||
</article>
|
||||
<article class="seat-card" data-seat="passenger">
|
||||
<div class="seat-identity"><i data-lucide="armchair"></i><div><strong>副驾</strong><span>front_right</span></div></div>
|
||||
@@ -362,13 +356,7 @@
|
||||
<span data-function="ventilation"><i data-lucide="wind"></i>通风 <b>--</b></span>
|
||||
<span data-function="heating"><i data-lucide="flame"></i>加热 <b>--</b></span>
|
||||
</div>
|
||||
<div class="motion-state-grid">
|
||||
<div class="motion-axis" data-axis="longitudinal"><span>水平</span><b>--</b><i></i></div>
|
||||
<div class="motion-axis" data-axis="cushion_tilt"><span>坐垫俯仰</span><b>--</b><i></i></div>
|
||||
<div class="motion-axis" data-axis="backrest_recline"><span>靠背</span><b>--</b><i></i></div>
|
||||
<div class="motion-axis" data-axis="legrest_height"><span>腿托升降</span><b>--</b><i></i></div>
|
||||
<div class="motion-axis" data-axis="legrest_extension"><span>腿托伸缩</span><b>--</b><i></i></div>
|
||||
</div>
|
||||
<div class="motion-state-grid" aria-live="polite"></div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3015,11 +3015,14 @@ pre {
|
||||
bottom: 18px;
|
||||
}
|
||||
|
||||
.joint-twin-shell [data-callout="longitudinal"],
|
||||
.joint-twin-shell [data-callout="legrest_extension"] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.seat-axis-unavailable {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.joint-twin-shell .twin-callout {
|
||||
min-width: 67px;
|
||||
padding: 5px 6px;
|
||||
|
||||
@@ -13,8 +13,8 @@ operations:
|
||||
target_scope: passenger
|
||||
operation: set
|
||||
parameters:
|
||||
position_pct: 19
|
||||
reason: 宝宝午睡模式水平电机目标 40mm,按腾势 N7 215mm 行程换算。
|
||||
position_pct: 81
|
||||
reason: 宝宝午睡模式水平电机目标 175mm,按腾势 N7 215mm 行程换算。
|
||||
- domain: seat
|
||||
capability: seat.set_cushion_tilt
|
||||
target_scope: passenger
|
||||
|
||||
@@ -14,8 +14,8 @@ operations:
|
||||
target_scope: passenger
|
||||
operation: set
|
||||
parameters:
|
||||
position_pct: 49
|
||||
reason: 小腿解压悬浮模式水平电机目标 105mm,按腾势 N7 215mm 行程换算。
|
||||
position_pct: 51
|
||||
reason: 小腿解压悬浮模式水平电机目标 110mm,按腾势 N7 215mm 行程换算。
|
||||
- domain: seat
|
||||
capability: seat.set_cushion_tilt
|
||||
target_scope: passenger
|
||||
|
||||
@@ -13,8 +13,8 @@ operations:
|
||||
target_scope: passenger
|
||||
operation: set
|
||||
parameters:
|
||||
position_pct: 49
|
||||
reason: 巡航沙发模式水平电机目标 105mm,按腾势 N7 215mm 行程换算。
|
||||
position_pct: 51
|
||||
reason: 巡航沙发模式水平电机目标 110mm,按腾势 N7 215mm 行程换算。
|
||||
- domain: seat
|
||||
capability: seat.set_cushion_tilt
|
||||
target_scope: passenger
|
||||
|
||||
@@ -13,8 +13,8 @@ operations:
|
||||
target_scope: passenger
|
||||
operation: set
|
||||
parameters:
|
||||
position_pct: 0
|
||||
reason: 轻松进出模式上下车水平电机目标 0mm。
|
||||
position_pct: 100
|
||||
reason: 轻松进出模式上下车水平电机目标 215mm。
|
||||
- domain: seat
|
||||
capability: seat.set_cushion_tilt
|
||||
target_scope: passenger
|
||||
|
||||
@@ -14,8 +14,8 @@ operations:
|
||||
target_scope: passenger
|
||||
operation: set
|
||||
parameters:
|
||||
position_pct: 0
|
||||
reason: 电感沙滩椅水平电机目标 0mm。
|
||||
position_pct: 100
|
||||
reason: 电感沙滩椅水平电机目标 215mm。
|
||||
- domain: seat
|
||||
capability: seat.set_cushion_tilt
|
||||
target_scope: passenger
|
||||
|
||||
@@ -13,8 +13,8 @@ operations:
|
||||
target_scope: passenger
|
||||
operation: set
|
||||
parameters:
|
||||
position_pct: 49
|
||||
reason: 休闲躺椅模式水平电机目标 105mm,按腾势 N7 215mm 行程换算。
|
||||
position_pct: 51
|
||||
reason: 休闲躺椅模式水平电机目标 110mm,按腾势 N7 215mm 行程换算。
|
||||
- domain: seat
|
||||
capability: seat.set_cushion_tilt
|
||||
target_scope: passenger
|
||||
|
||||
@@ -14,8 +14,8 @@ operations:
|
||||
target_scope: passenger
|
||||
operation: set
|
||||
parameters:
|
||||
position_pct: 0
|
||||
reason: 冥想空间模式水平电机目标 0mm。
|
||||
position_pct: 100
|
||||
reason: 冥想空间模式水平电机目标 215mm。
|
||||
- domain: seat
|
||||
capability: seat.set_cushion_tilt
|
||||
target_scope: passenger
|
||||
|
||||
@@ -13,8 +13,8 @@ operations:
|
||||
target_scope: passenger
|
||||
operation: set
|
||||
parameters:
|
||||
position_pct: 100
|
||||
reason: 小憩模式水平电机目标 215mm,按腾势 N7 215mm 行程换算。
|
||||
position_pct: 0
|
||||
reason: 小憩模式水平电机目标 0mm,按腾势 N7 215mm 行程换算。
|
||||
- domain: seat
|
||||
capability: seat.set_cushion_tilt
|
||||
target_scope: passenger
|
||||
|
||||
@@ -13,8 +13,8 @@ operations:
|
||||
target_scope: passenger
|
||||
operation: set
|
||||
parameters:
|
||||
position_pct: 28
|
||||
reason: 办公模式水平电机目标 60mm,按腾势 N7 215mm 行程换算。
|
||||
position_pct: 72
|
||||
reason: 办公模式水平电机目标 155mm,按腾势 N7 215mm 行程换算。
|
||||
- domain: seat
|
||||
capability: seat.set_cushion_tilt
|
||||
target_scope: passenger
|
||||
|
||||
@@ -14,8 +14,8 @@ operations:
|
||||
target_scope: passenger
|
||||
operation: set
|
||||
parameters:
|
||||
position_pct: 28
|
||||
reason: 女王副驾水平电机目标 60mm,按腾势 N7 215mm 行程换算。
|
||||
position_pct: 72
|
||||
reason: 女王副驾水平电机目标 155mm,按腾势 N7 215mm 行程换算。
|
||||
- domain: seat
|
||||
capability: seat.set_cushion_tilt
|
||||
target_scope: passenger
|
||||
|
||||
@@ -16,8 +16,8 @@ operations:
|
||||
target_scope: passenger
|
||||
operation: set
|
||||
parameters:
|
||||
position_pct: 0
|
||||
reason: 零重力模式水平电机目标 0mm,按腾势 N7 215mm 行程换算。
|
||||
position_pct: 100
|
||||
reason: 零重力模式水平电机目标 215mm,按腾势 N7 215mm 行程换算。
|
||||
- domain: seat
|
||||
capability: seat.set_cushion_tilt
|
||||
target_scope: passenger
|
||||
|
||||
@@ -6,15 +6,15 @@ seat:
|
||||
name: 水平
|
||||
unit: mm
|
||||
range: 215
|
||||
reference_zero: rearmost
|
||||
positive_direction: forward
|
||||
reference_zero: forwardmost
|
||||
positive_direction: backward
|
||||
hall_total_pulses: 480
|
||||
cushion_tilt:
|
||||
name: 坐盆
|
||||
unit: percent
|
||||
range: 100
|
||||
reference_zero: lowest
|
||||
positive_direction: up
|
||||
reference_zero: highest
|
||||
positive_direction: down
|
||||
hall_total_pulses: 374
|
||||
backrest_recline:
|
||||
name: 座椅靠背
|
||||
|
||||
@@ -63,9 +63,10 @@ spec:
|
||||
confirmation_policy: on_ambiguity
|
||||
- name: seat.set_longitudinal_position
|
||||
description: >-
|
||||
Set one front seat's fore-aft rail position. position_pct is an absolute target: 0 is the
|
||||
rearmost mechanical limit and 100 is the forwardmost limit. Resolve relative requests such
|
||||
as "move back a little" against the current position supplied in runtime state. For a
|
||||
Set one front seat's fore-aft rail position. For the selected Denza N7 profile, position_pct
|
||||
is an absolute target: 0 is the forwardmost mechanical limit and 100 is the rearmost limit.
|
||||
Resolve relative requests such as "move back a little" against the current position supplied
|
||||
in runtime state: forward decreases the value and backward increases it. For a
|
||||
vehicle-specific physical relative request, distance_mm may be supplied and the runtime
|
||||
converts it to an absolute position_pct before CAN compilation.
|
||||
target_devices:
|
||||
@@ -92,8 +93,9 @@ spec:
|
||||
confirmation_policy: on_ambiguity
|
||||
- name: seat.set_cushion_tilt
|
||||
description: >-
|
||||
Set the seat-cushion pitch for one front seat. position_pct is absolute: 0 places the front
|
||||
edge at its lowest limit and 100 raises the front edge to its highest tilt limit.
|
||||
Set the seat-cushion pitch for one front seat. For the selected Denza N7 profile,
|
||||
position_pct is absolute: 0 places the front edge at its highest limit and 100 lowers the
|
||||
front edge to its lowest tilt limit.
|
||||
target_devices:
|
||||
- seat_cushion_tilt_motor
|
||||
target_scopes:
|
||||
|
||||
@@ -335,7 +335,7 @@ class SeatServiceIntegrationTests(unittest.TestCase):
|
||||
"domain": "seat",
|
||||
"capability": "seat.set_longitudinal_position",
|
||||
"target_scope": "driver",
|
||||
"operation": "decrease",
|
||||
"operation": "increase",
|
||||
"parameters": [{"name": "position_pct", "value": position}],
|
||||
"reason": "Model resolved the relative request.",
|
||||
}
|
||||
@@ -349,7 +349,7 @@ class SeatServiceIntegrationTests(unittest.TestCase):
|
||||
with patch.object(
|
||||
self.service.openai,
|
||||
"interpret",
|
||||
side_effect=[output(40), output(30)],
|
||||
side_effect=[output(60), output(70)],
|
||||
) as interpret:
|
||||
first = self.service.process(
|
||||
"主驾往后一点", "openai", "conv-motion-follow-up"
|
||||
@@ -362,18 +362,18 @@ class SeatServiceIntegrationTests(unittest.TestCase):
|
||||
first["actuator_state"]["driver"]["position"]["longitudinal"][
|
||||
"position_pct"
|
||||
],
|
||||
40,
|
||||
60,
|
||||
)
|
||||
self.assertEqual(
|
||||
second["actuator_state"]["driver"]["position"]["longitudinal"][
|
||||
"position_pct"
|
||||
],
|
||||
30,
|
||||
70,
|
||||
)
|
||||
second_context = interpret.call_args_list[1].args[1]
|
||||
self.assertEqual(
|
||||
second_context["devices"]["driver"]["position"]["longitudinal"]["position_pct"],
|
||||
40,
|
||||
60,
|
||||
)
|
||||
|
||||
def test_model_relative_physical_motion_uses_vehicle_profile(self) -> None:
|
||||
@@ -384,7 +384,7 @@ class SeatServiceIntegrationTests(unittest.TestCase):
|
||||
"domain": "seat",
|
||||
"capability": "seat.set_longitudinal_position",
|
||||
"target_scope": "driver",
|
||||
"operation": "increase",
|
||||
"operation": "decrease",
|
||||
"parameters": [{"name": "distance_mm", "value": 50}],
|
||||
"reason": "Move forward by a physical distance.",
|
||||
},
|
||||
@@ -414,7 +414,7 @@ class SeatServiceIntegrationTests(unittest.TestCase):
|
||||
self.assertEqual(result["status"], "succeeded")
|
||||
self.assertEqual(result["vehicle"]["id"], "denza_n7")
|
||||
operations = result["function_config"]["spec"]["operations"]
|
||||
self.assertEqual(operations[0]["parameters"], {"position_pct": 73})
|
||||
self.assertEqual(operations[0]["parameters"], {"position_pct": 27})
|
||||
self.assertEqual(
|
||||
operations[1]["parameters"],
|
||||
{"position_pct": 38, "angle_deg": 116},
|
||||
|
||||
@@ -134,6 +134,14 @@ class SeatWebApiTests(unittest.TestCase):
|
||||
runtime["vehicle"]["seat"]["motors"]["longitudinal"]["range"],
|
||||
215,
|
||||
)
|
||||
self.assertEqual(
|
||||
runtime["vehicle"]["seat"]["motors"]["longitudinal"]["reference_zero"],
|
||||
"forwardmost",
|
||||
)
|
||||
self.assertEqual(
|
||||
runtime["vehicle"]["seat"]["motors"]["cushion_tilt"]["reference_zero"],
|
||||
"highest",
|
||||
)
|
||||
capability_names = {item["name"] for item in runtime["capabilities"]}
|
||||
self.assertIn("seat.save_posture_preset", capability_names)
|
||||
self.assertIn("seat.apply_posture_preset", capability_names)
|
||||
|
||||
@@ -242,7 +242,7 @@ class MultiDomainServiceTests(unittest.TestCase):
|
||||
by_capability = {item["capability"]: item for item in operations}
|
||||
self.assertEqual(
|
||||
by_capability["seat.set_longitudinal_position"]["parameters"],
|
||||
{"position_pct": 0},
|
||||
{"position_pct": 100},
|
||||
)
|
||||
self.assertEqual(
|
||||
by_capability["seat.set_cushion_tilt"]["parameters"],
|
||||
|
||||
@@ -24,7 +24,7 @@ class ModeProfileRegistryTests(unittest.TestCase):
|
||||
self.assertEqual(requests[0]["capability"], "seat.set_longitudinal_position")
|
||||
self.assertEqual(requests[0]["target_scope"], "passenger")
|
||||
self.assertIn(
|
||||
{"name": "position_pct", "value": 0},
|
||||
{"name": "position_pct", "value": 100},
|
||||
requests[0]["parameters"],
|
||||
)
|
||||
by_capability = {request["capability"]: request for request in requests}
|
||||
@@ -88,6 +88,47 @@ class ModeProfileRegistryTests(unittest.TestCase):
|
||||
"calf_decompression_float",
|
||||
)
|
||||
|
||||
def test_denza_mode_positions_use_corrected_n7_zero_points(self) -> None:
|
||||
registry = ModeProfileRegistry(REPOSITORY_ROOT / "config" / "modes")
|
||||
expected_positions = {
|
||||
"baby_nap": (81, 30),
|
||||
"calf_decompression_float": (51, 75),
|
||||
"cruise_sofa": (51, 30),
|
||||
"easy_entry_exit": (100, 0),
|
||||
"electric_beach_chair": (100, 0),
|
||||
"leisure_lounger": (51, 30),
|
||||
"meditation_space": (100, 44),
|
||||
"nap": (0, 0),
|
||||
"office": (72, 22),
|
||||
"queen_passenger_driving": (72, 38),
|
||||
"zero_gravity": (100, 100),
|
||||
}
|
||||
|
||||
profiles = {
|
||||
profile.id: profile
|
||||
for profile in registry.profiles()
|
||||
if profile.id in expected_positions and profile.vehicle_ids == ("denza_n7",)
|
||||
}
|
||||
actual_positions = {}
|
||||
for profile_id, profile in profiles.items():
|
||||
requests = {
|
||||
request["capability"]: request
|
||||
for request in registry.requests_for_profiles([profile])
|
||||
}
|
||||
actual_positions[profile_id] = tuple(
|
||||
next(
|
||||
parameter["value"]
|
||||
for parameter in requests[capability]["parameters"]
|
||||
if parameter["name"] == "position_pct"
|
||||
)
|
||||
for capability in (
|
||||
"seat.set_longitudinal_position",
|
||||
"seat.set_cushion_tilt",
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(actual_positions, expected_positions)
|
||||
|
||||
def test_common_modes_are_visible_with_vehicle_specific_overrides(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
|
||||
@@ -26,7 +26,10 @@ class VehicleProfileTests(unittest.TestCase):
|
||||
})
|
||||
self.assertEqual(profile.seat_motors["longitudinal"].range, 215)
|
||||
self.assertEqual(profile.seat_motors["longitudinal"].hall_total_pulses, 480)
|
||||
self.assertEqual(profile.seat_motors["longitudinal"].reference_zero, "rearmost")
|
||||
self.assertEqual(profile.seat_motors["longitudinal"].reference_zero, "forwardmost")
|
||||
self.assertEqual(profile.seat_motors["longitudinal"].positive_direction, "backward")
|
||||
self.assertEqual(profile.seat_motors["cushion_tilt"].reference_zero, "highest")
|
||||
self.assertEqual(profile.seat_motors["cushion_tilt"].positive_direction, "down")
|
||||
self.assertEqual(profile.seat_motors["backrest_recline"].range, 68)
|
||||
self.assertEqual(profile.seat_motors["backrest_recline"].hall_total_pulses, 917)
|
||||
self.assertEqual(profile.seat_motors["legrest_height"].range, 78)
|
||||
|
||||
Reference in New Issue
Block a user