Compare commits

...

2 Commits

Author SHA1 Message Date
1c18fbf0d1 [claude] InstancedMesh for portal tori + merged tile edge geometry (#415) (#464)
Some checks failed
Deploy Nexus / deploy (push) Failing after 3s
Staging Smoke Test / smoke-test (push) Successful in 6s
Co-authored-by: Claude (Opus 4.6) <claude@hermes.local>
Co-committed-by: Claude (Opus 4.6) <claude@hermes.local>
2026-03-25 01:30:31 +00:00
b4f6ff5222 fix: bust service worker cache to pull latest upstream modules (#467)
Some checks failed
Deploy Nexus / deploy (push) Failing after 8s
Staging Smoke Test / smoke-test (push) Successful in 1s
2026-03-24 23:00:51 +00:00
4 changed files with 71 additions and 43 deletions

View File

@@ -51,10 +51,8 @@ const GLASS_TILE_STEP = GLASS_TILE_SIZE + GLASS_TILE_GAP;
export const GLASS_RADIUS = 4.55;
const tileGeo = new THREE.PlaneGeometry(GLASS_TILE_SIZE, GLASS_TILE_SIZE);
const tileEdgeGeo = new THREE.EdgesGeometry(tileGeo);
/** @type {Array<{mat: THREE.LineBasicMaterial, distFromCenter: number}>} */
export const glassEdgeMaterials = [];
export const glassEdgeMaterials = []; // kept for API compat; no longer populated
const _tileDummy = new THREE.Object3D();
/** @type {Array<{x: number, z: number, distFromCenter: number}>} */
@@ -81,14 +79,26 @@ for (let i = 0; i < _tileSlots.length; i++) {
glassTileIM.instanceMatrix.needsUpdate = true;
glassPlatformGroup.add(glassTileIM);
for (const { x, z, distFromCenter } of _tileSlots) {
const mat = glassEdgeBaseMat.clone();
const edges = new THREE.LineSegments(tileEdgeGeo, mat);
edges.rotation.x = -Math.PI / 2;
edges.position.set(x, 0.002, z);
glassPlatformGroup.add(edges);
glassEdgeMaterials.push({ mat, distFromCenter });
// Merge all tile edge geometry into a single LineSegments draw call.
// Each tile contributes 4 edges (8 vertices). Previously this was 69 separate
// LineSegments objects with cloned materials — a significant draw-call overhead.
const _HS = GLASS_TILE_SIZE / 2;
const _edgeVerts = new Float32Array(_tileSlots.length * 8 * 3);
let _evi = 0;
for (const { x, z } of _tileSlots) {
const y = 0.002;
_edgeVerts[_evi++]=x-_HS; _edgeVerts[_evi++]=y; _edgeVerts[_evi++]=z-_HS;
_edgeVerts[_evi++]=x+_HS; _edgeVerts[_evi++]=y; _edgeVerts[_evi++]=z-_HS;
_edgeVerts[_evi++]=x+_HS; _edgeVerts[_evi++]=y; _edgeVerts[_evi++]=z-_HS;
_edgeVerts[_evi++]=x+_HS; _edgeVerts[_evi++]=y; _edgeVerts[_evi++]=z+_HS;
_edgeVerts[_evi++]=x+_HS; _edgeVerts[_evi++]=y; _edgeVerts[_evi++]=z+_HS;
_edgeVerts[_evi++]=x-_HS; _edgeVerts[_evi++]=y; _edgeVerts[_evi++]=z+_HS;
_edgeVerts[_evi++]=x-_HS; _edgeVerts[_evi++]=y; _edgeVerts[_evi++]=z+_HS;
_edgeVerts[_evi++]=x-_HS; _edgeVerts[_evi++]=y; _edgeVerts[_evi++]=z-_HS;
}
const _mergedEdgeGeo = new THREE.BufferGeometry();
_mergedEdgeGeo.setAttribute('position', new THREE.BufferAttribute(_edgeVerts, 3));
glassPlatformGroup.add(new THREE.LineSegments(_mergedEdgeGeo, glassEdgeBaseMat));
export const voidLight = new THREE.PointLight(NEXUS.colors.accent, 0.5, 14);
voidLight.position.set(0, -3.5, 0);

View File

@@ -10,32 +10,57 @@ scene.add(portalGroup);
export let portals = [];
// Shared geometry and material for all portal tori — populated in createPortals()
const _portalGeo = new THREE.TorusGeometry(3.0, 0.2, 16, 100);
const _portalMat = new THREE.MeshBasicMaterial({
color: 0xffffff, // instance color provides per-portal tint
transparent: true,
opacity: 1.0, // online/offline brightness encoded into instance color
blending: THREE.AdditiveBlending,
side: THREE.DoubleSide,
depthWrite: false,
});
const _portalDummy = new THREE.Object3D();
const _portalColor = new THREE.Color();
/** @type {THREE.InstancedMesh|null} */
let _portalIM = null;
function createPortals() {
const portalGeo = new THREE.TorusGeometry(3.0, 0.2, 16, 100);
// One InstancedMesh for all portal tori — N portals = 1 draw call.
_portalIM = new THREE.InstancedMesh(_portalGeo, _portalMat, portals.length);
_portalIM.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
_portalIM.userData.zoomLabel = 'Portal';
_portalIM.userData.portals = portals; // for instanceId look-up on click
portals.forEach(portal => {
portals.forEach((portal, i) => {
const isOnline = portal.status === 'online';
// Encode online/offline brightness into the instance color so we need
// only one shared material (AdditiveBlending: output = bg + color).
_portalColor.set(portal.color).convertSRGBToLinear()
.multiplyScalar(isOnline ? 0.7 : 0.15);
_portalIM.setColorAt(i, _portalColor);
const portalMat = new THREE.MeshBasicMaterial({
color: new THREE.Color(portal.color).convertSRGBToLinear(),
transparent: true,
opacity: isOnline ? 0.7 : 0.15,
blending: THREE.AdditiveBlending,
side: THREE.DoubleSide,
});
const portalMesh = new THREE.Mesh(portalGeo, portalMat);
portalMesh.position.set(portal.position.x, portal.position.y + 0.5, portal.position.z);
portalMesh.rotation.y = portal.rotation.y;
portalMesh.rotation.x = Math.PI / 2;
portalMesh.name = `portal-${portal.id}`;
portalMesh.userData.destinationUrl = portal.destination?.url || null;
portalMesh.userData.portalColor = new THREE.Color(portal.color).convertSRGBToLinear();
portalGroup.add(portalMesh);
_portalDummy.position.set(portal.position.x, portal.position.y + 0.5, portal.position.z);
_portalDummy.rotation.set(Math.PI / 2, portal.rotation.y || 0, 0);
_portalDummy.updateMatrix();
_portalIM.setMatrixAt(i, _portalDummy.matrix);
});
_portalIM.instanceColor.needsUpdate = true;
_portalIM.instanceMatrix.needsUpdate = true;
portalGroup.add(_portalIM);
}
/** Update per-instance colors after a portal health check. */
export function refreshPortalInstanceColors() {
if (!_portalIM) return;
portals.forEach((portal, i) => {
const brightness = portal.status === 'online' ? 0.7 : 0.15;
_portalColor.set(portal.color).convertSRGBToLinear().multiplyScalar(brightness);
_portalIM.setColorAt(i, _portalColor);
});
_portalIM.instanceColor.needsUpdate = true;
}
// rebuildGravityZones forward ref

View File

@@ -4,6 +4,7 @@ import { scene, ambientLight } from './scene-setup.js';
import { cloudMaterial } from './platform.js';
import { rebuildRuneRing } from './effects.js';
import { S } from './state.js';
import { refreshPortalInstanceColors } from './portals.js';
// === PORTAL HEALTH CHECKS ===
const PORTAL_HEALTH_CHECK_MS = 5 * 60 * 1000;
@@ -41,16 +42,8 @@ export async function runPortalHealthChecks() {
rebuildRuneRing();
if (_rebuildGravityZonesFn) _rebuildGravityZonesFn();
if (_portalGroupRef) {
for (const child of _portalGroupRef.children) {
const portalId = child.name.replace('portal-', '');
const portalData = _portalsRef.find(p => p.id === portalId);
if (portalData) {
const isOnline = portalData.status === 'online';
child.material.opacity = isOnline ? 0.7 : 0.15;
}
}
}
// Refresh portal InstancedMesh colors to reflect new online/offline statuses.
refreshPortalInstanceColors();
}
export function initPortalHealthChecks() {

4
sw.js
View File

@@ -1,8 +1,8 @@
// The Nexus — Service Worker
// Cache-first for assets, network-first for API calls
const CACHE_NAME = 'nexus-v1';
const ASSET_CACHE = 'nexus-assets-v1';
const CACHE_NAME = 'nexus-v3';
const ASSET_CACHE = 'nexus-assets-v3';
const CORE_ASSETS = [
'/',