Files
gta6/.claude/skills/placing-editor-models/browser-checks.md
T
knsoandClaude Opus 5 6a056262a8 chore(skills): Projekt-Skill zum Platzieren von three-d-stage-Modellen
Ablauf wie beim Hood Yard mit den gefundenen Fallstricken (Dreiecke je Knoten,
Bodenplatte +6 cm, globale Materiallisten, Asset-Test je Ort, launch.json nur im
Hauptordner, gl.finish-Messung, Vorwärmen prüfen) und Werkzeugen: Modell vermessen,
Kartendaten eines Weltfensters, Vergleichsbild Satellit/Spiel, Screenshot-Dekodierung,
erprobte Browser-Ausschnitte.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:22:20 +02:00

124 lines
7.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Browserprüfung eines Orts (verborgener Browser-Bereich)
Alle Ausschnitte sind am Hood Yard erprobt (2026-09-14). `javascript_tool` bricht nach 45 s ab: lange Abläufe auf mehrere Aufrufe verteilen. Jeder Aufruf hat eigene Variablen — was später gebraucht wird, an `window` hängen.
## 1. Dev-Server
- Das Vorschau-Werkzeug liest Server nur aus `.claude/launch.json` des **Hauptordners**. Auf 5173 läuft oft schon der Hauptordner.
- Arbeitet die Sitzung in einem Arbeitsbaum, darf sie diese Datei nicht ändern: Nutzer fragen, dann `ExitWorktree(keep)` → Eintrag setzen → `preview_start` → `EnterWorktree(path)`. Am Ende wieder raus und den Eintrag entfernen.
```json
{ "name": "place-check", "runtimeExecutable": "bash",
"runtimeArgs": ["-lc", "cd /Users/Kenso/Developer/gta6/.claude/worktrees/<name> && npx vite --port 5181 --strictPort"],
"port": 5181, "url": "http://localhost:5181" }
```
- Arbeitsbaum vorbereiten: `cp -Rc node_modules <baum>/node_modules`, dazu `three-player-controller/package.json` und `dist/` in `<baum>/three-player-controller/` kopieren. Git ignoriert Dateien im nicht ausgecheckten Submodul.
## 2. Debug-Seite (nie committen, danach löschen)
Der verborgene Bereich meldet oft 0 × 0 px, `requestAnimationFrame` steht, und three's `Timer` liefert delta 0, solange `document.hidden` gilt. Vor jedem Zug `resize_window` auf 1280 × 800 setzen (die App setzt es am Zugende zurück). `index-debug.html` im Root: Kopie von `index.html` mit diesem Skript im `<head>` vor dem Modul-Skript:
```html
<script>
(() => {
Object.defineProperty(document, "hidden", { configurable: true, get: () => false });
Object.defineProperty(document, "visibilityState", { configurable: true, get: () => "visible" });
const queue = new Map(); let nextId = 1;
window.requestAnimationFrame = (cb) => { const id = nextId++; queue.set(id, cb); return id; };
window.cancelAnimationFrame = (id) => { queue.delete(id); };
const worker = new Worker(URL.createObjectURL(new Blob(["setInterval(() => postMessage(0), 33);"], { type: "text/javascript" })));
worker.onmessage = () => { const cbs = [...queue.values()]; queue.clear(); const now = performance.now(); for (const cb of cbs) cb(now); };
})();
</script>
```
## 3. Szene, Kamera, Renderer greifbar machen
Wirft ein Hook, stirbt die Zeichenschleife endgültig. Vite lädt die Seite bei Abhängigkeits-Rebuilds neu (`performance.now()` klein), dann ist `window.__probe` weg — deshalb `??=`.
```js
const deps = performance.getEntriesByType("resource").map((e) => e.name);
const THREE = await import(deps.find((n) => n.includes("/node_modules/.vite/deps/three.js")));
window.__frames = [];
THREE.Scene.prototype.onBeforeRender = function (renderer, scene, camera) {
Object.assign((window.__probe ??= {}), { renderer, scene, camera });
window.__frames.push(performance.now());
if (window.__frames.length > 4000) window.__frames.splice(0, 2000);
if (window.__applyView) window.__applyView(camera);
};
```
## 4. Hinspringen über die Minimap
`M` per `keydown`/`keyup` (`code: "KeyM"`) auf `window`. Das Kartenbild bleibt im verborgenen Bereich bei „Karte lädt …“, bis ein **Screenshot** gemacht wurde. Danach klicken, mit `X`, `Y` = Welt-Ziel neben dem Ort (nicht auf ein Gebäude):
```js
const map = [...document.querySelectorAll("canvas")].find((c) => c !== window.__probe.renderer.domElement);
const rect = map.getBoundingClientRect(), dpr = map.width / rect.width;
const scale = Math.min(map.width / 3000, map.height / 3000);
const ox = (map.width - 3000 * scale) / 2, oy = (map.height - 3000 * scale) / 2;
const opts = { clientX: rect.left + ((X + 1000) * scale + ox) / dpr, clientY: rect.top + ((2500 - Y) * scale + oy) / dpr, button: 0, bubbles: true };
map.dispatchEvent(new MouseEvent("mousedown", opts)); map.dispatchEvent(new MouseEvent("mouseup", opts));
```
Die Wurzel des Orts findet man über ihre Lage: `scene.children.find((o) => Math.abs(o.position.x - at[0]) < 0.01 && Math.abs(o.position.z + at[1]) < 0.01)`.
## 5. Zustand prüfen
- **Kollisionsmarken:** Mit `root.traverse` die Meshes ohne `userData.excludeFromCollider` sammeln. Ihre Materialnamen müssen genau die festen Teile sein.
- **Quantisierung:** Alle festen Meshes haben `geometry.attributes.position.array instanceof Float32Array`, dafür sorgt `prepareModel`. Fehlt das, landet der Collider bei ±1 m um den Weltursprung.
- **Vorwärmen:** Vor dem Sprung ist der Ort noch nicht in der Szene. `renderer.info.memory.textures` liegt am Startpunkt trotzdem schon um die Texturzahl des Orts höher.
## 6. Blicke aufnehmen
Kamera übersteuern, auch in einem Wrapper um `CSM.prototype.update`, sonst fehlen die Schatten im Bild:
```js
const { CSM } = await import(deps.find((n) => n.includes("three_examples_jsm_csm_CSM")));
const eye = new THREE.Vector3(), target = new THREE.Vector3(), up = new THREE.Vector3(), m = new THREE.Matrix4();
window.__applyView = (camera) => {
if (!window.__view) return;
eye.fromArray(window.__view.position); target.fromArray(window.__view.target); up.fromArray(window.__view.up ?? [0, 1, 0]);
m.lookAt(eye, target, up).setPosition(eye);
camera.matrixWorld.copy(m); camera.matrixWorldInverse.copy(m).invert();
};
if (!CSM.prototype.__wrapped) {
const update = CSM.prototype.update;
CSM.prototype.update = function (...a) { window.__applyView(this.camera); return update.apply(this, a); };
CSM.prototype.__wrapped = true;
}
```
- **Blicke:** Augenhöhe von vorn und von hinten (y 1,7), dazu eine Draufsicht (`position [x, 60, z]`, `up [0, 0, -1]`, Norden oben). Pro Blick `window.__view` setzen und ~1 s warten.
- **Aufnehmen:** `renderer.render(scene, camera)`, auf ein 2D-Canvas mit 1280 × 800 zeichnen, `toDataURL("image/jpeg", 0.85)` zurückgeben.
- **Speichern:** Das Ergebnis ist zu groß und landet unter `tool-results/`. `scripts/decode-view.py <tool-result.txt> <bild.jpg>` holt das Bild heraus.
- **Aufräumen:** `window.__view = null`.
## 7. Kollision ablaufen
```js
Object.defineProperty(document, "pointerLockElement", { configurable: true, get: () => renderer.domElement });
const capsule = scene.getObjectByName("capsule");
const heading = () => { const d = camera.getWorldDirection(new THREE.Vector3()); return Math.atan2(d.x, -d.z) * 180 / Math.PI; }; // 0 = Norden
const turn = (deg) => window.dispatchEvent(new MouseEvent("mousemove", { movementX: deg / 8.6 * 300, movementY: 0 }));
Object.assign(window, { capsule, heading, turn });
```
- **Start:** Kapsel mit `capsule.position.set(x, 1, z)` auf freien Boden des Orts setzen, mit Blick auf eine Wand; der Weg darf nicht über Bordsteine führen. Drehen, `heading()` prüfen, dann `keydown` `KeyW`. Jede Sekunde die Lage notieren, danach `keyup`.
- **Stand:** Auf blankem Chunk-Boden steht die Kapsel bei y 0,75, auf der Bodenplatte des Orts um deren Oberkante höher (Hood Yard: 0,81).
- **Wand:** Die Kapsel muss mit wenigen cm Abstand vor der Wand stehen bleiben. Gras und Blätter muss sie ohne Halt durchlaufen.
- **Tempo:** etwa 0,38 m/s bei 33-ms-Takt, also genug Zeit geben (15–25 s).
## 8. Kosten messen
`renderer.info.render` zählt die Schattenstufen nicht mit, es wird nach `shadowMap.render` zurückgesetzt. Stattdessen takten, einmal mit und einmal ohne die Wurzel des Orts in der Szene:
```js
const gl = renderer.getContext();
const measure = (n) => { const t = []; for (let i = 0; i < n; i++) { gl.finish(); const s = performance.now(); renderer.render(scene, camera); gl.finish(); t.push(performance.now() - s); } t.sort((a, b) => a - b); return t[n >> 1]; };
```
- **Median:** Beim Hood Yard kosteten Blick vom Rand und Blick im Ort je +0,5 ms.
- **Ruckler beim Einsetzen:** Nach einem Sprung die größte Lücke in `window.__frames` rund um den Moment suchen, in dem der Ort in der Szene erscheint. Mit Vorwärmen waren das 86 ms bei 33-ms-Takt, ohne kamen allein ~130 ms für den Texturupload dazu.