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

7.8 KiB
Raw Blame History

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.
{ "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:

<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 ??=.

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):

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:

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

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:

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.