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>
This commit is contained in:
knsoandClaude Opus 5 committed 2026-09-14 19:22:20 +02:00
1 parent 7e4af1d0bc
commit 6a056262a8
6 files changed
+560

No files matched your search

@@ -0,0 +1,123 @@
# 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.