Skip to content
Back to skills

Dayz P3d Inspector

ASecurity

Use when: inspect model, show me the p3d, view memory points, move points, recipe json, build p3d, round-trip p3d. Visual Recipe editor. Not structural/collision audits: dayz-p3d-audit; not textured PAA preview: dayz-3d-viewer.

  • 16 stars
  • 0 votes
  • 1 copy
  • 1 view
  • Added October 2, 2026
ai-agentspythongobashangulargitapidocumentation

Works with

  • cursor
  • cli
  • api

Security analysis

A96/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro scans all 6 files and shows the line behind each finding

Scanned October 3, 2026

npx -y skills add willy92wins/DayZ-Modding-Knowledge-Pack --skill dayz-p3d-inspector --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Dayz P3d Inspector?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Dayz P3d Inspector
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/willy92wins-dayz-p3d-inspector/badge)](https://www.skillsdirectory.com/skills/willy92wins-dayz-p3d-inspector)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: dayz-p3d-inspector
description: "Use when: inspect model, show me the p3d, view memory points, move points, recipe json, build p3d, round-trip p3d. Visual Recipe editor. Not structural/collision audits: dayz-p3d-audit; not textured PAA preview: dayz-3d-viewer."
---

# DayZ P3D Inspector — Interactive 3D Model Viewer, Editor & Builder

Full round-trip pipeline for DayZ .p3d files:

```
.p3d  --extract-->  Recipe JSON  --viewer-->  Edit visually  --build-->  new .p3d
```

Extract model data, visualize everything in Three.js, drag memory points
interactively, export the edited Recipe, and rebuild a fresh .p3d from it.

## Scope (and what this skill delegates)

This skill **owns** three things:

1. The **Recipe JSON** as a single, complete data contract over a .p3d file
   (all LODs + memory points + axes + selections + properties + referenced paths
   + bounding box).
2. The **interactive editor** (Three.js, drag&drop memory points and axis
   endpoints, snap-to-grid, modal Recipe export).
3. The **Recipe -> .p3d builder** (the inverse of the extractor; closes the
   round-trip loop).

Anything else is delegated to a sibling skill — do **not** duplicate it here:

| Task                                      | Use this skill instead                  |
|-------------------------------------------|-----------------------------------------|
| ODOL (binarized) .p3d -> MLOD (editable)  | external ODOL->MLOD converter (pre-step) |
| Visual viewer with PAA textures / glTF    | `dayz-3d-viewer` (or invoke before)     |
| PAA <-> PNG conversion                    | `dayz-3d-viewer` (`paa_to_png.py`)      |
| Deep config / path / structure validation | `dayz-p3d-audit` (post-step)            |
| Generating a .p3d from scratch (Blender)  | `dayz-model-pipeline`                   |
| Editing / aligning proxies on a host model | `dayz-proxy-align`                     |

The viewer in this skill renders untextured flat-shaded geometry on purpose:
its job is to make memory points, axes, selections, collision LODs, and bounds
inspectable and editable. For texture-rich previews, generate the Recipe here
and feed the source .p3d through `dayz-3d-viewer` separately.

## Install Dependencies

```bash
# py3d DayZ fork >= 1.6.0. Pick ONE route; both end in the same assert below.
# NEVER `pip install py3d` (PyPI = point-cloud lib) NOR git+upstream (no guards).
#
# Route A - this pack is checked out:
pip install -e tools/py3d
# Route B - a vendored wheel is present in this skill's wheels/ directory. That wheel
# is placed by the local py3d rollout and is NOT shipped in this pack; the installer
# fails closed when it is missing, and refuses to guess when legacy wheels sit beside it.
#   python3 scripts/install_py3d.py
python3 -c "import py3d; assert getattr(py3d,'IS_DAYZ_FORK',False) and tuple(map(int,py3d.__version__.split('.')))>=(1,6,0), (py3d.__version__, py3d.__file__)"
pip install numpy --break-system-packages
```

Only the extractor and builder need py3d — the viewer is pure Python that
emits a self-contained HTML file.

## Scripts

All in this skill's `scripts/` directory:

| Script | Purpose |
|--------|---------|
| `p3d_inspector_extract.py` | P3D -> Recipe JSON extractor (all LODs, memory, axes, selections) |
| `p3d_inspector_viewer.py`  | Recipe JSON -> Interactive HTML viewer generator (Phase 2 — drag & drop) |
| `p3d_inspector_build.py`   | Recipe JSON -> .p3d builder (MLOD) with mass policy + auto-fix |

## Quick Start

### From a real .p3d file

```python
import sys
sys.path.insert(0, '/path/to/skill/scripts')
from p3d_inspector_extract import extract_recipe
from p3d_inspector_viewer import generate_inspector_html

recipe = extract_recipe('model.p3d', verbose=True)
generate_inspector_html(recipe, output_path='inspector.html', model_name='My Model')
```

### Demo mode (no p3d needed)

```bash
python scripts/p3d_inspector_viewer.py --demo inspector_demo.html
```

### CLI

```bash
# Extract recipe from p3d
python scripts/p3d_inspector_extract.py model.p3d -v -o recipe.json

# Generate viewer from recipe
python scripts/p3d_inspector_viewer.py recipe.json inspector.html

# Build .p3d from an edited Recipe
python scripts/p3d_inspector_build.py recipe.json output.p3d -v
```

### Build Recipe -> .p3d (Python)

```python
import json
from p3d_inspector_build import build_p3d

with open('recipe_edited.json') as f:
    recipe = json.load(f)

result = build_p3d(recipe, 'model_rebuilt.p3d', auto_fix_enabled=True, verbose=True)
# result = { "output_path", "lods_written", "fixes_applied", "warnings" }
```

## Recipe JSON Format

The Recipe JSON is the central data contract between all pipeline stages:

```json
{
  "meta": {
    "source": "model.p3d",
    "version": 1,
    "mode": "audit|propose|demo"
  },
  "lods": [
    {
      "type": "visual_0|visual_1|visual_2|visual_3|geometry|fire_geometry|view_geometry|landcontact|roadway|paths|hitpoints|memory|shadow",
      "resolution": 0.0,
      "num_points": 328,
      "num_faces": 640,
      "geometry": { "positions": [], "normals": [], "uvs": [], "material_groups": {} },
      "wireframe": { "positions": [], "edges": [], "faces": [] },
      "selections": { "name": { "vertices": [], "faces": [], "vertex_count": 0, "face_count": 0 } },
      "properties": { "autocenter": "0" }
    }
  ],
  "memory_points": [
    { "index": 0, "position": [x,y,z], "selections": ["name"], "category": "center|interaction|axis|placing|port|proxy|other", "label": "name" }
  ],
  "axes": {
    "axis_name": { "points": [[x1,y1,z1],[x2,y2,z2]], "direction": [dx,dy,dz], "length": 0.015 }
  },
  "bounding_box": { "min": [], "max": [], "size": [], "center": [] },
  "referenced_paths": { "textures": [], "materials": [] }
}
```

`material_groups` keys use the format `"texture|material"` (pipe-separated; either side may
be empty). UVs are stored in glTF V-flipped convention so they render correctly in the
viewer; the builder un-flips them before writing back to the .p3d.

## LOD Types & Resolution Constants (BIS MLOD)

The classifier maps `lod.resolution` to a logical type:

| Resolution | Type | Has geometry/wireframe? | Notes |
|------------|------|-------------------------|-------|
| `0.0..10`        | `visual_0..3`     | full geometry  | rendered LODs |
| `1.0e3..1.5e4`   | `shadow`          | wireframe      | shadow volumes |
| `1.0e13`         | `geometry`        | wireframe      | physical collision; needs `Component01..` + mass |
| `1.0e15`         | `memory`          | points only    | memory points + animation axes |
| `2.0e15`         | `landcontact`     | wireframe      | per-vertex ground contact points |
| `3.0e15`         | `roadway`         | wireframe      | walkable surfaces |
| `4.0e15`         | `paths`           | wireframe      | AI navigation paths |
| `5.0e15`         | `hitpoints`       | wireframe      | named-selection destruction points |
| `6.0e15`         | `view_geometry`   | wireframe      | line-of-sight occlusion |
| `7.0e15`         | `fire_geometry`   | wireframe      | bullet/projectile collision |

Anything else falls back to `unknown_<resolution>` and the builder skips it
with a warning.

## Builder Behaviour

- **Visual LODs** — rebuilds tri faces from `geometry.material_groups`, re-inverts
  UVs back to DayZ convention (Recipe stores glTF V-flip), deduplicates per-vertex
  normals into a shared face-normal pool.
- **Wireframe LODs** (`geometry`, `fire_geometry`, `view_geometry`, `landcontact`,
  `roadway`, `paths`, `hitpoints`, `shadow`) — rebuilds faces from `wireframe.faces`
  (tris / quads; N-gons are fan-triangulated), preserves `properties`, auto-adds
  `autocenter=0` and `class=house` to the Geometry LOD only if absent.
- **Mass** is applied **only** to `geometry` and `fire_geometry`. ViewGeo,
  Shadow, LandContact, Roadway, Paths, HitPoints get no per-point mass.
- **Memory LOD** — ignores any `lod` entry of type `memory` in the Recipe and
  rebuilds from `memory_points` + `axes`, creating one Selection per unique
  selection name referenced in `memory_points[].selections[]`.
- **Mass policy** for Geometry/Fire LOD points (priority):
  1. `lod.properties["_point_mass"]` override (per-LOD float)
  2. `recipe.meta.point_mass_default` override (global float)
  3. Heuristic: scan `referenced_paths` for density keywords
     (metal/steel/iron=7800, wood=600, plastic=1200, default=2000 kg/m^3),
     then `per_point = (bbox_volume * density) / num_points`.
- **Auto-fix** (on by default; disable with `auto_fix_enabled=False` or `--no-autofix`):
  - Detects inverted face winding in Visual LOD 0 (>50% faces with normal pointing
    opposite to geometric cross-product) and reverses winding on **all** LODs.
  - Reports every fix applied in `result["fixes_applied"]`.
- **Deeper validation** (path existence, BB anchor, action targets, Component01
  coverage, etc.) is intentionally **not** in scope here. Pipe the freshly built
  .p3d through the `dayz-p3d-audit` skill for that.

## Viewer Features

### Layer Toggles
- **Visual**: Flat-shaded model mesh (no textures by design — see Scope above)
- **Geometry**: Green wireframe overlay showing collision LOD
- **Fire Geo**: Red wireframe showing ballistic geometry
- **View Geo**: Blue wireframe showing view culling geometry
- **LandContact**: Yellow-green wireframe showing ground contact points
- **Roadway**: Cyan wireframe showing walkable surfaces
- **Paths**: Magenta wireframe showing AI navigation paths
- **HitPoints**: Orange wireframe showing destruction hit points
- **Memory Pts**: Colored spheres at memory point positions
- **Axes**: Purple arrows showing animation axes
- **Bounds**: Bounding box (cyan) + box_placing bounds (orange)
- **Labels**: Floating text labels on memory points and axes
- **Grid**: Reference grid
- **Wireframe**: Visual mesh wireframe overlay
- **Export Recipe**: Opens modal with edited Recipe JSON for copy

The 7 wireframe LOD layers are off by default; enable them as needed.

### Memory Point Categories (Color-Coded)
- **Center** (cyan): `pos center`, `ce_center`
- **Interaction** (red): `actionPos`, action-related points
- **Axis** (purple): Animation axis endpoints (2 points per axis)
- **Placing** (orange): `box_placing_min`, `box_placing_max`
- **Port** (deep orange): `port_*`, `cable_*` (LFPG electrical)
- **Proxy** (gray): Proxy attachment points
- **Other** (light gray): Unclassified points

### Sidebar Panels
- **Points**: List of all memory points with live coordinates, click to focus
- **LODs**: Summary of all LODs with point/face counts and selections
- **Selections**: All named selections across all LODs
- **Paths**: Referenced texture/material paths with `P:\` drive warnings (UX hint;
  for full path/structure validation use `dayz-p3d-audit`)

### Interaction
- **Orbit**: Left mouse drag on empty space
- **Pan**: Right mouse button drag
- **Zoom**: Scroll wheel
- **Select point**: Click on a sphere -> shows info panel with coordinates
- **Drag point**: Click sphere + drag -> moves it in camera-perpendicular plane
- **Snap to grid**: Hold **Shift** while dragging to snap to grid
- **Grid size**: Press **1** for 0.01m (1cm), **2** for 0.05m (5cm)
- **Focus**: Click point in sidebar list -> camera flies to it
- **Export**: Click "Export Recipe" -> modal with JSON, use Select All + Ctrl+C

### Drag & Drop Details (Phase 2)

The drag system uses hover-based orbit disabling: when the cursor hovers over a memory
point sphere, OrbitControls is pre-emptively disabled so that pointerdown on the sphere
starts a drag instead of orbiting. Moving away from spheres re-enables orbit.

When dragging a memory point, the following are updated in real-time:
- Sphere position in 3D scene
- HTML overlay label position
- Axis line + arrow geometry (if the point is an axis endpoint)
- Placing bounding box (if the point is `box_placing_min` / `box_placing_max`)
- Sidebar coordinates text
- Recipe JSON data (R object in memory)
- Selection info panel coordinates

A floating coordinate display shows X/Y/Z during drag. The coordinate display also
shows the current snap grid size when Shift is held.

## Technical Notes

### Three.js Setup
- Three.js **r0.147.0** via classic UMD `<script>` from CDN (NOT ESM/importmap).
  This is intentional: when the HTML is opened from `file://` on Windows Chrome,
  ESM `<script type="module">` + import map + cross-origin CDN imports break
  with `net::ERR_BLOCKED_BY_ORB`. UMD works from `file://` without any flags.
- OrbitControls from `examples/js/controls/OrbitControls.js`. r0.147 is the
  last version that still ships UMD examples; r0.148+ removed `examples/js/`
  in favor of ESM-only. Don't bump above r0.147 without re-introducing an ESM
  module wrapper or a local OrbitControls fork.
- No CSS2DRenderer — labels use pure HTML overlay divs with `Vector3.project()`
- No Box3Helper — bounding boxes use manual LineSegments
- All materials are MeshStandardMaterial (not Basic) to support emissive on selection
- BufferAttribute with explicit Float32Array (not Float32BufferAttribute)

### Event Handling
- Hover detection via mousemove sets `ctrl.enabled=false` when over a sphere
- This prevents OrbitControls from capturing pointerdown on spheres
- pointerdown on sphere: selects + prepares drag state
- pointermove with >5px threshold: activates drag, creates camera-perpendicular plane
- pointerup: ends drag, re-enables orbit
- Drag plane created perpendicular to camera view direction through point position

### Recipe Export
- Export opens a modal with textarea containing the modified Recipe JSON
- User clicks "Select All" then Ctrl+C to copy
- Clipboard API is blocked on local file:// URLs, so textarea copy is the reliable path

## Pipeline Integration

```
+-------------+     +--------------+     +--------------+     +-------------+
| OBJ/FBX/GLB | --> | Claude       | --> | Inspector    | --> | p3d_inspect |
| (raw model) |     | proposes     |     | user adjusts |     | _build.py   |
|             |     | recipe       |     | via drag&drop|     |             |
+-------------+     +--------------+     +--------------+     +-------------+
       ^                [TODO]               ^                       |
       |                                     |                       v
       |            +--------------+         | Export Recipe   +-------------+
       |            | Existing     | ------> | (audit mode)    | model.p3d   |
       +------------| .p3d file    | -- extract --> Recipe     | (MLOD)      |
                    +--------------+                            +-------------+

Round-trip edit workflow (fully working):
   p3d ---> extract ---> Recipe ---> viewer (drag&drop) ---> export ---> build ---> p3d
```

If the input .p3d is binarized (ODOL), convert it with an external ODOL->MLOD converter first to get an
MLOD copy. After building, optionally run `dayz-p3d-audit` over the result for a
deep structural review.

Components status:
- **Extractor** (`p3d_inspector_extract.py`) — done (selection iteration fix applied)
- **Viewer + drag&drop** (`p3d_inspector_viewer.py`) — done (Phase 2)
- **Builder Recipe->p3d** (`p3d_inspector_build.py`) — done (round-trip verified)
- **Smart proposal** (raw mesh -> Recipe with inferred memory points/selections) — TODO

## Planned Features

### Phase 3: Selection Painting
- Color faces by selection membership
- Click faces to assign to selections
- Face winding validation (red=inverted, green=correct)
- Component01 coverage check

Existing tooling: the selection painter in `dayz-animation-pipeline`
(`references/selection-painter-for-actions.md`, used on A6_SR2M to repaint the
`bolt`/`trigger` selections with py3d write-back) already covers interactive
face painting — start from there instead of implementing this phase from scratch.

### Phase 4: Animation Preview
- Parse `model.cfg` animations
- Interactive sliders for animation phases
- Material state toggles (LED on/off/color)
- Live preview of animated selections

## Known Limitations

- Only MLOD .p3d files supported (not ODOL/binarized — pre-process via an
  external ODOL->MLOD converter).
- Viewer renders untextured flat-shaded geometry by design — for textured
  previews use `dayz-3d-viewer` separately.
- Clipboard API blocked on file:// URLs — use textarea Select All + Ctrl+C.
- Drag works on the camera-perpendicular plane (no axis-lock yet).
- Extractor does not capture per-point `mass` (Geometry/Fire LODs); the builder
  fills it via the material-density heuristic. To preserve exact masses from an
  existing .p3d, add them to the Recipe manually (via `lod.properties["_point_mass"]`)
  or pass a global `recipe.meta.point_mass_default`.
- Builder does not yet write proxies, sharp edges, or `#UVSet#` tags beyond
  what py3d emits automatically. For placing/orienting proxies on a host model
  use `dayz-proxy-align`.

## Changelog

### v4.1 — Viewer fix for `file://` (Windows Chrome ORB)

- **FIX** Viewer used `<script type="module">` + import map + ESM imports from
  `cdn.jsdelivr.net/npm/three@0.160.0`. When the HTML was opened from `file://`
  (typical case: user double-clicks the file on Windows), Chrome's Opaque
  Resource Blocking refused the OrbitControls cross-origin module fetch with
  `net::ERR_BLOCKED_BY_ORB`. The whole script tag failed silently — `window.TL`
  was undefined, no canvas appeared, no toolbar buttons worked.
- **NEW** Switched to Three.js **r0.147.0** UMD via classic `<script src>`
  loaders, which load from `file://` without any browser flags. Same scene,
  same drag&drop, same APIs (`T = THREE` alias keeps the rest of the code
  unchanged). r0.147 is the last version with UMD `examples/js/` shipped.

### v4 — Correct LOD classifier (BIS 1e15 family)

- **FIX** `classify_lod()` and the builder's `LOD_RESOLUTION` map were using
  the wrong constants for FireGeometry (was `2.0e13`, should be `7.0e15`) and
  ViewGeometry (was `3.0e13`, should be `6.0e15`), and a `resolution > 1.0e14`
  catch-all bucketed every special LOD as `memory`. On real .p3d files this
  collapsed Memory + LandContact + Roadway + Paths + HitPoints + ViewGeo +
  FireGeo into a single `memory` type, then sequentially overwrote
  `recipe.memory_points` from each — so the genuine memory points were lost.
- **NEW** Full BIS MLOD 1e15-family support: `memory` (1e15), `landcontact` (2e15),
  `roadway` (3e15), `paths` (4e15), `hitpoints` (5e15), `view_geometry` (6e15),
  `fire_geometry` (7e15). The extractor stores wireframe data for all of them;
  the builder rebuilds them with the correct resolution.
- **NEW** Mass policy clarified: only `geometry` and `fire_geometry` receive
  per-point mass; LandContact, Roadway, Paths, HitPoints, ViewGeo and Shadow
  do not.
- **NEW** Viewer toolbar: 4 new toggles (LandContact yellow-green, Roadway
  cyan, Paths magenta, HitPoints orange), off by default.
- **DOCS** New "LOD Types & Resolution Constants (BIS MLOD)" reference table.

### v3 — Scope realignment

- **NEW** "Scope" section — explicit list of what this skill owns vs. what it
  delegates to `dayz-3d-viewer`, `dayz-p3d-audit`, and
  `dayz-model-pipeline`. Removes documentation drift / overlap with sibling skills.
- **NEW** Pipeline integration note pointing at the external ODOL->MLOD converter (pre)
  and `dayz-p3d-audit` (post).
- **DOCS** "Texture rendering in viewer — TODO" reframed as a deliberate scope
  decision (delegated to `dayz-3d-viewer`).
- **DOCS** Builder validation policy explicitly delegates deep structural checks
  to `dayz-p3d-audit`.
- **DOCS** Aligned all documented signatures with the real code
  (`build_p3d(..., auto_fix_enabled=True, ...)`).

### v2 — Builder + Extractor fix

- **NEW** `p3d_inspector_build.py` — Recipe JSON -> .p3d (MLOD) builder.
  Supports all Recipe LOD types, mass-by-density, auto-fix winding. Round-trip
  verified against demo recipe (memory points, axes, selections, properties
  all preserved bit-for-bit — demo recipe only; real models: Memory-selection
  loss documented in SP-002).
- **FIX** `p3d_inspector_extract.py` — `extract_selections()` and
  `extract_memory_data()` iterated `lod.selections` as if the values were
  Selection objects with a `.name` attribute. In the current py3d API
  `lod.selections` is an `OrderedDict` that yields string keys on iteration,
  and each `Selection.points` / `.faces` is a `{Point|Face: weight}` dict
  (not indexable). The old code crashed with `AttributeError: 'str' object
  has no attribute 'name'` on any real .p3d file. Fixed by iterating
  `.items()` and mapping `Point` / `Face` object identities back to LOD-local
  indices via `id()` lookup tables.

## Memory selection round-trip + collision winding (added 2026-05-23)

### Round-trip is NOT lossless for Memory selections (SP-002)
The build path can lose/mis-write Memory-LOD selection membership: `build_memory_lod()`
reconstructs the Memory LOD from `recipe.memory_points[].selections` + `recipe.axes`, but the
extractor leaves those empty on real models — the real membership lives in
`recipe.lods[memory].selections`, which the builder IGNORES → a rebuild can wipe memory
selections (axes, dmgzones, crew, proxies) while names/geometry still look fine. Before any
irreversible rebuild, do a round-trip test (extract→build→re-extract) and compare selection
COUNTS, not just names. Mitigation for a .p3d with critical memory selections: edit the MLOD
with py3d directly and write with py3d, bypassing the recipe→build round-trip. (The inspector
remains fine for inspection/visualization.)

### Collision LOD winding is read per component, never matched to Visual (SP-003)
When generating/editing a collision LOD, run `dayz-model-pipeline` Rule 18's per-component check
before deploying: every non-proxy face must sit in a closed, convex `ComponentNN` and read
INWARD (DayZ left-handed); a LOD short of those prerequisites, or with a degenerate face, is
unresolved, not healthy. The relative check against the Visual LOD (py3d `ERR_WINDING_INVERTED`)
only triggers it. Reverse only the faces that read outward, and set their stored normals as
`dayz-p3d-audit` killer #1's **Fix** says (`dayz-model-pipeline` SP-003).
*(Aligned 2026-10-02 with `dayz-p3d-audit` killer #1: this section was titled "Collision LOD
winding must match Visual (SP-003)" and read: "When generating/editing a collision LOD, compare
its winding sign against the Visual LOD (centroid method) before deploying — they must match
(~100% INWARD, DayZ left-handed).")*

## (added 2026-06-01) Stale recipe after external edits to .p3d (SP-022)

Recipe JSON can get out of sync with actual `.p3d` if `.p3d` was edited
by another tool (Object Builder, direct py3d, external ODOL->MLOD converter) without re-extracting
recipe. Before reporting a bug from inspector view:

1. Re-extract recipe with `extract_recipe.py` and compare structurally with
   cached version.
2. If they differ, source of truth is `.p3d`, not recipe.
3. If recipe is identical but viewer shows something "strange", verify
   direct py3d round-trip before claiming bug.

Origin: introspection 2026-06-01 §B-6; kt_roadkill_armed session Sprint G' 2026-05-31.

<!-- [merged 2026-06-05 from <claude-home>\skills user copy during plugin-canonical migration; almacen: .claude\skills] -->
## py3d `selections` API gotcha (OrderedDict, non-subscriptable points/faces)

`lod.selections` is an **OrderedDict** keyed by selection name. Iterating it
gives string keys, not Selection objects. Each `Selection.points` and
`.faces` is a **dict** `{Point | Face: weight}` — NOT index-subscriptable.
To find LOD-local indices you must build `id()` lookup tables.

The extractor handles this correctly. If you see `AttributeError: 'str'
object has no attribute 'name'` or `TypeError: 'Selection' object is not
subscriptable`, you're looking at old/stale code — use the version in
this skill.

## Recipe selection indexing - vertices = point-pool, NOT triangle-soup (SP-028, added 2026-07-14)

`selections[].vertices` in the Recipe indexes the LOD's UNIQUE POINT POOL (0..len(lod.points)-1). `geometry.positions` is TRIANGLE-SOUP (3 entries per face, len = num_faces*3). Indexing `positions` with selection vertices yields coherent-looking but WRONG coordinates (it lands on unrelated real geometry), producing false structural alarms ("selection points at the wrong mesh").

Before declaring any selection/structure bug from Recipe data, verify at the py3d object level: load the .p3d, take `lod.selections[name].faces`, read each face's `vertices[].point_index` coords, and bbox THOSE. That is engine truth. Origin: kt_roadkill_armed 2026-06-07 false P1 (turret_yaw "was" the car spikes; the model was correct all along). Cross-ref SP-002, LL-111.

## Rules promoted from lessons corpus (added 2026-07-27)

Promoted from `AI/20_Knowledge/lessons-learned.md` to arrive via trigger instead
of depending on someone remembering to look them up. Each rule cites source `LL-NNN`;
complete entry (symptom, origin, evidence) lives there. Do not remove citation: the index
`lessons-index.md` detects promotion by looking for that reference inside skills.

- **LL-109** — For round-trips that normalize, sort, or deduplicate, test `f(f(x)) == f(x)` and define separately which fields must be stable from first pass. Document any allowed renormalization.

## Round-trip caveat

Memory selections may not survive the extract → edit → rebuild cycle (SP-002). Verify selection
membership after a rebuild, not just selection names.
- **LL-010** — Before asserting a piece is missing, enumerate `.p3d` proxies, inspect their sizes, and open substantial candidates. Config only reveals what is rigged; debinarize and split connected components if geometry lives in a proxy.
- **LL-177** — Before extracting selections or corrected geometry, locate actual edited file —including backups marking intervention— and have pipeline read that copy. Do not validate provenance solely because bbox looks coherent.


## Non-semantic MLOD face padding (SP-229, added 2026-08-31)

An MLOD face record always reserves four vertex slots. A triangular face leaves the fourth
slot unused, and Object Builder can leave non-zero bytes there after face reordering or
editing. Those bytes carry no face data and binarization discards them. Source:
`p3dtxt/README.md:11` and `p3dtxt/src/main.rs:38-42`.

A changed `.p3d` hash after a rewrite therefore does not by itself prove data loss. For an
MLOD round-trip, compare parsed fields while ignoring the unused fourth slot of triangles,
or binarize both versions and compare the resulting ODOL semantics. Keep byte identity only
for producers that explicitly promise canonical zero padding. This caveat is separate from
the Memory-selection loss described in SP-002.

## Measure visibility by AREA sampling, not by a centroid ray (SP-446, added 2026-09-28)

[EXACT] To decide whether a selection is visible from outside, cast rays from random points across the whole face (order of 3000 per LOD) and report the free-area percentage; a single ray from the centroid lies. Measured on a heater model: centroid rays reported "48/48 blocked in all 6 directions" while area sampling showed 84-86 % free — every face was a long thin triangle whose centroid sat behind a 1-2 mm wire. The false diagnosis nearly triggered rebuilding the model (the emissive grid) instead of fixing the config.

Files in this skill

  • SKILL.md26.1 KB
  • scripts/install_py3d.py8.3 KB
  • scripts/p3d_inspector_build.py19.5 KB
  • scripts/p3d_inspector_extract.py20.4 KB
  • scripts/p3d_inspector_viewer.py39.5 KB
  • tests/test_install_py3d.py4.6 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.