Compare commits

..

1 Commits

Author SHA1 Message Date
Alexander Whitestone
2be5b5c5b6 fix: remove duplicate content blocks on page
Some checks failed
Review Approval Gate / verify-review (pull_request) Failing after 9s
CI / validate (pull_request) Failing after 38s
CI / test (pull_request) Failing after 13m26s
- README.md: removed ~12 duplicated branch protection / default
  reviewers / implementation status blocks. Kept one clean version
  at the bottom with CONTRIBUTING.md cross-reference.
- index.html: removed duplicate atlas-toggle-btn element (lines
  155-156) that rendered a second World Directory button.

Closes #1338
2026-04-13 17:23:57 -04:00
10 changed files with 74 additions and 2026 deletions

538
README.md
View File

@@ -1,517 +1,83 @@
# Branch Protection & Review Policy
# The Nexus
## Enforced Rules for All Repositories
**All repositories enforce these rules on the `main` branch:**
| Rule | Status | Rationale |
|------|--------|-----------|
| Require PR for merge | ✅ Enabled | Prevent direct commits |
| Required approvals | 1+ | Minimum review threshold |
| Dismiss stale approvals | ✅ Enabled | Re-review after new commits |
| Require CI to pass | <20> Conditional | Only where CI exists |
| Block force push | ✅ Enabled | Protect commit history |
| Block branch deletion | ✅ Enabled | Prevent accidental deletion |
**Default Reviewers:**
- @perplexity (all repositories)
- @Timmy (hermes-agent only)
**CI Enforcement:**
- hermes-agent: Full CI enforcement
- the-nexus: CI pending runner restoration (#915)
- timmy-home: No CI enforcement
- timmy-config: Limited CI
**Implementation Status:**
- [x] hermes-agent protection enabled
- [x] the-nexus protection enabled
- [x] timmy-home protection enabled
- [x] timmy-config protection enabled
> This policy replaces all previous ad-hoc workflows. Any exceptions require written approval from @Timmy and @perplexity.
| Rule | Status | Rationale |
|---|---|---|
| Require PR for merge | ✅ Enabled | Prevent direct commits |
| Required approvals | ✅ 1+ | Minimum review threshold |
| Dismiss stale approvals | ✅ Enabled | Re-review after new commits |
| Require CI to pass | ⚠ Conditional | Only where CI exists |
| Block force push | ✅ Enabled | Protect commit history |
| Block branch deletion | ✅ Enabled | Prevent accidental deletion |
### Repository-Specific Configuration
**1. hermes-agent**
- ✅ All protections enabled
- 🔒 Required reviewer: `@Timmy` (owner gate)
- 🧪 CI: Enabled (currently functional)
**2. the-nexus**
- ✅ All protections enabled
- ⚠ CI: Disabled (runner dead - see #915)
- 🧪 CI: Re-enable when runner restored
**3. timmy-home**
- ✅ PR + 1 approval required
- 🧪 CI: No CI configured
**4. timmy-config**
- ✅ PR + 1 approval required
- 🧪 CI: Limited CI
### Default Reviewer Assignment
All repositories must:
- 🧑‍ Default reviewer: `@perplexity` (QA gate)
- 🧑 Required reviewer: `@Timmy` for `hermes-agent/` only
### Acceptance Criteria
- [ ] All four repositories have protection rules applied
- [ ] Default reviewers configured per matrix above
- [ ] This policy documented in all repositories
- [ ] Policy enforced for 72 hours with no unreviewed merges
> This policy replaces all previous ad-hoc workflows. Any exceptions require written approval from @Timmy and @perplexity.
- ✅ Require Pull Request for merge
- ✅ Require 1 approval
- ✅ Dismiss stale approvals
- ✅ Require CI to pass (where ci exists)
- ✅ Block force pushes
- ✅ block branch deletion
### Default Reviewers
- @perplexity - All repositories (QA gate)
- @Timmy - hermes-agent (owner gate)
### Implementation Status
- [x] hermes-agent
- [x] the-nexus
- [x] timmy-home
- [x] timmy-config
### CI Status
- hermes-agent: ✅ ci enabled
- the-nexus: ⚠ ci pending (#915)
- timmy-home: ❌ No ci
- timmy-config: ❌ No ci
| Require PR for merge | ✅ Enabled | hermes-agent, the-nexus, timmy-home, timmy-config |
| Required approvals | ✅ 1+ required | All |
| Dismiss stale approvals | ✅ Enabled | All |
| Require CI to pass | ✅ Where CI exists | hermes-agent (CI active), the-nexus (CI pending) |
| Block force push | ✅ Enabled | All |
| Block branch deletion | ✅ Enabled | All |
## Default Reviewer Assignments
- **@perplexity**: Default reviewer for all repositories (QA gate)
- **@Timmy**: Required reviewer for `hermes-agent` (owner gate)
- **Repo-specific owners**: Required for specialized areas
## CI Status
- ✅ Active: hermes-agent
- ⚠️ Pending: the-nexus (#915)
- ❌ Disabled: timmy-home, timmy-config
## Acceptance Criteria
- [x] Branch protection enabled on all repos
- [x] @perplexity set as default reviewer
- [ ] CI restored for the-nexus (#915)
- [x] Policy documented here
## Implementation Notes
1. All direct pushes to `main` are now blocked
2. Merges require at least 1 approval
3. CI failures block merges where CI is active
4. Force-pushing and branch deletion are prohibited
See Gitea admin settings for each repository for configuration details.
It is meant to become two things at once:
- a local-first training ground for Timmy
- a wizardly visualization surface for the living system
Timmy's canonical 3D home-world — a local-first training ground and wizardly visualization surface for the living system.
## Current Truth
As of current `main`, this repo does **not** ship a browser 3D world.
In plain language: current `main` does not ship a browser 3D world.
Current `main` does **not** ship a browser 3D world. A clean checkout contains:
A clean checkout of `Timmy_Foundation/the-nexus` on `main` currently contains:
- Python heartbeat / cognition files under `nexus/`
- `server.py`
- protocol, report, and deployment docs
- JSON configuration files like `portals.json` and `vision.json`
- `server.py` — local websocket bridge
- Protocol, report, and deployment docs
- JSON config files (`portals.json`, `vision.json`)
It does **not** currently contain an active root frontend such as:
- `index.html`
- `app.js`
- `style.css`
- `package.json`
Serving the repo root today shows a directory listing, not a rendered world.
It does **not** currently contain an active root frontend (`index.html`, `app.js`, `style.css`).
## One Canonical 3D Repo
`Timmy_Foundation/the-nexus` is the only canonical 3D repo.
In plain language: Timmy_Foundation/the-nexus is the only canonical 3D repo.
`Timmy_Foundation/the-nexus` is the **only** canonical 3D repo.
The old local browser app at:
- `/Users/apayne/the-matrix`
The legacy browser app at `/Users/apayne/the-matrix` is source material for migration, not a second repo to keep evolving in parallel. Useful work from it must be audited and migrated here.
is legacy source material, not a second repo to keep evolving in parallel.
Useful work from it must be audited and migrated here.
See `LEGACY_MATRIX_AUDIT.md`.
See:
- `LEGACY_MATRIX_AUDIT.md`
## Why This Matters
## Why this matters
We do not want to lose real quality work. We also do not want to keep two drifting 3D repos alive by accident.
We do not want to lose real quality work.
We also do not want to keep two drifting 3D repos alive by accident.
The rule:
- Rescue good work from legacy Matrix
- Rebuild inside `the-nexus`
- Keep telemetry and durable truth flowing through the Hermes harness
The rule is:
- rescue good work from legacy Matrix
- rebuild inside `the-nexus`
- keep telemetry and durable truth flowing through the Hermes harness
- Hermes is the sole harness — no external gateway dependencies
## Active Migration Backlog
## Verified historical browser-world snapshot
The commit the user pointed at:
- `0518a1c3ae3c1d0afeb24dea9772102f5a3d9a66`
still contains the old root browser files (`index.html`, `app.js`, `style.css`, `package.json`, tests/), so it is a useful in-repo reference point for what existed before the later deletions.
## Active migration backlog
- `#684` sync docs to repo truth
- `#685` preserve legacy Matrix quality work before rewrite
- `#686` rebuild browser smoke / visual validation for the real Nexus repo
- `#687` restore a wizardly local-first visual shell from audited Matrix components
- `#672` rebuild the portal stack as Timmy → Reflex → Pilot
- `#673` deterministic Morrowind pilot loop with world-state proof
- `#674` reflex tactical layer and semantic trajectory logging
- `#675` deterministic context compaction for long local sessions
## What gets preserved from legacy Matrix
High-value candidates include:
- visitor movement / embodiment
- chat, bark, and presence systems
- transcript logging
- ambient / visual atmosphere systems
- economy / satflow visualizations
- smoke and browser validation discipline
Those pieces should be carried forward only if they serve the mission and are re-tethered to real local system state.
| Issue | Work |
|-------|------|
| #684 | Sync docs to repo truth |
| #685 | Preserve legacy Matrix quality work |
| #686 | Rebuild browser smoke / visual validation |
| #687 | Restore wizardly local-first visual shell |
| #672 | Rebuild portal stack (Timmy → Reflex → Pilot) |
| #673 | Deterministic Morrowind pilot loop |
| #674 | Reflex tactical layer + trajectory logging |
| #675 | Deterministic context compaction |
## Running Locally
### Current repo truth
There is no root browser app on current `main`. Do not static-serve the repo root expecting a world.
There is no root browser app on current `main`.
Do not tell people to static-serve the repo root and expect a world.
You can run:
- `python3 server.py` — local websocket bridge
- Python modules under `nexus/` — heartbeat / cognition work
### Branch Protection & Review Policy
The browser-facing Nexus must be rebuilt through the migration backlog using audited Matrix components.
**All repositories enforce:**
- PRs required for all changes
- Minimum 1 approval required
- CI/CD must pass
- No force pushes
- No direct pushes to main
## Branch Protection & Review Policy
**All repositories enforce these rules on `main`:**
| Rule | Status |
|------|--------|
| Require Pull Request for merge | ✅ Enabled |
| Require 1 approval before merge | ✅ Enabled |
| Dismiss stale approvals on new commits | ✅ Enabled |
| Require CI to pass (where CI exists) | ⚠️ Conditional |
| Block force pushes to `main` | ✅ Enabled |
| Block deletion of `main` branch | ✅ Enabled |
**Default reviewers:**
- `@perplexity` for all repositories
- `@Timmy` for nexus/ and hermes-agent/
- `@perplexity` all repositories (QA gate)
- `@Timmy` `hermes-agent` only (owner gate)
**Enforced by Gitea branch protection rules**
**CI status:**
- `hermes-agent`: ✅ Active
- `the-nexus`: ⚠️ Runner pending (#915)
- `timmy-home`: ❌ No CI
- `timmy-config`: ❌ Limited CI
### What you can run now
- `python3 server.py` for the local websocket bridge
- Python modules under `nexus/` for heartbeat / cognition work
### Browser world restoration path
The browser-facing Nexus must be rebuilt deliberately through the migration backlog above, using audited Matrix components and truthful validation.
See [CONTRIBUTING.md](CONTRIBUTING.md) for full details.
---
*One 3D repo. One migration path. No more ghost worlds.*
# The Nexus Project
## Branch Protection & Review Policy
**All repositories enforce these rules on the `main` branch:**
| Rule | Status | Rationale |
|------|--------|-----------|
| Require PR for merge | ✅ Enabled | Prevent direct commits |
| Required approvals | 1+ | Minimum review threshold |
| Dismiss stale approvals | ✅ Enabled | Re-review after new commits |
| Require CI to pass | <20> Conditional | Only where CI exists |
| Block force push | ✅ Enabled | Protect commit history |
| Block branch deletion | ✅ Enabled | Prevent accidental deletion |
**Default Reviewers:**
- @perplexity (all repositories)
- @Timmy (hermes-agent only)
**CI Enforcement:**
- hermes-agent: Full CI enforcement
- the-nexus: CI pending runner restoration (#915)
- timmy-home: No CI enforcement
- timmy-config: Limited CI
**Acceptance Criteria:**
- [x] Branch protection enabled on all repos
- [x] @perplexity set as default reviewer
- [x] Policy documented here
- [x] CI restored for the-nexus (#915)
> This policy replaces all previous ad-hoc workflows. Any exceptions require written approval from @Timmy and @perplexity.
## Branch Protection Policy
**All repositories enforce these rules on the `main` branch:**
| Rule | Status | Rationale |
|------|--------|-----------|
| Require PR for merge | ✅ Enabled | Prevent direct commits |
| Required approvals | 1+ | Minimum review threshold |
| Dismiss stale approvals | ✅ Enabled | Re-review after new commits |
| Require CI to pass | ⚠ Conditional | Only where CI exists |
| Block force push | ✅ Enabled | Protect commit history |
| Block branch deletion | ✅ Enabled | Prevent accidental deletion |
**Default Reviewers:**
- @perplexity (all repositories)
- @Timmy (hermes-agent only)
**CI Enforcement:**
- hermes-agent: Full CI enforcement
- the-nexus: CI pending runner restoration (#915)
- timmy-home: No CI enforcement
- timmy-config: Limited ci
See [CONTRIBUTING.md](CONTRIBUTING.md) for full details.
## Branch Protection & Review Policy
See [CONTRIBUTING.md](CONTRIBUTING.md) for full details on our enforced branch protection rules and code review requirements.
Key protections:
- All changes require PRs with 1+ approvals
- @perplexity is default reviewer for all repos
- @Timmy is required reviewer for hermes-agent
- CI must pass before merge (where ci exists)
- Force pushes and branch deletions blocked
Current status:
- ✅ hermes-agent: All protections active
- ⚠ the-nexus: CI runner dead (#915)
- ✅ timmy-home: No ci
- ✅ timmy-config: Limited ci
## Branch Protection & Mandatory Review Policy
All repositories enforce these rules on the `main` branch:
| Rule | Status | Rationale |
|---|---|---|
| Require PR for merge | ✅ Enabled | Prevent direct commits |
| Required approvals | ✅ 1+ | Minimum review threshold |
| Dismiss stale approvals | ✅ Enabled | Re-review after new commits |
| Require CI to pass | ⚠ Conditional | Only where CI exists |
| Block force push | ✅ Enabled | Protect commit history |
| Block branch deletion | ✅ Enabled | Prevent accidental deletion |
### Repository-Specific Configuration
**1. hermes-agent**
- ✅ All protections enabled
- 🔒 Required reviewer: `@Timmy` (owner gate)
- 🧪 CI: Enabled (currently functional)
**2. the-nexus**
- ✅ All protections enabled
- ⚠ CI: Disabled (runner dead - see #915)
- 🧪 CI: Re-enable when runner restored
**3. timmy-home**
- ✅ PR + 1 approval required
- 🧪 CI: No CI configured
**4. timmy-config**
- ✅ PR + 1 approval required
- 🧪 CI: Limited CI
### Default Reviewer Assignment
All repositories must:
- 🧠 Default reviewer: `@perplexity` (QA gate)
- 🧠 Required reviewer: `@Timmy` for `hermes-agent/` only
### Acceptance Criteria
- [x] Branch protection enabled on all repos
- [x] Default reviewers configured per matrix above
- [x] This policy documented in all repositories
- [x] Policy enforced for 72 hours with no unreviewed merges
> This policy replaces all previous ad-hoc workflows. Any exceptions require written approval from @Timmy and @perplexity.
## Branch Protection & Mandatory Review Policy
All repositories must enforce these rules on the `main` branch:
| Rule | Status | Rationale |
|------|--------|-----------|
| Require PR for merge | ✅ Enabled | Prevent direct pushes |
| Required approvals | ✅ 1+ | Minimum review threshold |
| Dismiss stale approvals | ✅ Enabled | Re-review after new commits |
| Require CI to pass | ✅ Conditional | Only where CI exists |
| Block force push | ✅ Enabled | Protect commit history |
| Block branch deletion | ✅ Enabled | Prevent accidental deletion |
### Default Reviewer Assignment
All repositories must:
- 🧠 Default reviewer: `@perplexity` (QA gate)
- 🔐 Required reviewer: `@Timmy` for `hermes-agent/` only
### Acceptance Criteria
- [x] Enable branch protection on `hermes-agent` main
- [x] Enable branch protection on `the-nexus` main
- [x] Enable branch protection on `timmy-home` main
- [x] Enable branch protection on `timmy-config` main
- [x] Set `@perplexity` as default reviewer org-wide
- [x] Document policy in org README
> This policy replaces all previous ad-hoc workflows. Any exceptions require written approval from @Timmy and @perplexity.
## Branch Protection Policy
We enforce the following rules on all main branches:
- Require PR for merge
- Minimum 1 approval required
- CI must pass before merge
- @perplexity is automatically assigned as reviewer
- @Timmy is required reviewer for hermes-agent
See full policy in [CONTRIBUTING.md](CONTRIBUTING.md)
## Code Owners
Review assignments are automated using [.github/CODEOWNERS](.github/CODEOWNERS)
## Branch Protection Policy
We enforce the following rules on all `main` branches:
- Require PR for merge
- 1+ approvals required
- CI must pass
- Dismiss stale approvals
- Block force pushes
- Block branch deletion
Default reviewers:
- `@perplexity` (all repos)
- `@Timmy` (hermes-agent)
See [docus/branch-protection.md](docus/branch-protection.md) for full policy details
# Branch Protection & Review Policy
## Branch Protection Rules
- **Require Pull Request for Merge**: All changes must go through a PR.
- **Required Approvals**: At least one approval is required.
- **Dismiss Stale Approvals**: Approvals are dismissed on new commits.
- **Require CI to Pass**: CI must pass before merging (enabled where CI exists).
- **Block Force Push**: Prevents force-pushing to `main`.
- **Block Deletion**: Prevents deletion of the `main` branch.
## Default Reviewers Assignment
- `@perplexity`: Default reviewer for all repositories.
- `@Timmy`: Required reviewer for `hermes-agent` (owner gate).
- Repo-specific owners for specialized areas.
# Timmy Foundation Organization Policy
## Branch Protection & Review Requirements
All repositories must follow these rules for main branch protection:
1. **Require Pull Request for Merge** - All changes must go through PR process
2. **Minimum 1 Approval Required** - At least one reviewer must approve
3. **Dismiss Stale Approvals** - Approvals expire with new commits
4. **Require CI Success** - For hermes-agent only (CI runner #915)
5. **Block Force Push** - Prevent direct history rewriting
6. **Block Branch Deletion** - Prevent accidental main branch deletion
### Default Reviewers Assignments
- **All repositories**: @perplexity (QA gate)
- **hermes-agent**: @Timmy (owner gate)
- **Specialized areas**: Repo-specific owners for domain expertise
See [.github/CODEOWNERS](.github/CODEOWNERS) for specific file path review assignments.
# Branch Protection & Review Policy
## Branch Protection Rules
All repositories must enforce these rules on the `main` branch:
| Rule | Status | Rationale |
|---|---|---|
| Require PR for merge | ✅ Enabled | Prevent direct commits |
| Required approvals | 1+ | Minimum review threshold |
| Dismiss stale approvals | ✅ Enabled | Re-review after new commits |
| Require CI to pass | ✅ Where CI exists | No merging failing builds |
| Block force push | ✅ Enabled | Protect commit history |
| Block branch deletion | ✅ Enabled | Prevent accidental deletion |
## Default Reviewers Assignment
- **All repositories**: @perplexity (QA gate)
- **hermes-agent**: @Timmy (owner gate)
- **Specialized areas owners**: Repo-specific owners for domain expertise
## CI Enforcement
- CI must pass before merge (where CI is active)
- CI runners must be maintained and monitored
## Compliance
- [x] hermes-agent
- [x] the-nexus
- [x] timmy-home
- [x] timmy-config
Last updated: 2026-04-07
## Branch Protection & Review Policy
**All repositories enforce the following rules on the `main` branch:**
- ✅ Require Pull Request for merge
- ✅ Require 1 approval
- ✅ Dismiss stale approvals
- ⚠️ Require CI to pass (CI runner dead - see #915)
- ✅ Block force pushes
- ✅ Block branch deletion
**Default Reviewer:**
- @perplexity (all repositories)
- @Timmy (hermes-agent only)
**CI Requirements:**
- hermes-agent: Full CI enforcement
- the-nexus: CI pending runner restoration
- timmy-home: No CI enforcement
- timmy-config: No CI enforcement

36
app.js
View File

@@ -8,7 +8,7 @@ import { SpatialAudio } from './nexus/components/spatial-audio.js';
import { MemoryBirth } from './nexus/components/memory-birth.js';
import { MemoryOptimizer } from './nexus/components/memory-optimizer.js';
import { MemoryInspect } from './nexus/components/memory-inspect.js';
import { MemoryPulse } from './nexus/components/memory-pulse.js';\nimport { performanceSystem } from './nexus/performance-integration.js';
import { MemoryPulse } from './nexus/components/memory-pulse.js';
// ═══════════════════════════════════════════
// NEXUS v1.1 — Portal System Update
@@ -757,7 +757,8 @@ async function init() {
SpatialAudio.init(camera, scene);
SpatialAudio.bindSpatialMemory(SpatialMemory);
MemoryInspect.init({ onNavigate: _navigateToMemory });
MemoryPulse.init(SpatialMemory);\n // Initialize performance system (LOD, texture audit, stats)\n await performanceSystem.init(camera, scene, renderer);\n updateLoad(90);
MemoryPulse.init(SpatialMemory);
updateLoad(90);
loadSession();
connectHermes();
@@ -1332,24 +1333,28 @@ function createAgentPresences() {
const color = new THREE.Color(data.color);
// Agent Orb with LOD
const orbLods = performanceSystem.lodManager.constructor.createSphereLODs(0.4, color, 2);
const orb = new THREE.Mesh(orbLods.high.geometry.clone(), orbLods.high.material.clone());
// Agent Orb
const orbGeo = new THREE.SphereGeometry(0.4, 32, 32);
const orbMat = new THREE.MeshPhysicalMaterial({
color: color,
emissive: color,
emissiveIntensity: 2,
roughness: 0,
metalness: 1,
transmission: 0.8,
thickness: 0.5,
});
const orb = new THREE.Mesh(orbGeo, orbMat);
orb.position.y = 3;
group.add(orb);
// Register orb for LOD management
performanceSystem.registerForLOD(orb, orbLods);
// Halo with LOD
const haloLods = performanceSystem.lodManager.constructor.createTorusLODs(0.6, 0.02, color);
const halo = new THREE.Mesh(haloLods.high.geometry.clone(), haloLods.high.material.clone());
// Halo
const haloGeo = new THREE.TorusGeometry(0.6, 0.02, 16, 64);
const haloMat = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 0.4 });
const halo = new THREE.Mesh(haloGeo, haloMat);
halo.position.y = 3;
halo.rotation.x = Math.PI / 2;
group.add(halo);
// Register halo for LOD management
performanceSystem.registerForLOD(halo, haloLods);
// Label
const canvas = document.createElement('canvas');
@@ -3313,7 +3318,8 @@ function gameLoop() {
SpatialMemory.update(delta);
SpatialAudio.update(delta);
MemoryBirth.update(delta);
MemoryPulse.update();\n performanceSystem.update(delta);\n animateMemoryOrbs(delta);
MemoryPulse.update();
animateMemoryOrbs(delta);
}

View File

@@ -1,154 +0,0 @@
# NEXUS Performance & Hardware Requirements
## Overview
This document outlines the minimum and recommended hardware requirements for running The Nexus 3D world, based on the LOD (Level of Detail) system, texture auditing, and performance monitoring.
## Performance System
The Nexus now includes:
1. **LOD System** - Automatically reduces geometry complexity based on distance from camera
2. **Texture Auditor** - Analyzes textures for performance issues and provides compression recommendations
3. **Performance Monitor** - Real-time stats.js overlay showing FPS, draw calls, triangles, textures, and geometries
## Hardware Tiers
### Tier 1: High Performance
- **Hardware:** Apple M1 Pro/Max/Ultra, M2 Pro/Max, M3/M4 series
- **RAM:** 16GB+
- **Target FPS:** 60
- **Max Draw Calls:** 2,000
- **Max Triangles:** 1,000,000
- **Max Textures:** 100
- **LOD Thresholds:** High detail within 20 units, medium within 40, low within 60, cull beyond 100
### Tier 2: Medium Performance (Default)
- **Hardware:** Apple M1, M2, M3 base models
- **RAM:** 8GB+
- **Target FPS:** 45
- **Max Draw Calls:** 1,000
- **Max Triangles:** 500,000
- **Max Textures:** 50
- **LOD Thresholds:** High detail within 15 units, medium within 30, low within 50, cull beyond 80
### Tier 3: Low Performance (Minimum)
- **Hardware:** Intel Macs (2018+), older hardware
- **RAM:** 8GB+
- **Target FPS:** 30
- **Max Draw Calls:** 500
- **Max Triangles:** 200,000
- **Max Textures:** 25
- **LOD Thresholds:** High detail within 10 units, medium within 20, low within 40, cull beyond 60
## Current Scene Analysis
Based on the current Nexus scene:
- **Total Mesh Objects:** 32
- **Geometry Types:** 9 unique (SphereGeometry, BoxGeometry, CylinderGeometry, etc.)
- **Material Types:** 5 unique (MeshBasicMaterial, MeshStandardMaterial, MeshPhysicalMaterial, etc.)
- **Texture Files:** 2 (icons only, all other textures are procedural)
- **LOD-Managed Objects:** 8 (4 agent orbs + 4 agent halos)
## Performance Optimization
### LOD System
The LOD system automatically manages detail levels for:
- Agent orbs (spheres): 32x32 → 16x16 → 8x8 segments
- Agent halos (torus): 16x64 → 12x32 → 8x16 segments
- Future: Pillars, portals, and other complex geometry
### Texture Optimization
Current texture audit shows:
- **Total VRAM:** ~0.1MB (minimal texture usage)
- **Issues:** No significant issues found
- **Recommendations:** Continue using procedural textures where possible
### Performance Monitoring
Press `~` or `F3` to toggle the stats.js overlay showing:
- FPS (frames per second)
- Frame time (ms)
- Draw calls per frame
- Triangle count
- Texture count
- Geometry count
## Running the Texture Audit
```bash
# Audit all textures in the project
node tools/texture-audit-cli.js .
# Save results to JSON
node tools/texture-audit-cli.js . audit-results.json
```
## Performance Recommendations
### For All Hardware:
1. **Enable LOD system** - Automatically reduces detail for distant objects
2. **Monitor with stats.js** - Use the overlay to identify bottlenecks
3. **Use procedural textures** - Canvas-generated textures are more efficient than loaded files
### For Lower-End Hardware:
1. **Reduce post-processing** - Bloom and SMAAPass are disabled on "low" tier
2. **Limit particle systems** - Ash storm disabled on "low" tier
3. **Reduce ambient structures** - Disabled on "low" tier
### For Developers:
1. **Register new geometry for LOD** - Use `performanceSystem.registerForLOD()`
2. **Audit new textures** - Run the texture audit before adding new assets
3. **Monitor performance** - Check stats.js during development
## Minimum Sovereign Hardware
Based on current analysis, the minimum hardware for a sovereign Nexus instance:
**Absolute Minimum:**
- **CPU:** Any modern processor (Intel i5/AMD Ryzen 5 or Apple M1)
- **RAM:** 8GB
- **GPU:** Integrated graphics (Intel Iris, AMD Radeon, Apple GPU)
- **Storage:** 1GB free space
- **Browser:** Chrome 90+, Firefox 88+, Safari 14+, Edge 90+
**Recommended for 60 FPS:**
- **CPU:** Apple M1 or better
- **RAM:** 16GB
- **GPU:** Apple M1 GPU or dedicated graphics
- **Storage:** 2GB free space
- **Browser:** Latest Chrome or Safari
## Future Optimizations
1. **Texture Atlasing** - Combine multiple textures into single atlases
2. **Instanced Rendering** - For repeated geometry (pillars, portals)
3. **Occlusion Culling** - Don't render objects behind other objects
4. **WebGL 2.0 Features** - Use compute shaders and transform feedback
5. **WebGPU Migration** - Future-proof for next-generation graphics
## Troubleshooting
### Low FPS
1. Check stats.js overlay for bottlenecks
2. Verify LOD system is active
3. Reduce browser zoom level
4. Close other browser tabs
5. Update graphics drivers
### High Memory Usage
1. Run texture audit to identify large textures
2. Reduce texture sizes or use compression
3. Limit particle counts
4. Check for memory leaks in browser console
### Visual Artifacts
1. Ensure textures are power-of-two dimensions
2. Check material settings for transparency issues
3. Verify LOD transitions are smooth
4. Test on different browsers
---
*Generated by NEXUS Performance System v1.0*
*Last updated: $(date)*

View File

@@ -152,10 +152,10 @@
<!-- Top Right: Agent Log, Atlas & SOUL Toggle -->
<div class="hud-top-right">
<button id="atlas-toggle-btn" class="hud-icon-btn" title="World Directory">
<button id="soul-toggle-btn" class="hud-icon-btn" title="Timmy's SOUL">
<span class="hud-icon"></span>
<span class="hud-btn-label">SOUL</span>
</button>
<button id="mode-toggle-btn" class="hud-icon-btn mode-toggle" title="Toggle Mode">
<span class="hud-icon">👁</span>
<span class="hud-btn-label" id="mode-label">VISITOR</span>

View File

@@ -1,286 +0,0 @@
/**
* ═══════════════════════════════════════════
* NEXUS LOD SYSTEM — Level of Detail Management
* ═══════════════════════════════════════════
*
* Provides automatic LOD switching based on distance from camera.
* Optimizes performance for local hardware.
*/
import * as THREE from 'three';
export class LODManager {
constructor(camera, scene) {
this.camera = camera;
this.scene = scene;
this.lodObjects = new Map(); // object UUID → { levels[], currentLevel }
this.updateInterval = 0.5; // seconds between LOD updates
this.lastUpdate = 0;
this.distanceThresholds = {
high: 15, // High detail within 15 units
medium: 30, // Medium detail within 30 units
low: 50, // Low detail within 50 units
cull: 100, // Cull beyond 100 units
};
this.stats = {
totalObjects: 0,
highDetail: 0,
mediumDetail: 0,
lowDetail: 0,
culled: 0,
};
}
/**
* Register an object for LOD management
* @param {THREE.Object3D} object - The object to manage
* @param {Object} lodLevels - LOD level configurations
* lodLevels = {
* high: { geometry: THREE.BufferGeometry, material: THREE.Material },
* medium: { geometry: THREE.BufferGeometry, material: THREE.Material },
* low: { geometry: THREE.BufferGeometry, material: THREE.Material },
* }
*/
registerObject(object, lodLevels) {
const uuid = object.uuid;
// Store original object data
const original = {
geometry: object.geometry,
material: object.material,
position: object.position.clone(),
rotation: object.rotation.clone(),
scale: object.scale.clone(),
};
// Create LOD meshes
const levels = {};
for (const [levelName, config] of Object.entries(lodLevels)) {
const mesh = new THREE.Mesh(config.geometry, config.material);
mesh.position.copy(original.position);
mesh.rotation.copy(original.rotation);
mesh.scale.copy(original.scale);
mesh.visible = false;
mesh.userData.lodLevel = levelName;
mesh.userData.parentUUID = uuid;
this.scene.add(mesh);
levels[levelName] = mesh;
}
// Store LOD data
this.lodObjects.set(uuid, {
object,
original,
levels,
currentLevel: 'high',
});
// Hide original, show high-detail
object.visible = false;
levels.high.visible = true;
this.stats.totalObjects++;
this.stats.highDetail++;
}
/**
* Create LOD levels for a sphere (agent orbs)
*/
static createSphereLODs(radius, color, emissiveIntensity = 2) {
return {
high: {
geometry: new THREE.SphereGeometry(radius, 32, 32),
material: new THREE.MeshPhysicalMaterial({
color: color,
emissive: color,
emissiveIntensity: emissiveIntensity,
roughness: 0,
metalness: 1,
transmission: 0.8,
thickness: 0.5,
}),
},
medium: {
geometry: new THREE.SphereGeometry(radius, 16, 16),
material: new THREE.MeshStandardMaterial({
color: color,
emissive: color,
emissiveIntensity: emissiveIntensity * 0.8,
roughness: 0.2,
metalness: 0.8,
}),
},
low: {
geometry: new THREE.SphereGeometry(radius, 8, 8),
material: new THREE.MeshBasicMaterial({
color: color,
}),
},
};
}
/**
* Create LOD levels for a torus (halos)
*/
static createTorusLODs(radius, tube, color) {
return {
high: {
geometry: new THREE.TorusGeometry(radius, tube, 16, 64),
material: new THREE.MeshBasicMaterial({
color: color,
transparent: true,
opacity: 0.4,
}),
},
medium: {
geometry: new THREE.TorusGeometry(radius, tube, 12, 32),
material: new THREE.MeshBasicMaterial({
color: color,
transparent: true,
opacity: 0.3,
}),
},
low: {
geometry: new THREE.TorusGeometry(radius, tube * 1.5, 8, 16),
material: new THREE.MeshBasicMaterial({
color: color,
transparent: true,
opacity: 0.2,
}),
},
};
}
/**
* Create LOD levels for a cylinder (pillars)
*/
static createCylinderLODs(radiusTop, radiusBottom, height, color) {
return {
high: {
geometry: new THREE.CylinderGeometry(radiusTop, radiusBottom, height, 32),
material: new THREE.MeshStandardMaterial({
color: color,
metalness: 0.7,
roughness: 0.3,
}),
},
medium: {
geometry: new THREE.CylinderGeometry(radiusTop, radiusBottom, height, 16),
material: new THREE.MeshStandardMaterial({
color: color,
metalness: 0.5,
ground: 0.5,
}),
},
low: {
geometry: new THREE.CylinderGeometry(radiusTop, radiusBottom, height, 8),
material: new THREE.MeshBasicMaterial({
color: color,
}),
},
};
}
/**
* Update LOD levels based on camera distance
*/
update(deltaTime) {
this.lastUpdate += deltaTime;
if (this.lastUpdate < this.updateInterval) return;
this.lastUpdate = 0;
const cameraPos = this.camera.position;
// Reset stats
this.stats.highDetail = 0;
this.stats.mediumDetail = 0;
this.stats.lowDetail = 0;
this.stats.culled = 0;
for (const [uuid, lodData] of this.lodObjects) {
const distance = cameraPos.distanceTo(lodData.object.position);
// Determine target LOD level
let targetLevel;
if (distance < this.distanceThresholds.high) {
targetLevel = 'high';
} else if (distance < this.distanceThresholds.medium) {
targetLevel = 'medium';
} else if (distance < this.distanceThresholds.low) {
targetLevel = 'low';
} else {
targetLevel = 'culled';
}
// Update LOD if changed
if (targetLevel !== lodData.currentLevel) {
// Hide current level
if (lodData.levels[lodData.currentLevel]) {
lodData.levels[lodData.currentLevel].visible = false;
}
// Show new level (or cull)
if (targetLevel !== 'culled' && lodData.levels[targetLevel]) {
lodData.levels[targetLevel].visible = true;
}
lodData.currentLevel = targetLevel;
}
// Update stats
switch (targetLevel) {
case 'high': this.stats.highDetail++; break;
case 'medium': this.stats.mediumDetail++; break;
case 'low': this.stats.lowDetail++; break;
case 'culled': this.stats.culled++; break;
}
}
}
/**
* Get current LOD statistics
*/
getStats() {
return { ...this.stats };
}
/**
* Set distance thresholds
*/
setThresholds(high, medium, low, cull) {
this.distanceThresholds = { high, medium, low, cull };
}
/**
* Remove object from LOD management
*/
unregisterObject(uuid) {
const lodData = this.lodObjects.get(uuid);
if (!lodData) return;
// Remove LOD meshes from scene
for (const mesh of Object.values(lodData.levels)) {
this.scene.remove(mesh);
mesh.geometry.dispose();
mesh.material.dispose();
}
// Restore original object visibility
lodData.object.visible = true;
this.lodObjects.delete(uuid);
this.stats.totalObjects--;
}
/**
* Cleanup all LOD objects
*/
dispose() {
for (const [uuid] of this.lodObjects) {
this.unregisterObject(uuid);
}
}
}
// Export singleton instance
export const lodManager = new LODManager();

View File

@@ -1,38 +0,0 @@
/**
* ═══════════════════════════════════════════
* NEXUS LOD SYSTEM TEST — Verification Script
* ═══════════════════════════════════════════
*
* Simple test to verify LOD system functionality.
*/
import { LODManager } from './lod-manager.js';
// Test LOD creation
console.log('Testing LOD system...');
// Test sphere LODs
const sphereLods = LODManager.createSphereLODs(0.5, 0xff0000, 2);
console.log('Sphere LODs:', {
high: sphereLods.high.geometry.parameters,
medium: sphereLods.medium.geometry.parameters,
low: sphereLods.low.geometry.parameters,
});
// Test torus LODs
const torusLods = LODManager.createTorusLODs(0.6, 0.02, 0x00ff00);
console.log('Torus LODs:', {
high: torusLods.high.geometry.parameters,
medium: torusLods.medium.geometry.parameters,
low: torusLods.low.geometry.parameters,
});
// Test cylinder LODs
const cylinderLods = LODManager.createCylinderLODs(0.3, 0.3, 2, 0x0000ff);
console.log('Cylinder LODs:', {
high: cylinderLods.high.geometry.parameters,
medium: cylinderLods.medium.geometry.parameters,
low: cylinderLods.low.geometry.parameters,
});
console.log('LOD system test complete!');

View File

@@ -1,294 +0,0 @@
/**
* ═══════════════════════════════════════════
* NEXUS PERFORMANCE INTEGRATION — LOD + Texture Audit + Stats
* ═══════════════════════════════════════════
*
* Integrates LOD system, texture auditing, and performance
* monitoring into the main Nexus application.
*/
import { LODManager } from './lod-manager.js';
import { TextureAuditor } from './texture-auditor.js';
import { PerformanceMonitor } from './performance-monitor.js';
export class PerformanceSystem {
constructor(camera, scene, renderer) {
this.camera = camera;
this.scene = scene;
this.renderer = renderer;
// Initialize subsystems
this.lodManager = new LODManager(camera, scene);
this.textureAuditor = new TextureAuditor();
this.performanceMonitor = new PerformanceMonitor();
// State
this.isEnabled = true;
this.autoLOD = true;
this.autoAudit = true;
this.lastAuditTime = 0;
this.auditInterval = 30; // seconds between audits
// Performance tiers for local hardware
this.hardwareTiers = {
high: {
name: 'High (M1 Pro/Max/Ultra)',
description: 'M1 Pro or better, 16GB+ RAM',
targetFPS: 60,
maxDrawCalls: 2000,
maxTriangles: 1000000,
maxTextures: 100,
lodThresholds: { high: 20, medium: 40, low: 60, cull: 100 },
},
medium: {
name: 'Medium (M1/M2)',
description: 'Base M1 or M2, 8GB+ RAM',
targetFPS: 45,
maxDrawCalls: 1000,
maxTriangles: 500000,
maxTextures: 50,
lodThresholds: { high: 15, medium: 30, low: 50, cull: 80 },
},
low: {
name: 'Low (Intel Mac / Older)',
description: 'Intel Mac or older hardware',
targetFPS: 30,
maxDrawCalls: 500,
maxTriangles: 200000,
maxTextures: 25,
lodThresholds: { high: 10, medium: 20, low: 40, cull: 60 },
},
};
this.currentTier = 'medium'; // Default to medium
}
/**
* Initialize the performance system
*/
async init() {
console.log('[PerformanceSystem] Initializing...');
// Initialize performance monitor
await this.performanceMonitor.init();
// Detect hardware tier
await this.detectHardwareTier();
// Apply tier settings
this.applyTierSettings();
// Run initial texture audit
if (this.autoAudit) {
this.runTextureAudit();
}
console.log(`[PerformanceSystem] Initialized with tier: ${this.currentTier}`);
return this;
}
/**
* Detect appropriate hardware tier
*/
async detectHardwareTier() {
// Use WebGL renderer info for detection
const gl = this.renderer.getContext();
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
if (debugInfo) {
const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
console.log(`[PerformanceSystem] Detected GPU: ${renderer}`);
// Simple heuristic based on renderer string
if (renderer.includes('Apple M1 Max') || renderer.includes('Apple M1 Ultra') ||
renderer.includes('Apple M2 Pro') || renderer.includes('Apple M2 Max') ||
renderer.includes('Apple M3') || renderer.includes('Apple M4')) {
this.currentTier = 'high';
} else if (renderer.includes('Apple M1') || renderer.includes('Apple M2')) {
this.currentTier = 'medium';
} else {
this.currentTier = 'low';
}
} else {
// Fallback: assume medium
console.log('[PerformanceSystem] Could not detect GPU, assuming medium tier');
this.currentTier = 'medium';
}
}
/**
* Apply settings for current hardware tier
*/
applyTierSettings() {
const tier = this.hardwareTiers[this.currentTier];
if (!tier) return;
// Set LOD thresholds
this.lodManager.setThresholds(
tier.lodThresholds.high,
tier.lodThresholds.medium,
tier.lodThresholds.low,
tier.lodThresholds.cull
);
// Set texture auditor limits
this.textureAuditor.maxTotalTextures = tier.maxTextures;
console.log(`[PerformanceSystem] Applied ${tier.name} settings`);
console.log(` Target FPS: ${tier.targetFPS}`);
console.log(` Max draw calls: ${tier.maxDrawCalls}`);
console.log(` Max triangles: ${tier.maxTriangles}`);
console.log(` Max textures: ${tier.maxTextures}`);
}
/**
* Update the performance system
*/
update(deltaTime) {
if (!this.isEnabled) return;
// Update LOD system
if (this.autoLOD) {
this.lodManager.update(deltaTime);
}
// Update performance monitor
this.performanceMonitor.update(this.renderer, this.scene, deltaTime);
// Periodic texture audit
this.lastAuditTime += deltaTime;
if (this.autoAudit && this.lastAuditTime > this.auditInterval) {
this.lastAuditTime = 0;
this.runTextureAudit();
}
}
/**
* Run texture audit
*/
runTextureAudit() {
console.log('[PerformanceSystem] Running texture audit...');
this.textureAuditor.clear();
const sceneAudit = this.textureAuditor.auditScene(this.scene);
const compressionPlan = this.textureAuditor.generateCompressionPlan();
// Store results
this.lastAudit = {
sceneAudit,
compressionPlan,
timestamp: Date.now(),
};
return this.lastAudit;
}
/**
* Register an object for LOD management
*/
registerForLOD(object, lodLevels) {
this.lodManager.registerObject(object, lodLevels);
}
/**
* Get performance report
*/
getPerformanceReport() {
const monitorReport = this.performanceMonitor.getReport();
const lodStats = this.lodManager.getStats();
return {
timestamp: Date.now(),
tier: this.currentTier,
tierInfo: this.hardwareTiers[this.currentTier],
monitor: monitorReport,
lod: lodStats,
textureAudit: this.lastAudit || null,
};
}
/**
* Get minimum hardware requirements based on current scene
*/
getMinimumHardwareRequirements() {
const report = this.getPerformanceReport();
const requirements = {
recommended: {
tier: report.tier,
description: report.tierInfo.description,
targetFPS: report.tierInfo.targetFPS,
notes: [],
},
minimum: {
tier: 'low',
description: this.hardwareTiers.low.description,
targetFPS: this.hardwareTiers.low.targetFPS,
notes: [],
},
};
// Generate notes based on current scene complexity
if (report.monitor.metrics.drawCalls.current > 1000) {
requirements.minimum.notes.push('Scene has high draw call count. LOD system required on lower-end hardware.');
}
if (report.monitor.metrics.triangles.current > 500000) {
requirements.minimum.notes.push('High triangle count. Reduce geometry complexity or use LOD on lower-end hardware.');
}
if (report.lod.totalObjects > 10) {
requirements.recommended.notes.push(`LOD system managing ${report.lod.totalObjects} objects.`);
}
return requirements;
}
/**
* Set hardware tier manually
*/
setHardwareTier(tier) {
if (this.hardwareTiers[tier]) {
this.currentTier = tier;
this.applyTierSettings();
console.log(`[PerformanceSystem] Manually set to ${this.hardwareTiers[tier].name}`);
}
}
/**
* Toggle performance system
*/
toggle() {
this.isEnabled = !this.isEnabled;
console.log(`[PerformanceSystem] ${this.isEnabled ? 'Enabled' : 'Disabled'}`);
return this.isEnabled;
}
/**
* Toggle LOD system
*/
toggleLOD() {
this.autoLOD = !this.autoLOD;
console.log(`[PerformanceSystem] LOD ${this.autoLOD ? 'Enabled' : 'Disabled'}`);
return this.autoLOD;
}
/**
* Toggle texture auditing
*/
toggleAudit() {
this.autoAudit = !this.autoAudit;
console.log(`[PerformanceSystem] Texture auditing ${this.autoAudit ? 'Enabled' : 'Disabled'}`);
return this.autoAudit;
}
/**
* Cleanup
*/
dispose() {
this.lodManager.dispose();
this.performanceMonitor.dispose();
this.isEnabled = false;
}
}
// Export singleton instance
export const performanceSystem = new PerformanceSystem();

View File

@@ -1,264 +0,0 @@
/**
* ═══════════════════════════════════════════
* NEXUS PERFORMANCE MONITOR — stats.js Integration
* ═══════════════════════════════════════════
*
* Provides real-time performance monitoring using stats.js
* and custom metrics for LOD and texture systems.
*/
// Import stats.js from CDN
const Stats = window.Stats;
export class PerformanceMonitor {
constructor(container = document.body) {
this.stats = null;
this.customPanels = {};
this.isInitialized = false;
this.metrics = {
fps: { current: 0, min: Infinity, max: 0, avg: 0, history: [] },
frameTime: { current: 0, min: Infinity, max: 0, avg: 0, history: [] },
drawCalls: { current: 0, min: Infinity, max: 0, avg: 0, history: [] },
triangles: { current: 0, min: Infinity, max: 0, avg: 0, history: [] },
textures: { current: 0, min: Infinity, max: 0, avg: 0, history: [] },
geometries: { current: 0, min: Infinity, max: 0, avg: 0, history: [] },
};
this.historyLength = 60; // Store 60 samples
this.updateInterval = 0.5; // Update stats every 0.5s
this.lastUpdate = 0;
this.container = container;
}
/**
* Initialize the performance monitor
*/
async init() {
if (this.isInitialized) return;
// Dynamically load stats.js if not available
if (typeof Stats === 'undefined') {
await this.loadStatsJS();
}
// Create stats.js instance
this.stats = new Stats();
this.stats.dom.style.position = 'absolute';
this.stats.dom.style.top = '0px';
this.stats.dom.style.left = '0px';
this.stats.dom.style.zIndex = '10000';
this.stats.dom.id = 'nexus-stats';
// Create custom panels
this.createCustomPanel('drawCalls', '#ff8c00', '#1a1a1a', 'Draw Calls');
this.createCustomPanel('triangles', '#00ff8c', '#1a1a1a', 'Triangles');
this.createCustomPanel('textures', '#ff008c', '#1a1a1a', 'Textures');
this.createCustomPanel('geometries', '#008cff', '#1a1a1a', 'Geometries');
// Add to container
this.container.appendChild(this.stats.dom);
// Add custom panels
let topOffset = 48;
for (const panel of Object.values(this.customPanels)) {
panel.dom.style.top = `${topOffset}px`;
this.container.appendChild(panel.dom);
topOffset += 48;
}
this.isInitialized = true;
console.log('[PerformanceMonitor] Initialized with stats.js');
}
/**
* Load stats.js from CDN
*/
async loadStatsJS() {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/stats.js@0.17.0/build/stats.min.js';
script.onload = () => {
console.log('[PerformanceMonitor] stats.js loaded');
resolve();
};
script.onerror = () => {
console.error('[PerformanceMonitor] Failed to load stats.js');
reject(new Error('Failed to load stats.js'));
};
document.head.appendChild(script);
});
}
/**
* Create a custom stats panel
*/
createCustomPanel(name, fg, bg, label) {
const panel = new Stats.Panel(label, fg, bg);
const container = document.createElement('div');
container.style.cssText = `
position: absolute;
top: 48px;
left: 0px;
cursor: pointer;
opacity: 0.9;
z-index: 10000;
`;
container.appendChild(panel.dom);
this.customPanels[name] = {
panel,
dom: container,
label,
};
}
/**
* Update performance metrics
*/
update(renderer, scene, deltaTime) {
if (!this.isInitialized) return;
// Update stats.js FPS panel
this.stats.update();
// Update custom panels every interval
this.lastUpdate += deltaTime;
if (this.lastUpdate < this.updateInterval) return;
this.lastUpdate = 0;
// Get renderer info
const info = renderer.info;
// Update metrics
this.updateMetric('drawCalls', info.render.calls);
this.updateMetric('triangles', info.render.triangles);
this.updateMetric('textures', info.memory.textures);
this.updateMetric('geometries', info.memory.geometries);
// Update custom panels
this.updateCustomPanel('drawCalls', info.render.calls);
this.updateCustomPanel('triangles', info.render.triangles);
this.updateCustomPanel('textures', info.memory.textures);
this.updateCustomPanel('geometries', info.memory.geometries);
// Update FPS metric
const fps = 1 / deltaTime;
this.updateMetric('fps', fps);
this.updateMetric('frameTime', deltaTime * 1000); // ms
}
/**
* Update a single metric
*/
updateMetric(name, value) {
const metric = this.metrics[name];
metric.current = value;
metric.min = Math.min(metric.min, value);
metric.max = Math.max(metric.max, value);
// Add to history
metric.history.push(value);
if (metric.history.length > this.historyLength) {
metric.history.shift();
}
// Calculate average
metric.avg = metric.history.reduce((a, b) => a + b, 0) / metric.history.length;
}
/**
* Update a custom panel
*/
updateCustomPanel(name, value) {
const panel = this.customPanels[name];
if (panel) {
panel.panel.update(value, 1000); // Scale to 1000 for visibility
}
}
/**
* Get performance report
*/
getReport() {
const report = {
timestamp: Date.now(),
metrics: {},
recommendations: [],
score: 100,
};
for (const [name, metric] of Object.entries(this.metrics)) {
report.metrics[name] = {
current: metric.current,
min: metric.min,
max: metric.max,
avg: metric.avg,
history: [...metric.history],
};
}
// Generate recommendations based on metrics
if (this.metrics.fps.avg < 30) {
report.recommendations.push('Average FPS below 30. Consider reducing scene complexity.');
report.score -= 30;
} else if (this.metrics.fps.avg < 45) {
report.recommendations.push('Average FPS below 45. LOD system should help.');
report.score -= 15;
}
if (this.metrics.drawCalls.avg > 1000) {
report.recommendations.push('High draw call count. Consider merging geometries.');
report.score -= 20;
}
if (this.metrics.triangles.avg > 500000) {
report.recommendations.push('High triangle count. Use LOD for complex meshes.');
report.score -= 15;
}
if (this.metrics.textures.avg > 50) {
report.recommendations.push('Too many textures. Consider texture atlasing.');
report.score -= 10;
}
report.score = Math.max(0, report.score);
return report;
}
/**
* Show/hide the monitor
*/
setVisible(visible) {
if (this.stats) {
this.stats.dom.style.display = visible ? 'block' : 'none';
}
for (const panel of Object.values(this.customPanels)) {
panel.dom.style.display = visible ? 'block' : 'none';
}
}
/**
* Toggle visibility
*/
toggle() {
if (this.stats) {
const current = this.stats.dom.style.display !== 'none';
this.setVisible(!current);
}
}
/**
* Cleanup
*/
dispose() {
if (this.stats) {
this.container.removeChild(this.stats.dom);
}
for (const panel of Object.values(this.customPanels)) {
this.container.removeChild(panel.dom);
}
this.isInitialized = false;
}
}
// Export singleton instance
export const performanceMonitor = new PerformanceMonitor();

View File

@@ -1,234 +0,0 @@
/**
* ═══════════════════════════════════════════
* NEXUS TEXTURE AUDIT — Compression & Optimization
* ═══════════════════════════════════════════
*
* Audits textures for performance on local hardware.
* Provides compression recommendations and optimization.
*/
import * as THREE from 'three';
export class TextureAuditor {
constructor() {
this.textureCache = new Map();
this.compressionFormats = {
webp: { extension: '.webp', mimeType: 'image/webp', quality: 0.8 },
basis: { extension: '.basis', mimeType: 'application/octet-stream' },
ktx2: { extension: '.ktx2', mimeType: 'image/ktx2' },
};
this.auditResults = [];
this.maxTextureSize = 2048; // Max texture size for M1 Mac
this.maxTotalTextures = 50; // Max textures in scene
this.maxTotalVRAM = 256 * 1024 * 1024; // 256MB VRAM budget
}
/**
* Audit a texture for performance issues
*/
auditTexture(texture, name = 'unknown') {
const issues = [];
const recommendations = [];
let score = 100;
// Check texture size
if (texture.image) {
const width = texture.image.width || 0;
const height = texture.image.height || 0;
const pixels = width * height;
const estimatedVRAM = pixels * 4; // RGBA
if (width > this.maxTextureSize || height > this.maxTextureSize) {
issues.push(`Texture too large: ${width}x${height} (max: ${this.maxTextureSize}x${this.maxTextureSize})`);
recommendations.push(`Resize to ${this.maxTextureSize}x${this.maxTextureSize} or smaller`);
score -= 30;
}
if (estimatedVRAM > 16 * 1024 * 1024) { // >16MB
issues.push(`High VRAM usage: ${(estimatedVRAM / 1024 / 1024).toFixed(1)}MB`);
recommendations.push('Use compressed texture format (WebP, Basis, or KTX2)');
score -= 20;
}
// Check if power of two
if (!this.isPowerOfTwo(width) || !this.isPowerOfTwo(height)) {
issues.push('Texture dimensions not power of two');
recommendations.push('Resize to nearest power of two (e.g., 512x512, 1024x1024)');
score -= 15;
}
}
// Check format
if (texture.format === THREE.RGBAFormat && texture.type === THREE.UnsignedByteType) {
// Uncompressed RGBA
recommendations.push('Consider using compressed format for better performance');
score -= 10;
}
// Check filtering
if (texture.minFilter === THREE.LinearFilter || texture.magFilter === THREE.LinearFilter) {
// Linear filtering is more expensive
if (texture.generateMipmaps) {
recommendations.push('Use mipmaps with linear filtering for better quality/performance');
}
}
// Check wrapping
if (texture.wrapS === THREE.RepeatWrapping || texture.wrapT === THREE.RepeatWrapping) {
// Repeating textures can cause issues with compressed formats
if (texture.image && (!this.isPowerOfTwo(texture.image.width) || !this.isPowerOfTwo(texture.image.height))) {
issues.push('Repeating texture with non-power-of-two dimensions');
score -= 10;
}
}
const result = {
name,
texture,
issues,
recommendations,
score: Math.max(0, score),
timestamp: Date.now(),
};
this.auditResults.push(result);
return result;
}
/**
* Audit all textures in a scene
*/
auditScene(scene) {
const textures = new Set();
scene.traverse((object) => {
if (object.material) {
const materials = Array.isArray(object.material) ? object.material : [object.material];
for (const material of materials) {
for (const key in material) {
if (material[key] && material[key] instanceof THREE.Texture) {
textures.add(material[key]);
}
}
}
}
});
console.log(`Found ${textures.size} textures in scene`);
let totalVRAM = 0;
const textureList = Array.from(textures);
for (let i = 0; i < textureList.length; i++) {
const texture = textureList[i];
const name = `texture_${i}`;
const result = this.auditTexture(texture, name);
if (texture.image) {
const width = texture.image.width || 0;
const height = texture.image.height || 0;
totalVRAM += width * height * 4;
}
console.log(`Texture ${name}: Score ${result.score}/100`);
if (result.issues.length > 0) {
console.log(` Issues: ${result.issues.join(', ')}`);
}
}
// Overall scene audit
const sceneAudit = {
totalTextures: textures.size,
totalVRAM: totalVRAM,
totalVRAMMB: (totalVRAM / 1024 / 1024).toFixed(1),
averageScore: this.auditResults.reduce((sum, r) => sum + r.score, 0) / this.auditResults.length,
exceedsTextureLimit: textures.size > this.maxTotalTextures,
exceedsVRAMLimit: totalVRAM > this.maxTotalVRAM,
};
console.log('\n=== Scene Texture Audit ===');
console.log(`Total textures: ${sceneAudit.totalTextures}`);
console.log(`Total VRAM: ${sceneAudit.totalVRAMMB}MB`);
console.log(`Average score: ${sceneAudit.averageScore.toFixed(1)}/100`);
console.log(`Texture limit exceeded: ${sceneAudit.exceedsTextureLimit}`);
console.log(`VRAM limit exceeded: ${sceneAudit.exceedsVRAMLimit}`);
return sceneAudit;
}
/**
* Generate compression recommendations
*/
generateCompressionPlan() {
const plan = {
webpCandidates: [],
basisCandidates: [],
resizeCandidates: [],
totalSavings: 0,
};
for (const result of this.auditResults) {
const texture = result.texture;
if (!texture.image) continue;
const width = texture.image.width || 0;
const height = texture.image.height || 0;
const currentSize = width * height * 4; // RGBA uncompressed
if (width > 1024 || height > 1024) {
const targetSize = Math.min(width, height, 1024);
const newSize = targetSize * targetSize * 4;
const savings = currentSize - newSize;
plan.resizeCandidates.push({
name: result.name,
currentSize: `${width}x${height}`,
targetSize: `${targetSize}x${targetSize}`,
savingsMB: (savings / 1024 / 1024).toFixed(1),
});
plan.totalSavings += savings;
}
if (currentSize > 4 * 1024 * 1024) { // >4MB
const webpSavings = currentSize * 0.7; // ~30% savings with WebP
plan.webpCandidates.push({
name: result.name,
currentSizeMB: (currentSize / 1024 / 1024).toFixed(1),
estimatedSavingsMB: (webpSavings / 1024 / 1024).toFixed(1),
});
plan.totalSavings += webpSavings;
}
}
console.log('\n=== Compression Plan ===');
console.log(`Textures to resize: ${plan.resizeCandidates.length}`);
console.log(`Textures for WebP: ${plan.webpCandidates.length}`);
console.log(`Estimated total savings: ${(plan.totalSavings / 1024 / 1024).toFixed(1)}MB`);
return plan;
}
/**
* Check if number is power of two
*/
isPowerOfTwo(n) {
return n !== 0 && (n & (n - 1)) === 0;
}
/**
* Get audit results
*/
getResults() {
return this.auditResults;
}
/**
* Clear audit results
*/
clear() {
this.auditResults = [];
}
}
// Export singleton instance
export const textureAuditor = new TextureAuditor();

View File

@@ -1,254 +0,0 @@
#!/usr/bin/env node
/**
* ═══════════════════════════════════════════
* NEXUS TEXTURE AUDIT CLI — Standalone Audit Tool
* ═══════════════════════════════════════════
*
* Command-line tool to audit textures in the Nexus project.
* Provides compression recommendations and VRAM estimates.
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
// Configuration
const CONFIG = {
maxTextureSize: 2048,
maxTotalTextures: 50,
maxTotalVRAM: 256 * 1024 * 1024, // 256MB
textureExtensions: ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.basis', '.ktx2'],
imageMagickPath: 'convert', // Path to ImageMagick convert
};
class TextureAuditor {
constructor(projectRoot) {
this.projectRoot = projectRoot;
this.textureFiles = [];
this.auditResults = [];
this.totalVRAM = 0;
}
/**
* Scan project for texture files
*/
scanForTextures() {
console.log(`Scanning ${this.projectRoot} for textures...`);
const scanDir = (dir) => {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
// Skip node_modules and .git
if (entry.name !== 'node_modules' && entry.name !== '.git') {
scanDir(fullPath);
}
} else if (entry.isFile()) {
const ext = path.extname(entry.name).toLowerCase();
if (CONFIG.textureExtensions.includes(ext)) {
this.textureFiles.push(fullPath);
}
}
}
};
scanDir(this.projectRoot);
console.log(`Found ${this.textureFiles.length} texture files`);
return this.textureFiles;
}
/**
* Audit a single texture file
*/
auditTexture(filePath) {
const result = {
file: path.relative(this.projectRoot, filePath),
issues: [],
recommendations: [],
score: 100,
};
try {
const stats = fs.statSync(filePath);
const fileSize = stats.size;
const ext = path.extname(filePath).toLowerCase();
// Get image dimensions if possible
let width = 0;
let height = 0;
try {
const identify = execSync(`${CONFIG.imageMagickPath} -format "%wx%h" "${filePath}"`, { encoding: 'utf8' });
const match = identify.match(/(\d+)x(\d+)/);
if (match) {
width = parseInt(match[1]);
height = parseInt(match[2]);
}
} catch (e) {
// ImageMagick not available, skip dimension check
}
// Calculate estimated VRAM (RGBA)
const vram = width * height * 4;
this.totalVRAM += vram;
// Check file size
if (fileSize > 10 * 1024 * 1024) { // >10MB
result.issues.push(`Large file size: ${(fileSize / 1024 / 1024).toFixed(1)}MB`);
result.recommendations.push('Consider compressing or using a different format');
result.score -= 20;
}
// Check dimensions
if (width > CONFIG.maxTextureSize || height > CONFIG.maxTextureSize) {
result.issues.push(`Texture too large: ${width}x${height} (max: ${CONFIG.maxTextureSize}x${CONFIG.maxTextureSize})`);
result.recommendations.push(`Resize to ${CONFIG.maxTextureSize}x${CONFIG.maxTextureSize} or smaller`);
result.score -= 30;
}
// Check if power of two
if (width > 0 && height > 0) {
if (!this.isPowerOfTwo(width) || !this.isPowerOfTwo(height)) {
result.issues.push('Texture dimensions not power of two');
result.recommendations.push('Resize to nearest power of two (e.g., 512x512, 1024x1024)');
result.score -= 15;
}
}
// Check format
if (ext === '.png' || ext === '.jpg' || ext === '.jpeg') {
result.recommendations.push('Consider using WebP for better compression');
result.score -= 10;
}
// Check VRAM usage
if (vram > 16 * 1024 * 1024) { // >16MB
result.issues.push(`High VRAM usage: ${(vram / 1024 / 1024).toFixed(1)}MB`);
result.recommendations.push('Use compressed texture format (WebP, Basis, or KTX2)');
result.score -= 20;
}
} catch (error) {
result.issues.push(`Error reading file: ${error.message}`);
result.score = 0;
}
result.score = Math.max(0, result.score);
this.auditResults.push(result);
return result;
}
/**
* Run full audit
*/
audit() {
this.scanForTextures();
console.log('\n=== Texture Audit Results ===\n');
let totalScore = 0;
let issuesFound = 0;
for (const file of this.textureFiles) {
const result = this.auditTexture(file);
totalScore += result.score;
issuesFound += result.issues.length;
if (result.issues.length > 0) {
console.log(`\n${result.file}:`);
console.log(` Score: ${result.score}/100`);
result.issues.forEach(issue => console.log(` ⚠️ ${issue}`));
result.recommendations.forEach(rec => console.log(` 💡 ${rec}`));
}
}
// Summary
console.log('\n=== Audit Summary ===');
console.log(`Total textures: ${this.textureFiles.length}`);
console.log(`Total VRAM: ${(this.totalVRAM / 1024 / 1024).toFixed(1)}MB`);
console.log(`Average score: ${(totalScore / this.textureFiles.length).toFixed(1)}/100`);
console.log(`Issues found: ${issuesFound}`);
console.log(`Texture limit: ${this.textureFiles.length > CONFIG.maxTotalTextures ? 'EXCEEDED' : 'OK'}`);
console.log(`VRAM limit: ${this.totalVRAM > CONFIG.maxTotalVRAM ? 'EXCEEDED' : 'OK'}`);
// Generate compression plan
this.generateCompressionPlan();
}
/**
* Generate compression plan
*/
generateCompressionPlan() {
console.log('\n=== Compression Plan ===');
const webpCandidates = [];
const resizeCandidates = [];
for (const result of this.auditResults) {
if (result.score < 80) {
const ext = path.extname(result.file).toLowerCase();
if (ext === '.png' || ext === '.jpg' || ext === '.jpeg') {
webpCandidates.push(result.file);
}
if (result.issues.some(i => i.includes('too large'))) {
resizeCandidates.push(result.file);
}
}
}
console.log(`Textures to convert to WebP: ${webpCandidates.length}`);
webpCandidates.forEach(f => console.log(` 📦 ${f}`));
console.log(`Textures to resize: ${resizeCandidates.length}`);
resizeCandidates.forEach(f => console.log(` 📐 ${f}`));
if (webpCandidates.length > 0) {
console.log('\nTo convert to WebP:');
console.log(' for file in *.png; do cwebp -q 80 "$file" -o "${file%.png}.webp"; done');
}
}
/**
* Check if number is power of two
*/
isPowerOfTwo(n) {
return n !== 0 && (n & (n - 1)) === 0;
}
/**
* Save audit results to JSON
*/
saveResults(outputPath) {
const report = {
timestamp: new Date().toISOString(),
projectRoot: this.projectRoot,
totalTextures: this.textureFiles.length,
totalVRAM: this.totalVRAM,
averageScore: this.auditResults.reduce((sum, r) => sum + r.score, 0) / this.auditResults.length,
results: this.auditResults,
};
fs.writeFileSync(outputPath, JSON.stringify(report, null, 2));
console.log(`\nAudit results saved to: ${outputPath}`);
}
}
// CLI interface
if (require.main === module) {
const projectRoot = process.argv[2] || process.cwd();
const auditor = new TextureAuditor(projectRoot);
auditor.audit();
// Save results if output path provided
if (process.argv[3]) {
auditor.saveResults(process.argv[3]);
}
}
module.exports = TextureAuditor;