This commit is contained in:
vee
2026-07-30 18:55:28 +08:00
parent 49f5c49c53
commit 8cda026ac2
22 changed files with 315 additions and 86 deletions
+7
View File
@@ -0,0 +1,7 @@
.venv/
.env
.env.auth
__pycache__/
*.py[cod]
artifacts/runs/
tmp/*.log
+205 -28
View File
@@ -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) {
+4 -16
View File
@@ -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>
+4 -1
View File
@@ -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;
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+4 -4
View File
@@ -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: 座椅靠背
+7 -5
View File
@@ -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},
+8
View File
@@ -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"],
+42 -1
View File
@@ -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)
+4 -1
View File
@@ -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)