优化
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,
|
result: null,
|
||||||
progressTimer: null,
|
progressTimer: null,
|
||||||
seatTwin: null,
|
seatTwin: null,
|
||||||
|
seatActuatorState: {},
|
||||||
|
availableSeatAxes: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const $ = (selector, root = document) => root.querySelector(selector);
|
const $ = (selector, root = document) => root.querySelector(selector);
|
||||||
@@ -1629,36 +1631,83 @@ function classifyReferenceMaterial(material) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const TWIN_OUTLINE_STYLE = {
|
const TWIN_OUTLINE_STYLE = {
|
||||||
body: { color: 0x315d69, lineOpacity: 0.52, fillOpacity: 0.014, renderOrder: 30 },
|
body: {
|
||||||
glass: { color: 0x78a7b2, lineOpacity: 0.18, fillOpacity: 0.006, renderOrder: 31 },
|
color: 0x315d69,
|
||||||
detail: { color: 0x536f76, lineOpacity: 0.28, fillOpacity: 0.009, renderOrder: 32 },
|
lineOpacity: 0.36,
|
||||||
wheel: { color: 0x203d45, lineOpacity: 0.68, fillOpacity: 0.02, renderOrder: 33 },
|
occludedColor: 0xa7b8bb,
|
||||||
interior: { color: 0x607e83, lineOpacity: 0.24, fillOpacity: 0.008, renderOrder: 36 },
|
occludedLineOpacity: 0.12,
|
||||||
light: { color: 0x536f76, lineOpacity: 0.28, fillOpacity: 0.009, renderOrder: 32 },
|
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) {
|
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);
|
const geometry = new THREE.EdgesGeometry(mesh.geometry, thresholdAngle);
|
||||||
if (!geometry.getAttribute("position")?.count) {
|
if (!geometry.getAttribute("position")?.count) {
|
||||||
geometry.dispose();
|
geometry.dispose();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const material = new THREE.LineBasicMaterial({
|
const visibleMaterial = new THREE.LineBasicMaterial({
|
||||||
color: style.color,
|
color: style.color,
|
||||||
transparent: true,
|
transparent: true,
|
||||||
opacity: style.lineOpacity,
|
opacity: style.lineOpacity,
|
||||||
depthWrite: false,
|
depthWrite: false,
|
||||||
depthTest: true,
|
depthTest: true,
|
||||||
|
depthFunc: THREE.LessEqualDepth,
|
||||||
toneMapped: false,
|
toneMapped: false,
|
||||||
});
|
});
|
||||||
const outline = new THREE.LineSegments(geometry, material);
|
visibleMaterial.onBeforeCompile = (shader) => {
|
||||||
outline.name = `${mesh.name || "mesh"}_outline`;
|
shader.vertexShader = shader.vertexShader.replace(
|
||||||
outline.renderOrder = style.renderOrder;
|
"#include <project_vertex>",
|
||||||
outline.frustumCulled = mesh.frustumCulled;
|
"#include <project_vertex>\ngl_Position.z -= 0.0006 * gl_Position.w;",
|
||||||
mesh.add(outline);
|
);
|
||||||
mesh.userData.twinOutline = outline;
|
};
|
||||||
return outline;
|
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 = {}) {
|
function applyTwinOutlineStyle(THREE, root, options = {}) {
|
||||||
@@ -1694,6 +1743,9 @@ function configureTwinSolidEntity(THREE, roots, options = {}) {
|
|||||||
root.traverse((object) => {
|
root.traverse((object) => {
|
||||||
if (!object.isMesh || !object.visible) return;
|
if (!object.isMesh || !object.visible) return;
|
||||||
if (object.userData.twinOutline) object.userData.twinOutline.visible = false;
|
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];
|
const materials = Array.isArray(object.material) ? object.material : [object.material];
|
||||||
materials.filter(Boolean).forEach((material) => {
|
materials.filter(Boolean).forEach((material) => {
|
||||||
material.transparent = Boolean(options.transparent);
|
material.transparent = Boolean(options.transparent);
|
||||||
@@ -1792,12 +1844,20 @@ function configureReferenceVehicleModel(THREE, vehicle) {
|
|||||||
const material = sourceMaterial.clone();
|
const material = sourceMaterial.clone();
|
||||||
const descriptor = referenceMaterialDescriptor(material);
|
const descriptor = referenceMaterialDescriptor(material);
|
||||||
const category = classifyReferenceMaterial(material);
|
const category = classifyReferenceMaterial(material);
|
||||||
|
const outlineStyle = TWIN_OUTLINE_STYLE[category] || TWIN_OUTLINE_STYLE.detail;
|
||||||
const key = `${category}:${material.name || "unnamed"}`;
|
const key = `${category}:${material.name || "unnamed"}`;
|
||||||
report.materials.set(key, (report.materials.get(key) || 0) + 1);
|
report.materials.set(key, (report.materials.get(key) || 0) + 1);
|
||||||
if (material.map) {
|
[
|
||||||
material.map.colorSpace = THREE.SRGBColorSpace;
|
"map",
|
||||||
material.map.anisotropy = 8;
|
"aoMap",
|
||||||
}
|
"bumpMap",
|
||||||
|
"normalMap",
|
||||||
|
"roughnessMap",
|
||||||
|
"metalnessMap",
|
||||||
|
"emissiveMap",
|
||||||
|
].forEach((textureKey) => {
|
||||||
|
if (textureKey in material) material[textureKey] = null;
|
||||||
|
});
|
||||||
if (material.emissive) {
|
if (material.emissive) {
|
||||||
material.emissive.setHex(0x000000);
|
material.emissive.setHex(0x000000);
|
||||||
if ("emissiveIntensity" in material) material.emissiveIntensity = 0;
|
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) {
|
} else if (/chrome|metal|alloy|rim/.test(descriptor) && material.color) {
|
||||||
material.color.setHex(0xaeb9bb);
|
material.color.setHex(0xaeb9bb);
|
||||||
}
|
}
|
||||||
const outlineStyle = TWIN_OUTLINE_STYLE[category] || TWIN_OUTLINE_STYLE.detail;
|
|
||||||
material.transparent = true;
|
material.transparent = true;
|
||||||
material.opacity = outlineStyle.fillOpacity;
|
material.opacity = outlineStyle.fillOpacity;
|
||||||
material.depthWrite = false;
|
material.depthWrite = ["body", "detail", "wheel"].includes(category);
|
||||||
material.side = THREE.DoubleSide;
|
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 (material.emissive) material.emissive.setHex(0x000000);
|
||||||
if ("emissiveIntensity" in material) material.emissiveIntensity = 0;
|
if ("emissiveIntensity" in material) material.emissiveIntensity = 0;
|
||||||
material.needsUpdate = true;
|
material.needsUpdate = true;
|
||||||
@@ -1851,7 +1915,12 @@ function configureReferenceVehicleModel(THREE, vehicle) {
|
|||||||
const outlineCategory = categoryPriority.find((category) => categories.includes(category)) || "detail";
|
const outlineCategory = categoryPriority.find((category) => categories.includes(category)) || "detail";
|
||||||
const outlineStyle = TWIN_OUTLINE_STYLE[outlineCategory];
|
const outlineStyle = TWIN_OUTLINE_STYLE[outlineCategory];
|
||||||
object.renderOrder = outlineStyle.renderOrder - 1;
|
object.renderOrder = outlineStyle.renderOrder - 1;
|
||||||
addTwinMeshOutline(THREE, object, outlineStyle, 30);
|
addTwinMeshOutline(
|
||||||
|
THREE,
|
||||||
|
object,
|
||||||
|
outlineStyle,
|
||||||
|
outlineStyle.thresholdAngle ?? 60,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
return report;
|
return report;
|
||||||
}
|
}
|
||||||
@@ -2048,6 +2117,7 @@ function initSeatTwin() {
|
|||||||
|
|
||||||
const THREE = window.THREE;
|
const THREE = window.THREE;
|
||||||
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, powerPreference: "high-performance" });
|
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, powerPreference: "high-performance" });
|
||||||
|
renderer.autoClear = false;
|
||||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
||||||
if ("outputColorSpace" in renderer) renderer.outputColorSpace = THREE.SRGBColorSpace;
|
if ("outputColorSpace" in renderer) renderer.outputColorSpace = THREE.SRGBColorSpace;
|
||||||
else renderer.outputEncoding = THREE.sRGBEncoding;
|
else renderer.outputEncoding = THREE.sRGBEncoding;
|
||||||
@@ -2058,8 +2128,11 @@ function initSeatTwin() {
|
|||||||
scene.background = new THREE.Color(0xe8eeec);
|
scene.background = new THREE.Color(0xe8eeec);
|
||||||
scene.fog = new THREE.Fog(0xe8eeec, 12, 19);
|
scene.fog = new THREE.Fog(0xe8eeec, 12, 19);
|
||||||
const camera = new THREE.PerspectiveCamera(34, 1, 0.1, 40);
|
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);
|
const keyLight = new THREE.DirectionalLight(0xffffff, 3.2);
|
||||||
|
keyLight.layers.enable(1);
|
||||||
keyLight.position.set(-4, 7, 6);
|
keyLight.position.set(-4, 7, 6);
|
||||||
keyLight.castShadow = true;
|
keyLight.castShadow = true;
|
||||||
keyLight.shadow.mapSize.set(1024, 1024);
|
keyLight.shadow.mapSize.set(1024, 1024);
|
||||||
@@ -2069,6 +2142,7 @@ function initSeatTwin() {
|
|||||||
keyLight.shadow.camera.bottom = -4;
|
keyLight.shadow.camera.bottom = -4;
|
||||||
scene.add(keyLight);
|
scene.add(keyLight);
|
||||||
const fillLight = new THREE.DirectionalLight(0x9fc7d7, 1.15);
|
const fillLight = new THREE.DirectionalLight(0x9fc7d7, 1.15);
|
||||||
|
fillLight.layers.enable(1);
|
||||||
fillLight.position.set(5, 3, -5);
|
fillLight.position.set(5, 3, -5);
|
||||||
scene.add(fillLight);
|
scene.add(fillLight);
|
||||||
|
|
||||||
@@ -2148,6 +2222,14 @@ function initSeatTwin() {
|
|||||||
});
|
});
|
||||||
scene.add(fixedModels.driver.root);
|
scene.add(fixedModels.driver.root);
|
||||||
vehicleSystems.root.add(fixedModels.rear);
|
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 = () => ({
|
const initialDevice = () => ({
|
||||||
ventilation: { state: "off", level: 0 },
|
ventilation: { state: "off", level: 0 },
|
||||||
@@ -2300,6 +2382,7 @@ function initSeatTwin() {
|
|||||||
const anchorX = (projectedPosition.x * 0.5 + 0.5) * width;
|
const anchorX = (projectedPosition.x * 0.5 + 0.5) * width;
|
||||||
const anchorY = (-projectedPosition.y * 0.5 + 0.5) * height;
|
const anchorY = (-projectedPosition.y * 0.5 + 0.5) * height;
|
||||||
const offset = {
|
const offset = {
|
||||||
|
longitudinal: { x: -104, y: 50 },
|
||||||
backrest_recline: { x: -108, y: -28 },
|
backrest_recline: { x: -108, y: -28 },
|
||||||
cushion_tilt: { x: 106, y: -4 },
|
cushion_tilt: { x: 106, y: -4 },
|
||||||
legrest_height: { x: 112, y: 52 },
|
legrest_height: { x: 112, y: 52 },
|
||||||
@@ -2388,7 +2471,16 @@ function initSeatTwin() {
|
|||||||
);
|
);
|
||||||
camera.lookAt(cameraTargetX, 1.14, -0.08);
|
camera.lookAt(cameraTargetX, 1.14, -0.08);
|
||||||
positionTwinCallouts();
|
positionTwinCallouts();
|
||||||
|
renderer.clear();
|
||||||
|
camera.layers.set(0);
|
||||||
renderer.render(scene, camera);
|
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";
|
if (!canvas.dataset.rendered) canvas.dataset.rendered = "true";
|
||||||
window.requestAnimationFrame(animate);
|
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 } = {}) {
|
function selectVehicle(vehicleId, { silent = false } = {}) {
|
||||||
const vehicle = findVehicleProfile(vehicleId);
|
const vehicle = findVehicleProfile(vehicleId);
|
||||||
if (!vehicle) return;
|
if (!vehicle) return;
|
||||||
@@ -2468,6 +2612,8 @@ function selectVehicle(vehicleId, { silent = false } = {}) {
|
|||||||
const select = $("#vehicleSelect");
|
const select = $("#vehicleSelect");
|
||||||
if (select) select.value = vehicle.id;
|
if (select) select.value = vehicle.id;
|
||||||
applyTwinAngleRanges(vehicle);
|
applyTwinAngleRanges(vehicle);
|
||||||
|
renderSeatMotorRows(vehicle);
|
||||||
|
applySeatMotorAvailability(vehicle);
|
||||||
const longitudinal = vehicle.seat?.motors?.longitudinal;
|
const longitudinal = vehicle.seat?.motors?.longitudinal;
|
||||||
const backrest = vehicle.seat?.motors?.backrest_recline;
|
const backrest = vehicle.seat?.motors?.backrest_recline;
|
||||||
$("#vehicleNote").textContent = longitudinal && backrest
|
$("#vehicleNote").textContent = longitudinal && backrest
|
||||||
@@ -2476,7 +2622,7 @@ function selectVehicle(vehicleId, { silent = false } = {}) {
|
|||||||
if (state.result?.actuator_state) {
|
if (state.result?.actuator_state) {
|
||||||
renderSeatState(state.result.actuator_state);
|
renderSeatState(state.result.actuator_state);
|
||||||
} else if (state.seatTwin?.actuatorState) {
|
} else if (state.seatTwin?.actuatorState) {
|
||||||
updateSeatTwin(state.seatTwin.actuatorState);
|
renderSeatState(state.seatTwin.actuatorState);
|
||||||
}
|
}
|
||||||
if (!silent) showToast(`已切换车型:${vehicle.name}`);
|
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) {
|
function renderSeatState(actuatorState) {
|
||||||
resetSeatState();
|
|
||||||
if (!actuatorState) return;
|
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]) => {
|
Object.entries(functions).forEach(([feature, value]) => {
|
||||||
if (feature === "position") {
|
if (feature === "position") {
|
||||||
Object.entries(value).forEach(([axis, motion]) => {
|
Object.entries(value).forEach(([axis, motion]) => {
|
||||||
@@ -2793,7 +2970,7 @@ function renderSeatState(actuatorState) {
|
|||||||
$("b", item).textContent = value.state === "on" ? `ON · L${value.level}` : "OFF";
|
$("b", item).textContent = value.state === "on" ? `ON · L${value.level}` : "OFF";
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
updateSeatTwin(actuatorState);
|
updateSeatTwin(mergedActuatorState);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderDomainState(domainState) {
|
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/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/libs/fflate.min.js" defer></script>
|
||||||
<script src="https://unpkg.com/three@0.147.0/examples/js/loaders/FBXLoader.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>
|
<script type="module" src="/gesture-control.js?v=gesture-core-3"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -289,7 +289,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="twin-stage" id="seatTwin" data-selected-seat="passenger">
|
<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="整车孪生系统状态">
|
<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="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>
|
<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="ventilation"><i data-lucide="wind"></i>通风 <b>--</b></span>
|
||||||
<span data-function="heating"><i data-lucide="flame"></i>加热 <b>--</b></span>
|
<span data-function="heating"><i data-lucide="flame"></i>加热 <b>--</b></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="motion-state-grid">
|
<div class="motion-state-grid" aria-live="polite"></div>
|
||||||
<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>
|
|
||||||
</article>
|
</article>
|
||||||
<article class="seat-card" data-seat="passenger">
|
<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>
|
<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="ventilation"><i data-lucide="wind"></i>通风 <b>--</b></span>
|
||||||
<span data-function="heating"><i data-lucide="flame"></i>加热 <b>--</b></span>
|
<span data-function="heating"><i data-lucide="flame"></i>加热 <b>--</b></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="motion-state-grid">
|
<div class="motion-state-grid" aria-live="polite"></div>
|
||||||
<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>
|
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3015,11 +3015,14 @@ pre {
|
|||||||
bottom: 18px;
|
bottom: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.joint-twin-shell [data-callout="longitudinal"],
|
|
||||||
.joint-twin-shell [data-callout="legrest_extension"] {
|
.joint-twin-shell [data-callout="legrest_extension"] {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.seat-axis-unavailable {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
.joint-twin-shell .twin-callout {
|
.joint-twin-shell .twin-callout {
|
||||||
min-width: 67px;
|
min-width: 67px;
|
||||||
padding: 5px 6px;
|
padding: 5px 6px;
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ operations:
|
|||||||
target_scope: passenger
|
target_scope: passenger
|
||||||
operation: set
|
operation: set
|
||||||
parameters:
|
parameters:
|
||||||
position_pct: 19
|
position_pct: 81
|
||||||
reason: 宝宝午睡模式水平电机目标 40mm,按腾势 N7 215mm 行程换算。
|
reason: 宝宝午睡模式水平电机目标 175mm,按腾势 N7 215mm 行程换算。
|
||||||
- domain: seat
|
- domain: seat
|
||||||
capability: seat.set_cushion_tilt
|
capability: seat.set_cushion_tilt
|
||||||
target_scope: passenger
|
target_scope: passenger
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ operations:
|
|||||||
target_scope: passenger
|
target_scope: passenger
|
||||||
operation: set
|
operation: set
|
||||||
parameters:
|
parameters:
|
||||||
position_pct: 49
|
position_pct: 51
|
||||||
reason: 小腿解压悬浮模式水平电机目标 105mm,按腾势 N7 215mm 行程换算。
|
reason: 小腿解压悬浮模式水平电机目标 110mm,按腾势 N7 215mm 行程换算。
|
||||||
- domain: seat
|
- domain: seat
|
||||||
capability: seat.set_cushion_tilt
|
capability: seat.set_cushion_tilt
|
||||||
target_scope: passenger
|
target_scope: passenger
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ operations:
|
|||||||
target_scope: passenger
|
target_scope: passenger
|
||||||
operation: set
|
operation: set
|
||||||
parameters:
|
parameters:
|
||||||
position_pct: 49
|
position_pct: 51
|
||||||
reason: 巡航沙发模式水平电机目标 105mm,按腾势 N7 215mm 行程换算。
|
reason: 巡航沙发模式水平电机目标 110mm,按腾势 N7 215mm 行程换算。
|
||||||
- domain: seat
|
- domain: seat
|
||||||
capability: seat.set_cushion_tilt
|
capability: seat.set_cushion_tilt
|
||||||
target_scope: passenger
|
target_scope: passenger
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ operations:
|
|||||||
target_scope: passenger
|
target_scope: passenger
|
||||||
operation: set
|
operation: set
|
||||||
parameters:
|
parameters:
|
||||||
position_pct: 0
|
position_pct: 100
|
||||||
reason: 轻松进出模式上下车水平电机目标 0mm。
|
reason: 轻松进出模式上下车水平电机目标 215mm。
|
||||||
- domain: seat
|
- domain: seat
|
||||||
capability: seat.set_cushion_tilt
|
capability: seat.set_cushion_tilt
|
||||||
target_scope: passenger
|
target_scope: passenger
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ operations:
|
|||||||
target_scope: passenger
|
target_scope: passenger
|
||||||
operation: set
|
operation: set
|
||||||
parameters:
|
parameters:
|
||||||
position_pct: 0
|
position_pct: 100
|
||||||
reason: 电感沙滩椅水平电机目标 0mm。
|
reason: 电感沙滩椅水平电机目标 215mm。
|
||||||
- domain: seat
|
- domain: seat
|
||||||
capability: seat.set_cushion_tilt
|
capability: seat.set_cushion_tilt
|
||||||
target_scope: passenger
|
target_scope: passenger
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ operations:
|
|||||||
target_scope: passenger
|
target_scope: passenger
|
||||||
operation: set
|
operation: set
|
||||||
parameters:
|
parameters:
|
||||||
position_pct: 49
|
position_pct: 51
|
||||||
reason: 休闲躺椅模式水平电机目标 105mm,按腾势 N7 215mm 行程换算。
|
reason: 休闲躺椅模式水平电机目标 110mm,按腾势 N7 215mm 行程换算。
|
||||||
- domain: seat
|
- domain: seat
|
||||||
capability: seat.set_cushion_tilt
|
capability: seat.set_cushion_tilt
|
||||||
target_scope: passenger
|
target_scope: passenger
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ operations:
|
|||||||
target_scope: passenger
|
target_scope: passenger
|
||||||
operation: set
|
operation: set
|
||||||
parameters:
|
parameters:
|
||||||
position_pct: 0
|
position_pct: 100
|
||||||
reason: 冥想空间模式水平电机目标 0mm。
|
reason: 冥想空间模式水平电机目标 215mm。
|
||||||
- domain: seat
|
- domain: seat
|
||||||
capability: seat.set_cushion_tilt
|
capability: seat.set_cushion_tilt
|
||||||
target_scope: passenger
|
target_scope: passenger
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ operations:
|
|||||||
target_scope: passenger
|
target_scope: passenger
|
||||||
operation: set
|
operation: set
|
||||||
parameters:
|
parameters:
|
||||||
position_pct: 100
|
position_pct: 0
|
||||||
reason: 小憩模式水平电机目标 215mm,按腾势 N7 215mm 行程换算。
|
reason: 小憩模式水平电机目标 0mm,按腾势 N7 215mm 行程换算。
|
||||||
- domain: seat
|
- domain: seat
|
||||||
capability: seat.set_cushion_tilt
|
capability: seat.set_cushion_tilt
|
||||||
target_scope: passenger
|
target_scope: passenger
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ operations:
|
|||||||
target_scope: passenger
|
target_scope: passenger
|
||||||
operation: set
|
operation: set
|
||||||
parameters:
|
parameters:
|
||||||
position_pct: 28
|
position_pct: 72
|
||||||
reason: 办公模式水平电机目标 60mm,按腾势 N7 215mm 行程换算。
|
reason: 办公模式水平电机目标 155mm,按腾势 N7 215mm 行程换算。
|
||||||
- domain: seat
|
- domain: seat
|
||||||
capability: seat.set_cushion_tilt
|
capability: seat.set_cushion_tilt
|
||||||
target_scope: passenger
|
target_scope: passenger
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ operations:
|
|||||||
target_scope: passenger
|
target_scope: passenger
|
||||||
operation: set
|
operation: set
|
||||||
parameters:
|
parameters:
|
||||||
position_pct: 28
|
position_pct: 72
|
||||||
reason: 女王副驾水平电机目标 60mm,按腾势 N7 215mm 行程换算。
|
reason: 女王副驾水平电机目标 155mm,按腾势 N7 215mm 行程换算。
|
||||||
- domain: seat
|
- domain: seat
|
||||||
capability: seat.set_cushion_tilt
|
capability: seat.set_cushion_tilt
|
||||||
target_scope: passenger
|
target_scope: passenger
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ operations:
|
|||||||
target_scope: passenger
|
target_scope: passenger
|
||||||
operation: set
|
operation: set
|
||||||
parameters:
|
parameters:
|
||||||
position_pct: 0
|
position_pct: 100
|
||||||
reason: 零重力模式水平电机目标 0mm,按腾势 N7 215mm 行程换算。
|
reason: 零重力模式水平电机目标 215mm,按腾势 N7 215mm 行程换算。
|
||||||
- domain: seat
|
- domain: seat
|
||||||
capability: seat.set_cushion_tilt
|
capability: seat.set_cushion_tilt
|
||||||
target_scope: passenger
|
target_scope: passenger
|
||||||
|
|||||||
@@ -6,15 +6,15 @@ seat:
|
|||||||
name: 水平
|
name: 水平
|
||||||
unit: mm
|
unit: mm
|
||||||
range: 215
|
range: 215
|
||||||
reference_zero: rearmost
|
reference_zero: forwardmost
|
||||||
positive_direction: forward
|
positive_direction: backward
|
||||||
hall_total_pulses: 480
|
hall_total_pulses: 480
|
||||||
cushion_tilt:
|
cushion_tilt:
|
||||||
name: 坐盆
|
name: 坐盆
|
||||||
unit: percent
|
unit: percent
|
||||||
range: 100
|
range: 100
|
||||||
reference_zero: lowest
|
reference_zero: highest
|
||||||
positive_direction: up
|
positive_direction: down
|
||||||
hall_total_pulses: 374
|
hall_total_pulses: 374
|
||||||
backrest_recline:
|
backrest_recline:
|
||||||
name: 座椅靠背
|
name: 座椅靠背
|
||||||
|
|||||||
@@ -63,9 +63,10 @@ spec:
|
|||||||
confirmation_policy: on_ambiguity
|
confirmation_policy: on_ambiguity
|
||||||
- name: seat.set_longitudinal_position
|
- name: seat.set_longitudinal_position
|
||||||
description: >-
|
description: >-
|
||||||
Set one front seat's fore-aft rail position. position_pct is an absolute target: 0 is the
|
Set one front seat's fore-aft rail position. For the selected Denza N7 profile, position_pct
|
||||||
rearmost mechanical limit and 100 is the forwardmost limit. Resolve relative requests such
|
is an absolute target: 0 is the forwardmost mechanical limit and 100 is the rearmost limit.
|
||||||
as "move back a little" against the current position supplied in runtime state. For a
|
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
|
vehicle-specific physical relative request, distance_mm may be supplied and the runtime
|
||||||
converts it to an absolute position_pct before CAN compilation.
|
converts it to an absolute position_pct before CAN compilation.
|
||||||
target_devices:
|
target_devices:
|
||||||
@@ -92,8 +93,9 @@ spec:
|
|||||||
confirmation_policy: on_ambiguity
|
confirmation_policy: on_ambiguity
|
||||||
- name: seat.set_cushion_tilt
|
- name: seat.set_cushion_tilt
|
||||||
description: >-
|
description: >-
|
||||||
Set the seat-cushion pitch for one front seat. position_pct is absolute: 0 places the front
|
Set the seat-cushion pitch for one front seat. For the selected Denza N7 profile,
|
||||||
edge at its lowest limit and 100 raises the front edge to its highest tilt limit.
|
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:
|
target_devices:
|
||||||
- seat_cushion_tilt_motor
|
- seat_cushion_tilt_motor
|
||||||
target_scopes:
|
target_scopes:
|
||||||
|
|||||||
@@ -335,7 +335,7 @@ class SeatServiceIntegrationTests(unittest.TestCase):
|
|||||||
"domain": "seat",
|
"domain": "seat",
|
||||||
"capability": "seat.set_longitudinal_position",
|
"capability": "seat.set_longitudinal_position",
|
||||||
"target_scope": "driver",
|
"target_scope": "driver",
|
||||||
"operation": "decrease",
|
"operation": "increase",
|
||||||
"parameters": [{"name": "position_pct", "value": position}],
|
"parameters": [{"name": "position_pct", "value": position}],
|
||||||
"reason": "Model resolved the relative request.",
|
"reason": "Model resolved the relative request.",
|
||||||
}
|
}
|
||||||
@@ -349,7 +349,7 @@ class SeatServiceIntegrationTests(unittest.TestCase):
|
|||||||
with patch.object(
|
with patch.object(
|
||||||
self.service.openai,
|
self.service.openai,
|
||||||
"interpret",
|
"interpret",
|
||||||
side_effect=[output(40), output(30)],
|
side_effect=[output(60), output(70)],
|
||||||
) as interpret:
|
) as interpret:
|
||||||
first = self.service.process(
|
first = self.service.process(
|
||||||
"主驾往后一点", "openai", "conv-motion-follow-up"
|
"主驾往后一点", "openai", "conv-motion-follow-up"
|
||||||
@@ -362,18 +362,18 @@ class SeatServiceIntegrationTests(unittest.TestCase):
|
|||||||
first["actuator_state"]["driver"]["position"]["longitudinal"][
|
first["actuator_state"]["driver"]["position"]["longitudinal"][
|
||||||
"position_pct"
|
"position_pct"
|
||||||
],
|
],
|
||||||
40,
|
60,
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
second["actuator_state"]["driver"]["position"]["longitudinal"][
|
second["actuator_state"]["driver"]["position"]["longitudinal"][
|
||||||
"position_pct"
|
"position_pct"
|
||||||
],
|
],
|
||||||
30,
|
70,
|
||||||
)
|
)
|
||||||
second_context = interpret.call_args_list[1].args[1]
|
second_context = interpret.call_args_list[1].args[1]
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
second_context["devices"]["driver"]["position"]["longitudinal"]["position_pct"],
|
second_context["devices"]["driver"]["position"]["longitudinal"]["position_pct"],
|
||||||
40,
|
60,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_model_relative_physical_motion_uses_vehicle_profile(self) -> None:
|
def test_model_relative_physical_motion_uses_vehicle_profile(self) -> None:
|
||||||
@@ -384,7 +384,7 @@ class SeatServiceIntegrationTests(unittest.TestCase):
|
|||||||
"domain": "seat",
|
"domain": "seat",
|
||||||
"capability": "seat.set_longitudinal_position",
|
"capability": "seat.set_longitudinal_position",
|
||||||
"target_scope": "driver",
|
"target_scope": "driver",
|
||||||
"operation": "increase",
|
"operation": "decrease",
|
||||||
"parameters": [{"name": "distance_mm", "value": 50}],
|
"parameters": [{"name": "distance_mm", "value": 50}],
|
||||||
"reason": "Move forward by a physical distance.",
|
"reason": "Move forward by a physical distance.",
|
||||||
},
|
},
|
||||||
@@ -414,7 +414,7 @@ class SeatServiceIntegrationTests(unittest.TestCase):
|
|||||||
self.assertEqual(result["status"], "succeeded")
|
self.assertEqual(result["status"], "succeeded")
|
||||||
self.assertEqual(result["vehicle"]["id"], "denza_n7")
|
self.assertEqual(result["vehicle"]["id"], "denza_n7")
|
||||||
operations = result["function_config"]["spec"]["operations"]
|
operations = result["function_config"]["spec"]["operations"]
|
||||||
self.assertEqual(operations[0]["parameters"], {"position_pct": 73})
|
self.assertEqual(operations[0]["parameters"], {"position_pct": 27})
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
operations[1]["parameters"],
|
operations[1]["parameters"],
|
||||||
{"position_pct": 38, "angle_deg": 116},
|
{"position_pct": 38, "angle_deg": 116},
|
||||||
|
|||||||
@@ -134,6 +134,14 @@ class SeatWebApiTests(unittest.TestCase):
|
|||||||
runtime["vehicle"]["seat"]["motors"]["longitudinal"]["range"],
|
runtime["vehicle"]["seat"]["motors"]["longitudinal"]["range"],
|
||||||
215,
|
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"]}
|
capability_names = {item["name"] for item in runtime["capabilities"]}
|
||||||
self.assertIn("seat.save_posture_preset", capability_names)
|
self.assertIn("seat.save_posture_preset", capability_names)
|
||||||
self.assertIn("seat.apply_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}
|
by_capability = {item["capability"]: item for item in operations}
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
by_capability["seat.set_longitudinal_position"]["parameters"],
|
by_capability["seat.set_longitudinal_position"]["parameters"],
|
||||||
{"position_pct": 0},
|
{"position_pct": 100},
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
by_capability["seat.set_cushion_tilt"]["parameters"],
|
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]["capability"], "seat.set_longitudinal_position")
|
||||||
self.assertEqual(requests[0]["target_scope"], "passenger")
|
self.assertEqual(requests[0]["target_scope"], "passenger")
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
{"name": "position_pct", "value": 0},
|
{"name": "position_pct", "value": 100},
|
||||||
requests[0]["parameters"],
|
requests[0]["parameters"],
|
||||||
)
|
)
|
||||||
by_capability = {request["capability"]: request for request in requests}
|
by_capability = {request["capability"]: request for request in requests}
|
||||||
@@ -88,6 +88,47 @@ class ModeProfileRegistryTests(unittest.TestCase):
|
|||||||
"calf_decompression_float",
|
"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:
|
def test_common_modes_are_visible_with_vehicle_specific_overrides(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as directory:
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
root = Path(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"].range, 215)
|
||||||
self.assertEqual(profile.seat_motors["longitudinal"].hall_total_pulses, 480)
|
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"].range, 68)
|
||||||
self.assertEqual(profile.seat_motors["backrest_recline"].hall_total_pulses, 917)
|
self.assertEqual(profile.seat_motors["backrest_recline"].hall_total_pulses, 917)
|
||||||
self.assertEqual(profile.seat_motors["legrest_height"].range, 78)
|
self.assertEqual(profile.seat_motors["legrest_height"].range, 78)
|
||||||
|
|||||||
Reference in New Issue
Block a user