// PROCITY core canvas-text helpers — all in-world text is CanvasTexture (no font files). // NOTE for Lane B: per-sign planes are fine for one-offs (tooltips, plates); street signage at // scale must go through a per-chunk atlas (CITY_SPEC perf law) — build that in world/, not here. import * as THREE from 'three'; // Draw text into a canvas → { canvas, w, h } (px). Multi-line via \n. export function textCanvas(text, { font = 'bold 48px Arial', fg = '#222', bg = null, padX = 24, padY = 16, lineGap = 1.15 } = {}) { const lines = String(text).split('\n'); const c = document.createElement('canvas'); const x = c.getContext('2d'); x.font = font; const lineH = parseInt(font.match(/(\d+)px/)?.[1] || 48, 10) * lineGap; const w = Math.ceil(Math.max(...lines.map(l => x.measureText(l).width)) + padX * 2); const h = Math.ceil(lineH * lines.length + padY * 2); c.width = w; c.height = h; if (bg) { x.fillStyle = bg; x.fillRect(0, 0, w, h); } x.font = font; x.fillStyle = fg; x.textBaseline = 'top'; lines.forEach((l, i) => x.fillText(l, padX, padY + i * lineH)); return { canvas: c, w, h }; } // One-off text plane mesh, height in metres, width follows aspect. export function textPlane(text, heightM = 0.4, opts = {}) { const { canvas, w, h } = textCanvas(text, opts); const tex = new THREE.CanvasTexture(canvas); tex.colorSpace = THREE.SRGBColorSpace; tex.anisotropy = 4; const mesh = new THREE.Mesh( new THREE.PlaneGeometry(heightM * (w / h), heightM), new THREE.MeshBasicMaterial({ map: tex, transparent: !opts.bg }) ); mesh.userData.isTextPlane = true; return mesh; }