Render the exploded mission model during the repairing step so broken
nodes (e.g. ebike refroidisseur) stay visible in the world, and surface
their world positions to RepairRepairingStep so broken pieces spawn
from where they belong on the model rather than from a static offset.
- ExplodableModel: add hideNodeNames (mesh visibility off, restored on
unmount) and nodeAnchorNames + onNodeAnchorsChange (per-frame world
positions debounced via signature so React state updates only when
the values actually move).
- RepairGame: render ExplodableModel during 'repairing' with the broken
node names hidden + anchors forwarded; threads brokenAnchors state
to RepairRepairingStep.
- RepairScanSequence + RepairRepairingStep: propagate targetNodeName
through scanned and fallback broken-part lists.
- RepairRepairingStep: broken parts spawn at brokenAnchors[targetNodeName]
when set, falling back to legacy BROKEN_PART_START_OFFSETS otherwise.
- Update pylone path references from /models/pylone/model.gltf to .glb
(galleryModels, mapInstancingConfig, repairMissions). The new glb
exports a single cable node 'cable2' (no cable1).
- Pylon brokenParts: drop lampe and panneau2; the pylon design no longer
has detached broken pieces (the cable is just missing from the model).
- Pylon replacement cables target the pylon's 'cable2' node world position
via targetNodeName (used by the upcoming broken-anchor wiring).
- Ebike refroidisseur replacement and brokenPart both target the bike's
'refroidisseur' node so the broken piece spawns from its original
location and the replacement can install in the same slot.
When a replacement part with a caseLockGroup is grabbed, sibling parts
sharing the same group become non-interactable and ghosted (35% opacity)
until the held part is released. This implements the pylon cable choice
where the player picks either cable1 or cable2 (both valid) without
being able to grab both simultaneously.
- GrabbableObject: add disabled prop (skips interaction frame logic and
unmounts InteractableObject so it does not register with the manager)
and onGrabChange callback fired on press, release, hand grab, and hand
release. Force-releases when disabled becomes true mid-grab.
- SimpleModel: add opacity prop, traversed onto cloned mesh materials
(safe because cloneResources clones materials per instance).
- RepairObjectModel: forward ghosted prop as opacity 0.35.
- RepairRepairingStep: track heldPartByLockGroup and pass disabled +
ghosted to siblings of the currently held part.
- Ebike replacement parts: cooling core (correct, anchored at refroidisseur)
+ four distractors anchored at cabledroit/cablegauche/pucehaut/pucebas.
Removes the ad-hoc gant_l/talkie distractors in favor of consistent
case-anchored visuals.
- Pylon replacement parts: cable1 + cable2 (alternative correct, both with
caseLockGroup 'pylon-cable' for upcoming soft-lock) + refroidisseur and
two puce distractors anchored to packderelance.
- Farm replacement parts kept as-is (caseAnchor undefined falls back to
placeholder slot positions for backward compatibility).
- RepairGame threads anchors from RepairCaseModel through RepairMissionCase
to RepairRepairingStep; replacement-part initial position now resolves
to the anchor world position when caseAnchor is set, falling back to the
legacy slot index otherwise.
- RepairMissionConfig.requiredReplacementPartId (string) is replaced by
requiredReplacementPartIds (readonly string[]) so a mission can accept
several alternative correct parts (e.g. pylon will accept either cable).
- RepairMissionPartConfig gains optional caseAnchor (where the standalone
spawns inside packderelance), caseLockGroup (mutually exclusive parts),
and targetNodeName (snap onto a node of the broken model rather than a
placeholder slot in the case).
- RepairScannedBrokenPart gains targetNodeName so scan results can carry
this hint through to the repairing step.
- RepairRepairingStep validation logic (placed/wrong/feedback) now matches
any id in requiredReplacementPartIds. Existing data is migrated mechanically
(single-element arrays); part-level new fields are wired in subsequent
commits.
- Fix REPAIR_CASE_LID_NODE_NAME from "partiesup" to "partsup" (the actual
node name in packderelance.gltf), restoring the lid open/close animation
that was silently no-oping since introduction.
- Add REPAIR_CASE_PART_ANCHOR_NAMES (cabledroit, cablegauche, pucehaut,
pucebas, refroidisseur) and REPAIR_CASE_PART_ANCHOR_FALLBACKS for
case-local positions used when the GLTF lacks a node (refroidisseur).
- RepairCaseModel now resolves these anchor nodes on mount, hides existing
meshes underneath them, and creates lightweight Object3D placeholders for
missing names so the anchoring pipeline is uniform.
- Each frame, anchor world positions are converted to step-local space and
emitted via the new onAnchorsChange callback (debounced via signature like
placeholders). Consumers added in subsequent commits.
The browser MediaPipe model (hand_landmarker.task float16) was failing
to detect hands at 320x240 — MediaPipe ran for 10s straight without
ever returning a hand. Bumping the requested camera resolution to
640x480 (browser only — backend still ships 320x240 JPEGs over the
WebSocket) makes the model reliably pick hands up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(1) HandTrackingProvider always renders the same JSX root
(HandTrackingRuntime) so toggling `enabled` no longer remounts the
<Canvas> below — that remount was destroying the WebGL context every
time the player entered an interaction zone.
(2) Add HAND_TRACKING_LINGER_MS (2s) cooldown on `enabled` so brief
walk-throughs of a trigger zone don't tear down MediaPipe before it
has time to initialize the webcam + model + first frame (cold start
~800ms).
Resolves the WebGL context lost + respawn loop and restores visible
hand tracking in the backend runtime. Browser JS runtime detection
quality is a separate follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
In dev, <StrictMode> intentionally mounts → unmounts → remounts each
effect to surface non-idempotent code. The hand tracking hooks were
calling getUserMedia and creating MediaPipe / WebSocket runtimes on
every mount, which in practice ran the full start/stop/start cycle
inside a few milliseconds and pushed WebGL over its limit on top
of the loaded scene → context lost.
Add HAND_TRACKING_RUNTIME_START_DELAY_MS (80ms) and delay the actual
start() call behind a setTimeout in both useBrowserHandTracking and
useRemoteHandTracking. The cleanup clears the timer, so a fast
mount/unmount never reaches start(). 80ms is invisible to the user
(<5 frames at 60fps) and also absorbs rapid `nearby` toggles at
trigger borders.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Several mitigations against the WebGL context lost that fires when
hand tracking starts on a loaded scene:
- Canvas: fixed DPR [1,1], antialias off, scoped id="game-canvas",
context-lost handler releases MediaPipe and logs GPU memory counters
- optimizeGLTFScene: cap anisotropy at 2 and stop forcing mipmaps /
needsUpdate on every pass — avoids massive texture re-uploads
- MediaPipe: force CPU delegate (HAND_TRACKING_BROWSER_DELEGATE),
cache the landmarker instance, and expose releaseBrowserHandLandmarker
- useBrowserHandTracking / useRemoteHandTracking: idempotent cleanup
guarded by a cleanedUp flag, try/catch around the detect loop, and
release of the landmarker on stop
- World: mount HandTrackingGlove only when the matching hand is
actually present in the snapshot (status connected + hands.length > 0)
- HandTrackingGlove: drop the eager useGLTF.preload that was running
at startup whether or not hand tracking was used
Does not yet absorb the React StrictMode double-mount — that is the
follow-up commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Store the lil-gui Source controller so setHandTrackingSource() from
the settings menu can refresh its display, and log the transition
for traceability.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PointerLockControls now targets #game-canvas and respects the
settings menu, and document.exitPointerLock() only runs when a
pointer lock is actually active. The terrain ground snap in
PlayerController is gated on sceneMode === "game" so it doesn't
fight the physics test scene's flat floor.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lower grabbable spawn, expose GPS preview position/rotation as
constants, fix physics spawn to use PLAYER_EYE_HEIGHT, and silence
the console.log noise around waypoint loading in TestMap.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Solid character mesh does not need transparency; BLEND combined with doubleSided
caused depth-sort issues rendering inside-faces over front-faces (visible as
triangular orange artifacts on the face). Default OPAQUE single-sided is correct.
- Talkie does not need a LOD swap; remove the entry from
MAP_LOD_MODEL_PATHS so the same model.glb is used at any distance.
- The ebike repair distractors are meant to be other disassembled bike
parts, not random props. Drop the talkie radio distractor and keep
only the (thematically plausible) insulation glove alongside the
correct cooling core replacement.
The talkie folder now ships a single binary GLB; update the four call
sites (TalkieModel, gallery, two repair mission entries) to load
model.glb. The talkie-LOD path is unchanged.
The exported sapin-LOD and buisson-LOD models do not match their full
detail counterpart's baseline scale, so they appear oversized once the
LOD swap kicks in. Add MAP_LOD_SCALE_MULTIPLIERS keyed by map name
(sapin: 0.5, buisson: 0.8) and apply it on top of the chunk's existing
scaleMultiplier whenever the resolved model path is the LOD variant.
- Bump high to 30m chunk / HD<20m and ultra to HD<30m so HD models
persist further before swapping.
- Render the 5 graphics preset cards on a single row.
- Vegetation LOD selection now uses the distance to the nearest instance
in each chunk instead of the chunk centre, matching MapInstancingSystem
and avoiding premature LOD swaps when the camera enters a chunk.
Register the three new vegetation LOD models in MAP_LOD_MODEL_PATHS and
extend VegetationSystem with per-chunk distance-based LOD selection
(mirroring MapInstancingSystem). Chunk model paths are re-evaluated on
the existing CHUNK_CONFIG.updateInterval cadence and Suspense keys
include the resolved path so a swap unmounts the previous instanced mesh
cleanly.
Switching alphaMode from BLEND to MASK with a 0.5 cutoff lets the lampe
material participate in instanced shadow rendering correctly. BLEND is
known to interact badly with InstancedMesh shadow casting, producing
incorrect or missing shadows on the pylone lampe.
Restore ultra to its original behaviour (50m chunk streaming, HD within
20m, no fog) and introduce a new max preset that disables chunk streaming
entirely (loads all chunks unconditionally) and pushes the HD/LOD swap
distance to 50m. Add chunkStreamingEnabled flag to GraphicsPresetConfig
so the streaming gate honours the preset. The settings card label shows
'All' when streaming is off.
Shadows occasionally failed to render on initial load and the Fabrik
doorway sometimes blocked the player. Both issues are tracked down to
geometry that mounts after Lighting:
- Shadows: GLTFs and the merged static map mount imperatively after
Lighting, so materials get compiled against a renderer state that
pre-dates the final scene and bake a 'no shadow map' permutation,
silently dropping shadows. A WebGL context-restore cycle fixes it,
but is too invasive. New 'useShadowMapWarmup' hook replays it
cheaply: once the scene mesh count has been stable for ~1s, it
disposes the directional shadow map (three.js reallocates it on
the next render) and marks every material 'needsUpdate' so shaders
rebind to the freshly created shadow sampler.
- Doorway: the door slab + its Solidify-modifier frame (children of
the 'Thicken' parent in the LaFabrik GLTF) sat inside the doorway
AABB and prevented the player from walking through. Stripped from
the collision octree alongside the existing 'porte' slab; visual
rendering is unaffected.
Also: extract sun-relative-to-camera placement into a small helper,
remove the temporary diagnostic logs, and document the shadow warmup
in three-debugging.md.
- Lighting: replace single-frame needsUpdate with a 3-rAF warmup that forces
scene.updateMatrixWorld + sun.shadow.needsUpdate + gl.shadowMap.needsUpdate.
This restores the SceneShadowWarmup behaviour (deleted in 777e51e) inline,
so shadows survive Physics Suspense remounts and webglcontextrestored.
- octreeCollisionConfig: remove (comment out) the thin LA_FABRIK interior box
at x=-6.93 that was sealing the doorway despite the mesh hole; fabrik mesh
octree already provides surrounding wall collision.
- DebugOctreeVisualization: add Fabrik-only filter to inspect interior
collisions/non-collisions in isolation.
The previous talkie-overlay refactor lost the rest/active behaviour and
left the radio-shake CSS animation running constantly. Restore the
intended polish:
- Position lerp between TALKIE_REST_Y (idle) and TALKIE_ACTIVE_Y
(raised when narrator is speaking) with a subtle floating bob.
- Subtle z-axis float at idle, faster shake when active.
- Gate the CSS radio-shake on the new --active modifier so the talkie
is calm when no narrator dialogue is playing.
- Keep the face-camera rotation [0.18, PI, -0.08] from the original
overlay version.
Visibility still kicks in at the reveal step (no regression on the
recent fix).