Merge branch 'develop' into feat/e-bike
🔍 Lint / 🪄 Check lint (push) Has been cancelled
🔍 Lint / 🎨 Check format (push) Has been cancelled
🔍 Lint / 🔎 Typecheck (push) Has been cancelled
📊 Quality / 🔒 Security Audit (push) Has been cancelled
📊 Quality / 📋 Dependency Freshness (push) Has been cancelled
📊 Quality / 📦 Bundle Size (push) Has been cancelled
🔍 Lint / 🏗 Build (push) Has been cancelled

This commit is contained in:
math-pixel
2026-06-02 20:36:04 +02:00
83 changed files with 866 additions and 680 deletions
+1 -1
View File
@@ -135,7 +135,7 @@ export const galleryModels: GalleryModel[] = [
},
{ id: "potager", name: "Potager", path: "/models/potager/potager.gltf" },
{ id: "puce", name: "Puce", path: "/models/puce/model.gltf" },
{ id: "pylone", name: "Pylône", path: "/models/pylone/model.gltf" },
{ id: "pylone", name: "Pylône", path: "/models/pylone/model.glb" },
{
id: "refroidisseur",
name: "Refroidisseur",
+48 -1
View File
@@ -4,7 +4,7 @@ export const REPAIR_CASE_MODEL_PATH = "/models/packderelance/model.gltf";
export const REPAIR_CASE_OPEN_SOUND_PATH = "/sounds/effect/open-malette.mp3";
export const REPAIR_CASE_CLOSE_SOUND_PATH = "/sounds/effect/close-malette.mp3";
export const REPAIR_CASE_LID_NODE_NAME = "partiesup";
export const REPAIR_CASE_LID_NODE_NAME = "partsup";
export const REPAIR_CASE_CLOSED_ROTATION_OFFSET_DEGREES = 0;
export const REPAIR_CASE_OPEN_ROTATION_OFFSET_DEGREES = 115;
export const REPAIR_CASE_ANIMATION_DURATION = 0.8;
@@ -27,3 +27,50 @@ export const REPAIR_CASE_FOCUS_SCALE = 2.25;
export const REPAIR_CASE_PLACEHOLDER_NAME_PREFIX = "placeholder_";
export const REPAIR_CASE_PLACEHOLDER_SNAP_RADIUS = 0.65;
export const REPAIR_CASE_PLACEHOLDER_SNAP_DURATION = 0.25;
/**
* Names of nodes inside the packderelance GLTF where standalone part models
* are anchored (visually injected). The original meshes under these nodes are
* hidden at runtime so the standalone model takes their place.
*
* Some entries (e.g. `refroidisseur`) do not exist as nodes in the GLTF; an
* empty Object3D is created at mount time at the corresponding case-local
* fallback position so the anchoring pipeline is uniform.
*/
export const REPAIR_CASE_PART_ANCHOR_NAMES = [
"cabledroit",
"cablegauche",
"pucehaut",
"pucebas",
"refroidisseur",
] as const;
export type RepairCasePartAnchorName =
(typeof REPAIR_CASE_PART_ANCHOR_NAMES)[number];
/**
* Case-local positions used when an anchor node is missing from the GLTF.
* Values are expressed in the case model's local coordinate system (the case
* is rendered at small intrinsic scale; magnitudes are in the 0.01-0.25 range
* to match the existing nodes such as `cabledroit`).
*/
export const REPAIR_CASE_PART_ANCHOR_FALLBACKS: Record<
RepairCasePartAnchorName,
Vector3Tuple
> = {
cabledroit: [0.0087, 0.0139, 0.1921],
cablegauche: [0.0087, 0.0139, 0.2477],
pucehaut: [-0.0207, 0.009, -0.0479],
pucebas: [0.0987, 0.009, -0.0479],
refroidisseur: [0.05, 0.014, 0.05],
};
/**
* Quaternion applied to anchor nodes that are created at runtime (because
* the corresponding node is absent from the GLTF). Matches the rotation of
* the existing part nodes in packderelance to keep visual orientation
* consistent.
*/
export const REPAIR_CASE_PART_ANCHOR_FALLBACK_QUATERNION = [
0.7071068286895752, 0, 0, 0.7071068286895752,
] as const satisfies readonly [number, number, number, number];
+60 -29
View File
@@ -25,26 +25,48 @@ export const REPAIR_MISSIONS: Record<RepairMissionId, RepairMissionConfig> = {
interactUiPath: REPAIR_INTERACT_UI_PATH,
brokenUiPath: REPAIR_BROKEN_UI_PATH,
case: DEFAULT_REPAIR_CASE,
requiredReplacementPartId: "ebike-cooling-core-replacement",
requiredReplacementPartIds: ["ebike-cooling-core-replacement"],
brokenParts: [
{
id: "ebike-cooling-core",
label: "Cooling core",
modelPath: "/models/refroidisseur/model.gltf",
nodeName: "refroidisseur",
targetNodeName: "refroidisseur",
caseSlotName: "placeholder_1",
},
],
replacementParts: [
{
id: "ebike-cooling-core-replacement",
label: "Replacement cooling core",
label: "Refroidisseur",
modelPath: "/models/refroidisseur/model.gltf",
caseAnchor: "refroidisseur",
targetNodeName: "refroidisseur",
},
{
id: "ebike-glove-distractor",
label: "Insulation glove",
modelPath: "/models/gant_l/model.gltf",
id: "ebike-cable-right-distractor",
label: "Câble droit",
modelPath: "/models/cable1/model.gltf",
caseAnchor: "cabledroit",
},
{
id: "ebike-cable-left-distractor",
label: "Câble gauche",
modelPath: "/models/cable2/model.gltf",
caseAnchor: "cablegauche",
},
{
id: "ebike-puce-haut-distractor",
label: "Puce haute",
modelPath: "/models/puce/model.gltf",
caseAnchor: "pucehaut",
},
{
id: "ebike-puce-bas-distractor",
label: "Puce basse",
modelPath: "/models/puce/model.gltf",
caseAnchor: "pucebas",
},
],
},
@@ -53,43 +75,52 @@ export const REPAIR_MISSIONS: Record<RepairMissionId, RepairMissionConfig> = {
label: "Power pylon",
description:
"Restore the pylon lamp relay and damaged panel before reconnecting the grid",
modelPath: "/models/pylone/model.gltf",
modelPath: "/models/pylone/model.glb",
stageUiPath: "/assets/world/UI/pylon-mission-notification.webm",
interactUiPath: REPAIR_INTERACT_UI_PATH,
brokenUiPath: REPAIR_BROKEN_UI_PATH,
case: DEFAULT_REPAIR_CASE,
reassemblySeconds: 1.8,
requiredReplacementPartId: "pylon-grid-relay-replacement",
scanPartSeconds: 1.4,
brokenParts: [
{
id: "pylon-grid-relay",
label: "Grid relay",
nodeName: "lampe",
caseSlotName: "placeholder_1",
},
{
id: "pylon-damaged-panel",
label: "Damaged solar panel",
nodeName: "panneau2",
caseSlotName: "placeholder_2",
},
requiredReplacementPartIds: [
"pylon-cable-right-replacement",
"pylon-cable-left-replacement",
],
scanPartSeconds: 1.4,
brokenParts: [],
replacementParts: [
{
id: "pylon-grid-relay-replacement",
label: "Replacement grid relay",
modelPath: "/models/pylone/model.gltf",
id: "pylon-cable-right-replacement",
label: "Câble droit",
modelPath: "/models/cable1/model.gltf",
caseAnchor: "cabledroit",
caseLockGroup: "pylon-cable",
targetNodeName: "cable2",
},
{
id: "pylon-stone-distractor",
label: "Stone counterweight",
modelPath: "/models/galet/model.gltf",
id: "pylon-cable-left-replacement",
label: "Câble gauche",
modelPath: "/models/cable2/model.gltf",
caseAnchor: "cablegauche",
caseLockGroup: "pylon-cable",
targetNodeName: "cable2",
},
{
id: "pylon-cooling-distractor",
label: "Cooling core",
label: "Refroidisseur",
modelPath: "/models/refroidisseur/model.gltf",
caseAnchor: "refroidisseur",
},
{
id: "pylon-puce-haut-distractor",
label: "Puce haute",
modelPath: "/models/puce/model.gltf",
caseAnchor: "pucehaut",
},
{
id: "pylon-puce-bas-distractor",
label: "Puce basse",
modelPath: "/models/puce/model.gltf",
caseAnchor: "pucebas",
},
],
},
@@ -104,7 +135,7 @@ export const REPAIR_MISSIONS: Record<RepairMissionId, RepairMissionConfig> = {
brokenUiPath: REPAIR_BROKEN_UI_PATH,
case: DEFAULT_REPAIR_CASE,
reassemblySeconds: 1.2,
requiredReplacementPartId: "farm-irrigation-pump-replacement",
requiredReplacementPartIds: ["farm-irrigation-pump-replacement"],
scanPartSeconds: 0.9,
brokenParts: [
{
+19 -1
View File
@@ -1,5 +1,11 @@
export const HAND_TRACKING_FRAME_WIDTH = 320;
export const HAND_TRACKING_FRAME_HEIGHT = 240;
// The browser MediaPipe model (hand_landmarker.task float16) is more
// sensitive than the backend Python model and needs a higher-resolution
// frame to detect hands reliably. The backend keeps 320x240 because that
// is the JPEG payload size sent over the WebSocket.
export const HAND_TRACKING_BROWSER_CAMERA_WIDTH = 640;
export const HAND_TRACKING_BROWSER_CAMERA_HEIGHT = 480;
export const HAND_TRACKING_TARGET_FPS = 10;
export const HAND_TRACKING_JPEG_QUALITY = 0.55;
export const HAND_TRACKING_CAMERA_TIMEOUT_MS = 8_000;
@@ -8,9 +14,21 @@ export const HAND_TRACKING_BROWSER_WASM_URL =
"https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.35/wasm";
export const HAND_TRACKING_BROWSER_MODEL_URL =
"https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/1/hand_landmarker.task";
export const HAND_TRACKING_BROWSER_DELEGATE: "CPU" | "GPU" = "CPU";
export const HAND_TRACKING_BROWSER_DELEGATE: "CPU" | "GPU" = "GPU";
// Delay before the runtime actually starts after `enabled` flips to true.
// Absorbs React StrictMode's mount/unmount/mount cycle in dev and rapid
// `nearby` toggles at trigger borders. Invisible to the user (~5 frames).
export const HAND_TRACKING_RUNTIME_START_DELAY_MS = 80;
// How long the hand tracking stays active after the trigger condition
// (nearby / holding / repair step) turns off. Gives MediaPipe enough time
// to initialize webcam + model + first frame inference before we cleanup,
// so the user actually sees their hands when entering a zone briefly.
export const HAND_TRACKING_LINGER_MS = 2000;
// EMA weight applied to the latest landmark frame. Lower = smoother but
// laggier; higher = more responsive but more jitter from raw MediaPipe
// noise. 0.4 keeps the glove and grabbed objects from trembling without
// feeling sluggish.
export const HAND_TRACKING_LANDMARK_SMOOTHING = 0.4;
+1 -1
View File
@@ -9,7 +9,7 @@ export const MAP_INSTANCING_ASSETS = {
},
pylone: {
mapName: "pylone",
modelPath: "/models/pylone/model.gltf",
modelPath: "/models/pylone/model.glb",
scaleMultiplier: 1,
castShadow: true,
receiveShadow: true,