WeylandAI
Building Better Worlds
E Interact
Click or press E to continue
WASD Move   Mouse Look   E Interact   P Phone   ESC Release
Move
Look
E

WEYLANDAI

Building Better Worlds

SubX — Submittal Intelligence

AI-powered submittal processing. Upload specs, get matched hardware schedules in seconds.

Launch SubX

TakeoffX — Quantity Takeoffs

Point at plans. Get counts, costs, lead times. Real-time construction intelligence.

Launch TakeoffX

AR SiteVision

Walk the job. See what plans say vs what is built.

Launch AR

Pricing

Free Trial
$0
14 days
Professional
$49
/month
Enterprise
$199
/month

WeylandAI © 2026 MobCorp / MASCOM

`; } // ═══════════════════════════════════════════════════════════════════ // CLASSIC DESKTOP VIEW (full screen swap) // ═══════════════════════════════════════════════════════════════════ let classicViewActive = false; function enterClassicView() { const cv = $('classic-view'); cv.innerHTML = classicDesktopHTML(); cv.classList.add('active'); classicViewActive = true; if (locked) document.exitPointerLock(); } window.exitClassicView = function() { $('classic-view').classList.remove('active'); $('classic-view').innerHTML = ''; classicViewActive = false; }; function classicDesktopHTML() { return '' + '
' + '
' + '
WEYLANDAI
' + '
Building Better Worlds
' + '
' // Hero + '
' + '

AI-Powered
Construction Intelligence

' + '

' + 'Upload your RFP. Get submittals, takeoffs, and proposals in minutes — not weeks.

' + '
' + '' + 'Launch SubX' + '
' // Features + '
' + featureCard('SubX', 'Submittal Intelligence', 'Upload specs. AI matches hardware, cross-references standards, flags conflicts. Done in seconds.') + featureCard('TakeoffX', 'Quantity Takeoffs', 'Point at plans. Get counts, costs, lead times. Real-time construction math.') + featureCard('AR SiteVision', 'Augmented Reality', 'Walk the job with your phone. See what the plans say vs what\'s built.') + '
' // Pricing + '

Pricing

' + '
' + priceCard('Free Trial', '$0', '14 days', 'Full access. No card required.') + priceCard('Professional', '$49', '/month', 'Unlimited submittals. 50 takeoffs/mo.') + priceCard('Enterprise', '$199', '/month', 'Unlimited everything. Priority support. API access.') + '
' // Footer + '
' + '

WeylandAI © 2026 MobCorp / MASCOM

'; } function featureCard(name, sub, desc) { return '
' + '
' + name + '
' + '
' + sub + '
' + '
' + desc + '
'; } function priceCard(name, amount, period, desc) { return '
' + '
' + name + '
' + '
' + amount + '
' + '
' + period + '
' + '
' + desc + '
'; } // ═══════════════════════════════════════════════════════════════════ // MINIMAP // ═══════════════════════════════════════════════════════════════════ function updateMinimap() { const mc = $('minimap-canvas'); if (!mc) return; const ctx = mc.getContext('2d'); if (!ctx) return; const w = mc.width, h = mc.height; // World-to-minimap transform: map ~-30..30 world to 0..220 canvas // Z is flipped (negative Z = further into corridor = up on map) const S = 3.4; // px per meter const ox = w/2, oy = h * 0.55; // origin offset (center-ish, shifted down so corridor shows) function wx(x) { return ox + x * S; } function wy(z) { return oy - z * S; } // flip z // Background ctx.fillStyle = 'rgba(8,8,16,0.92)'; ctx.fillRect(0, 0, w, h); // Grid lines (subtle) ctx.strokeStyle = 'rgba(255,255,255, 0.65)'; ctx.lineWidth = 0.5; for (let g = -30; g <= 30; g += 10) { ctx.beginPath(); ctx.moveTo(wx(g), 0); ctx.lineTo(wx(g), h); ctx.stroke(); ctx.beginPath(); ctx.moveTo(0, wy(g)); ctx.lineTo(w, wy(g)); ctx.stroke(); } // ── Foundation slab (32x22m) ── ctx.fillStyle = 'rgba(120,116,104, 0.65)'; ctx.fillRect(wx(-16), wy(11), 32*S, 22*S); ctx.strokeStyle = 'rgba(120,116,104, 0.65)'; ctx.lineWidth = 0.5; ctx.strokeRect(wx(-16), wy(11), 32*S, 22*S); // ── Corridor walls ── ctx.strokeStyle = 'rgba(255,255,255,0.6)'; ctx.lineWidth = 1.5; // West wall (x=-2, z from 8 to -16) with door gaps const westDoors = [-12, -9, -6]; // z positions of door 131,134,137 const eastDoors = [-6, -9, -12]; // z positions of door 138,141,145 const doorW = 0.914; // 3'-0" in meters function drawWallWithDoors(x, zStart, zEnd, doors) { let z = zStart; // Sort doors by z descending (we draw top-to-bottom) const sorted = doors.slice().sort((a,b) => b - a); for (const dz of sorted) { // Wall segment from current z to door top if (z > dz + doorW/2 + 0.01) { ctx.beginPath(); ctx.moveTo(wx(x), wy(z)); ctx.lineTo(wx(x), wy(dz + doorW/2)); ctx.stroke(); } // Door opening (dashed, colored) ctx.strokeStyle = 'rgba(240,184,0,0.5)'; ctx.lineWidth = 1; ctx.setLineDash([2, 2]); ctx.beginPath(); ctx.moveTo(wx(x), wy(dz + doorW/2)); ctx.lineTo(wx(x), wy(dz - doorW/2)); ctx.stroke(); ctx.setLineDash([]); ctx.strokeStyle = 'rgba(255,255,255,0.6)'; ctx.lineWidth = 1.5; z = dz - doorW/2; } // Final segment if (z > zEnd + 0.01) { ctx.beginPath(); ctx.moveTo(wx(x), wy(z)); ctx.lineTo(wx(x), wy(zEnd)); ctx.stroke(); } } drawWallWithDoors(-2, 8, -16, westDoors); drawWallWithDoors(2, 8, -16, eastDoors); // Corridor end walls ctx.beginPath(); ctx.moveTo(wx(-2), wy(-16)); ctx.lineTo(wx(2), wy(-16)); ctx.stroke(); // Corridor entry (open) ctx.strokeStyle = 'rgba(255,255,255, 0.65)'; ctx.setLineDash([2,3]); ctx.beginPath(); ctx.moveTo(wx(-2), wy(8)); ctx.lineTo(wx(2), wy(8)); ctx.stroke(); ctx.setLineDash([]); // ── Door numbers ── ctx.font = '7px monospace'; ctx.fillStyle = 'rgba(240,184,0,0.7)'; ctx.textAlign = 'right'; ctx.fillText('131', wx(-2) - 3, wy(-12) + 2); ctx.fillText('134', wx(-2) - 3, wy(-9) + 2); ctx.fillText('137', wx(-2) - 3, wy(-6) + 2); ctx.textAlign = 'left'; ctx.fillText('138', wx(2) + 3, wy(-6) + 2); ctx.fillText('141', wx(2) + 3, wy(-9) + 2); ctx.fillText('145', wx(2) + 3, wy(-12) + 2); // ── Steel columns (6) ── ctx.fillStyle = 'rgba(90,101,112,0.6)'; const cols = [[-13,-9],[13,-9],[-13,9],[13,9],[-13,0],[13,0]]; for (const [cx2,cz] of cols) { ctx.beginPath(); ctx.arc(wx(cx2), wy(cz), 2, 0, Math.PI*2); ctx.fill(); } // ── Crane ── ctx.strokeStyle = 'rgba(232,200,32,0.4)'; ctx.lineWidth = 1; // Mast ctx.fillStyle = 'rgba(232,200,32,0.5)'; ctx.beginPath(); ctx.arc(wx(-22), wy(-18), 2.5, 0, Math.PI*2); ctx.fill(); // Arm ctx.beginPath(); ctx.moveTo(wx(-28), wy(-18)); ctx.lineTo(wx(2), wy(-18)); ctx.stroke(); // Label ctx.font = '6px monospace'; ctx.fillStyle = 'rgba(232,200,32,0.4)'; ctx.textAlign = 'center'; ctx.fillText('CRANE', wx(-22), wy(-18) + 10); // ── Material staging ── ctx.fillStyle = 'rgba(139,105,20, 0.65)'; ctx.fillRect(wx(5), wy(13), 4*S, 3*S); ctx.strokeStyle = 'rgba(139,105,20,0.4)'; ctx.lineWidth = 0.5; ctx.strokeRect(wx(5), wy(13), 4*S, 3*S); ctx.font = '6px monospace'; ctx.fillStyle = 'rgba(255,255,255, 0.65)'; ctx.textAlign = 'center'; ctx.fillText('STAGING', wx(7), wy(11.5)); // ── Foreman NPC ── ctx.fillStyle = '#f0b800'; ctx.beginPath(); ctx.arc(wx(4), wy(8), 2.5, 0, Math.PI*2); ctx.fill(); ctx.font = '6px monospace'; ctx.fillStyle = 'rgba(240,184,0,0.6)'; ctx.textAlign = 'left'; ctx.fillText('NPC', wx(4) + 5, wy(8) + 2); // ── Portal ── ctx.fillStyle = 'rgba(70,130,180,0.5)'; ctx.beginPath(); ctx.arc(wx(-8), wy(20), 3, 0, Math.PI*2); ctx.fill(); ctx.strokeStyle = 'rgba(70,130,180, 0.65)'; ctx.lineWidth = 0.5; ctx.beginPath(); ctx.arc(wx(-8), wy(20), 5, 0, Math.PI*2); ctx.stroke(); ctx.font = '6px monospace'; ctx.fillStyle = 'rgba(70,130,180,0.5)'; ctx.textAlign = 'center'; ctx.fillText('SubX', wx(-8), wy(20) + 10); // ── Site sign ── ctx.fillStyle = 'rgba(255,255,255, 0.65)'; ctx.fillRect(wx(-3), wy(24.5), 6*S, 1*S); ctx.font = '5px monospace'; ctx.fillStyle = 'rgba(255,255,255, 0.65)'; ctx.textAlign = 'center'; ctx.fillText('WEYLANDAI', wx(0), wy(23.8)); // ── Player position + direction ── const px = wx(player.x), py = wy(player.z); // Direction line const dirLen = 10; const dx = -Math.sin(player.yaw) * dirLen; // yaw=0 is +z, PI is -z const dy = -Math.cos(player.yaw) * dirLen; ctx.strokeStyle = 'rgba(52,211,153,0.5)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(px, py); ctx.lineTo(px + dx, py + dy); ctx.stroke(); // FOV cone const fovHalf = 0.45; const dx1 = -Math.sin(player.yaw - fovHalf) * dirLen; const dy1 = -Math.cos(player.yaw - fovHalf) * dirLen; const dx2 = -Math.sin(player.yaw + fovHalf) * dirLen; const dy2 = -Math.cos(player.yaw + fovHalf) * dirLen; ctx.fillStyle = 'rgba(52,211,153, 0.65)'; ctx.beginPath(); ctx.moveTo(px, py); ctx.lineTo(px+dx1, py+dy1); ctx.lineTo(px+dx2, py+dy2); ctx.closePath(); ctx.fill(); // Player dot ctx.fillStyle = '#34d399'; ctx.beginPath(); ctx.arc(px, py, 3, 0, Math.PI*2); ctx.fill(); ctx.strokeStyle = 'rgba(52,211,153,0.4)'; ctx.lineWidth = 0.5; ctx.beginPath(); ctx.arc(px, py, 5, 0, Math.PI*2); ctx.stroke(); // ── Title ── ctx.font = 'bold 7px monospace'; ctx.fillStyle = 'rgba(255,255,255, 0.65)'; ctx.textAlign = 'left'; ctx.fillText('CAMINO REAL', 4, 10); ctx.font = '6px monospace'; ctx.fillStyle = 'rgba(255,255,255, 0.65)'; ctx.fillText('DSA# 03-124524', 4, 19); } // ═══════════════════════════════════════════════════════════════════ // T-800 EXTRACTION ENGINE // ═══════════════════════════════════════════════════════════════════ function showNotification(title, body, type) { const el = document.createElement('div'); el.className = 'notif ' + (type || 'success'); el.innerHTML = '
' + title + '
' + body + '
'; document.body.appendChild(el); requestAnimationFrame(() => requestAnimationFrame(() => el.classList.add('show'))); setTimeout(() => { el.classList.remove('show'); setTimeout(() => el.remove(), 500); }, 4000); } function showNotificationAction(title, body, type, btnText, onClick) { const el = document.createElement('div'); el.className = 'notif ' + (type || 'success'); el.innerHTML = '
' + title + '
' + body + '
' + ''; document.body.appendChild(el); el.querySelector('.notif-btn').addEventListener('click', () => { el.classList.remove('show'); setTimeout(() => el.remove(), 500); onClick(); }); requestAnimationFrame(() => requestAnimationFrame(() => el.classList.add('show'))); } function showProposal() { togglePhone(true); const iframe = $('phone-iframe'); iframe.srcdoc = proposalHTML(); document.querySelectorAll('.phone-nav-btn').forEach(b => b.classList.remove('active')); } function proposalHTML() { return `

PROPOSAL

Division 08 — Doors, Frames & Hardware
Generated ${new Date().toLocaleDateString('en-US',{month:'long',day:'numeric',year:'numeric'})} by WeylandAI

Door Schedule

MarkSizeTypeQtyUnit
1013'-0" x 7'-0"HM Frame / HM Door1$1,240
1023'-0" x 7'-0"HM Frame / Pair1$2,860
1032'-8" x 7'-0"WD Frame / WD Door1$980

Hardware Schedule

ItemSpecQtyExt
Surface CloserLCN 40412$620
Panic DeviceVon Duprin 99EO1$485
Cont. HingesHager 780-224HD48$14,400
Flush BoltsIves FB51P24$2,160
Butt HingesStanley FBB1796$540
Lever LockSchlage ND80PD2$890
Floor CloserRixson 271$680
OH StopGlynn-Johnson 1001$145
TOTAL$42,680
Lead time: 4-6 weeks from approval. Finish: US26D Satin Chrome throughout. Fire-rated assemblies where noted on plans. Prevailing wage rates included.
Proposal sent.
`; } // ═══════════════════════════════════════════════════════════════════ // GEOLOCATION PIPELINE — Extract address → geocode → satellite tiles // ═══════════════════════════════════════════════════════════════════ function geolocateProject(address) { // For now, known projects map to coordinates. // Future: PDF text extraction → regex address → Nominatim geocode const knownSites = { 'glendale': { lat: 34.2075, lon: -118.2437, name: 'Glendale CC Camino Real' }, 'camino real': { lat: 34.2075, lon: -118.2437, name: 'Glendale CC Camino Real' }, }; // Try known sites first (from hardware_sets.json project name) let site = null; const projectName = 'glendale community college camino real'; for (const [key, val] of Object.entries(knownSites)) { if (projectName.includes(key)) { site = val; break; } } if (!site && address) { // Geocode via Nominatim (free, no key, CORS OK) fetch(`https://nominatim.openstreetmap.org/search?format=json&limit=1&q=${encodeURIComponent(address)}`, { headers: { 'User-Agent': 'WeylandAI/1.0' } }) .then(r => r.json()) .then(results => { if (results.length > 0) { loadSatelliteTiles(parseFloat(results[0].lat), parseFloat(results[0].lon)); showNotification('SITE LOCATED', results[0].display_name, 'success'); } }) .catch(() => {}); return; } if (site) { loadSatelliteTiles(site.lat, site.lon); } } function loadSatelliteTiles(lat, lon) { if (!renderer) return; // Try pre-cached local tile first (sovereign, no external dependency) const cached = new Image(); cached.crossOrigin = 'anonymous'; cached.onload = function() { const c = document.createElement('canvas'); c.width = c.height = 768; const ctx = c.getContext('2d'); ctx.drawImage(cached, 0, 0, 768, 768); renderer.loadTexture(0, c); buildWorldModel(ctx); }; cached.onerror = function() { fetchLiveSatellite(lat, lon); }; cached.src = '/sat_glendale_cc.png'; } function fetchLiveSatellite(lat, lon) { const zoom = 19, gs = 3, ts = 256; const n2 = Math.pow(2, zoom); const cx = Math.floor((lon + 180) / 360 * n2); const lr = lat * Math.PI / 180; const cy = Math.floor((1 - Math.log(Math.tan(lr) + 1 / Math.cos(lr)) / Math.PI) / 2 * n2); const satCanvas = document.createElement('canvas'); satCanvas.width = satCanvas.height = gs * ts; const sctx = satCanvas.getContext('2d'); let loaded = 0; for (let dy = -1; dy <= 1; dy++) { for (let dx = -1; dx <= 1; dx++) { const img = new Image(); img.crossOrigin = 'anonymous'; const px2 = (dx + 1) * ts, py2 = (dy + 1) * ts; img.onload = function() { sctx.drawImage(this, px2, py2, ts, ts); if (++loaded === gs * gs) { renderer.loadTexture(0, satCanvas); buildWorldModel(sctx); } }; img.onerror = function() { sctx.fillStyle = '#555'; sctx.fillRect(px2, py2, ts, ts); if (++loaded === gs * gs) renderer.loadTexture(0, satCanvas); }; img.src = `https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/${zoom}/${cy+dy}/${cx+dx}`; } } } // ─── World Model — Cognitive Reconnaissance ─── // Analyze satellite imagery to infer terrain features: // Trees (green, textured), parking (dark, uniform), buildings (light, sharp edges), // roads (grey, linear), grass (green, smooth) let worldModel = null; function buildWorldModel(ctx) { const gs = 32; const imgData = ctx.getImageData(0, 0, 768, 768); const px = imgData.data; const cellW = 768 / gs, cellH = 768 / gs; const model = { grid: gs, cells: [], heightMap: new Float32Array(gs * gs) }; for (let gy = 0; gy < gs; gy++) { for (let gx = 0; gx < gs; gx++) { let rSum=0, gSum=0, bSum=0, count=0; const samples = []; for (let sy = 0; sy < cellH; sy += 2) { for (let sx = 0; sx < cellW; sx += 2) { const ix = Math.floor(gx * cellW + sx); const iy = Math.floor(gy * cellH + sy); const idx = (iy * 768 + ix) * 4; const r = px[idx]/255, g = px[idx+1]/255, b = px[idx+2]/255; rSum += r; gSum += g; bSum += b; count++; samples.push((r+g+b)/3); } } const avgR = rSum/count, avgG = gSum/count, avgB = bSum/count; const avgL = (avgR + avgG + avgB) / 3; const mean = samples.reduce((a,b)=>a+b,0)/samples.length; const variance = samples.reduce((a,b)=>a+(b-mean)*(b-mean),0)/samples.length; const greenness = avgG - (avgR + avgB) * 0.5; let type = 'ground', height = 0; if (greenness > 0.04 && variance > 0.003) { type = 'tree'; height = 3.0 + variance * 40; } else if (greenness > 0.02 && variance < 0.003) { type = 'grass'; height = 0.05; } else if (avgL < 0.25 && variance < 0.002) { type = 'parking'; height = 0.02; } else if (avgL > 0.55 && variance < 0.004) { type = 'building'; height = 4.0 + avgL * 4; } else if (avgL > 0.3 && avgL < 0.55 && variance < 0.002) { type = 'road'; height = 0.01; } model.cells.push({gx, gy, type, height, avgR, avgG, avgB, variance}); model.heightMap[gy * gs + gx] = height; } } worldModel = model; console.log('[WORLD MODEL] Built:', gs+'x'+gs, 'Trees:', model.cells.filter(c=>c.type==='tree').length, 'Buildings:', model.cells.filter(c=>c.type==='building').length, 'Parking:', model.cells.filter(c=>c.type==='parking').length, 'Roads:', model.cells.filter(c=>c.type==='road').length); // Inject world model heights into Four.js parameterization // MOBLEYAN.w encodes world model presence, VOID.z/w encode terrain range if (renderer && renderer.params) { const maxH = Math.max(...model.heightMap); renderer.params.set(FOUR.SPACES.MOBLEYAN, 0.7, 0.65, 0.4, maxH > 0 ? 1.0 : 0.0); renderer.params.set(FOUR.SPACES.VOID, 0.1, 0.5, maxH / 20.0, 0.05); } } const T800 = { overlay: null, canvas: null, ctx: null, active: false, scanY: 0, // Simulated extraction phases — what the AI "finds" in the document extractionPhases: [ { pct: 5, status: 'SCANNING DOCUMENT STRUCTURE...', data: 'FORMAT: PDF/A-1b | PAGES: 42 | DPI: 300' }, { pct: 10, status: 'IDENTIFYING DRAWING TYPES...', data: 'A1.01 FLOOR PLAN | A2.01 ELEVATIONS | A5.01 DOOR SCHEDULE' }, { pct: 18, status: 'EXTRACTING STRUCTURAL GRID...', data: 'GRID: 6 BAYS @ 26\'-0" E-W | 3 BAYS @ 18\'-0" N-S' }, { pct: 25, status: 'READING COLUMN SCHEDULE...', data: 'W10x49 WIDE FLANGE | 6 COLUMNS | HEIGHT: 10.2m TO BEAM' }, { pct: 32, status: 'TRACING BEAM LAYOUT...', data: '5 BEAMS DETECTED | W12x26 | SPAN: 26\'-0" | MOMENT: 142 kip-ft' }, { pct: 38, status: 'SCANNING FLOOR DECK...', data: 'PARTIAL DECK L2 | 3" COMPOSITE | AREA: 1,260 SF' }, { pct: 45, status: 'EXTRACTING DOOR SCHEDULE...', data: null, doors: true }, { pct: 55, status: 'READING HARDWARE SETS...', data: null, hardware: true }, { pct: 65, status: 'IDENTIFYING MATERIAL STAGING...', data: 'PALLET 1: HAGER 780-224HD (x48) | PALLET 2: IVES FB51P (x24)' }, { pct: 72, status: 'SCANNING SITE LOGISTICS...', data: 'CRANE: LIEBHERR 81K | REACH: 48m | SCAFFOLDING: WEST ELEVATION' }, { pct: 80, status: 'COMPILING ENTITY TENSORS...', data: 'ENTITIES: 35 | TORCHES: 8 | PARAMETERIZATION: 72-FLOAT TENSOR' }, { pct: 88, status: 'GENERATING SDF PRIMITIVES...', data: 'sdBox: 18 | sdCyl: 10 | sdCone: 5 | TOTAL SDF OPS: 33' }, { pct: 95, status: 'MATERIALIZING WORLD...', data: 'COMPILING FRAGMENT SHADER... GGX PBR READY' }, { pct: 100, status: 'EXTRACTION COMPLETE', data: 'WORLD READY — ENTERING SITE' }, ], doorLines: [ 'DOOR 101 — 3\'-0" x 7\'-0" HM FRAME', ' CLOSER: LCN 4041 SURFACE', ' HINGES: HAGER 780-224HD (x3)', ' LOCK: SCHLAGE ND80PD RHODES', 'DOOR 102 — 3\'-0" x 7\'-0" PAIR', ' PANIC: VON DUPRIN 99EO', ' BOLTS: IVES FB51P FLUSH', ' HINGES: STANLEY FBB179 (x3/LEAF)', 'DOOR 103 — 2\'-8" x 7\'-0" WD FRAME', ' CLOSER: RIXSON 27 FLOOR', ' STOP: GLYNN-JOHNSON 100 SERIES OH', ], hardwareLines: [ 'SET 1: PASSAGE — SCHLAGE ND80PD + LCN 4041', 'SET 2: EXIT ONLY — VON DUPRIN 99EO + IVES FB51P', 'SET 3: OFFICE — SCHLAGE ND80PD + RIXSON 27', 'FINISH SCHEDULE: US26D SATIN CHROME', 'FIRE RATING: 90 MIN WHERE NOTED', ], start(fileName) { this.overlay = $('t800-overlay'); this.overlay.style.display = 'block'; this.active = true; this.scanY = 0; // Draw the uploaded filename being scanned const c = $('t800-canvas'); c.width = innerWidth * devicePixelRatio; c.height = innerHeight * devicePixelRatio; c.style.width = '100%'; c.style.height = '100%'; this.canvas = c; this.ctx = c.getContext('2d'); $('t800-status').textContent = 'INITIALIZING SCAN...'; $('t800-bar').style.width = '0%'; $('t800-data').innerHTML = '
TARGET: ' + (fileName||'DOCUMENT.PDF').toUpperCase() + '
'; this.runExtraction(); this.animateScan(); }, animateScan() { if (!this.active) return; const ctx = this.ctx; const w = this.canvas.width, h = this.canvas.height; ctx.fillStyle = 'rgba(0,0,0, 0.65)'; ctx.fillRect(0, 0, w, h); // Scan line this.scanY = (this.scanY + 2) % h; ctx.strokeStyle = 'rgba(255,30,30,0.4)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(0, this.scanY); ctx.lineTo(w, this.scanY); ctx.stroke(); // Red grid overlay (T-800 vision) ctx.strokeStyle = 'rgba(255,0,0, 0.65)'; for (let x = 0; x < w; x += 40) { ctx.beginPath(); ctx.moveTo(x,0); ctx.lineTo(x,h); ctx.stroke(); } for (let y = 0; y < h; y += 40) { ctx.beginPath(); ctx.moveTo(0,y); ctx.lineTo(w,y); ctx.stroke(); } // Random data points appearing (simulates document scan) for (let i = 0; i < 3; i++) { const rx = Math.random() * w, ry = Math.random() * h; ctx.fillStyle = 'rgba(255,30,30,' + (0.1 + Math.random()*0.3) + ')'; ctx.fillRect(rx, ry, 2 + Math.random()*4, 1); } // Blueprint-style lines appearing if (Math.random() > 0.85) { ctx.strokeStyle = 'rgba(255,50,50, 0.65)'; ctx.lineWidth = 0.5; const sx = Math.random()*w, sy = Math.random()*h; ctx.beginPath(); ctx.moveTo(sx, sy); ctx.lineTo(sx + (Math.random()-.5)*200, sy + (Math.random()-.5)*200); ctx.stroke(); } requestAnimationFrame(() => this.animateScan()); }, async runExtraction() { const dataEl = $('t800-data'); for (const phase of this.extractionPhases) { await this.delay(400 + Math.random() * 600); if (!this.active) return; $('t800-status').textContent = phase.status; $('t800-bar').style.width = phase.pct + '%'; if (phase.doors) { for (const line of this.doorLines) { await this.delay(120); dataEl.innerHTML += '
' + line + '
'; dataEl.scrollTop = dataEl.scrollHeight; } } else if (phase.hardware) { for (const line of this.hardwareLines) { await this.delay(150); dataEl.innerHTML += '
' + line + '
'; dataEl.scrollTop = dataEl.scrollHeight; } } else if (phase.data) { dataEl.innerHTML += '
' + phase.data + '
'; dataEl.scrollTop = dataEl.scrollHeight; } } // Extraction complete — dissolve into the game world await this.delay(800); this.overlay.style.transition = 'opacity 1.2s ease-out'; this.overlay.style.opacity = '0'; await this.delay(1200); this.overlay.style.display = 'none'; this.overlay.style.opacity = '1'; this.overlay.style.transition = ''; this.active = false; // Post-extraction: geolocate the project site and load satellite imagery geolocateProject(); // Post-extraction: rebuild 3D scene from extracted data // Demo extraction data (matches the T800 simulated extraction) reloadSceneFromExtraction({ project_name: fileName || 'Camino Real Building', address: 'Glendale Community College, Glendale CA', doors: [ { mark: '101', width_inches: 36, height_inches: 84, fire_rating: null, door_material: 'hollow_metal', frame_material: 'hollow_metal', hardware_group: 'HS-1', panic: 0 }, { mark: '102', width_inches: 36, height_inches: 84, fire_rating: null, door_material: 'hollow_metal', frame_material: 'hollow_metal', hardware_group: 'HS-1', panic: 0 }, { mark: '103', width_inches: 32, height_inches: 84, fire_rating: null, door_material: 'wood', frame_material: 'hollow_metal', hardware_group: 'HS-2', panic: 0 }, { mark: '104', width_inches: 36, height_inches: 84, fire_rating: '90', door_material: 'hollow_metal', frame_material: 'hollow_metal', hardware_group: 'HS-3', panic: 1 }, { mark: '105', width_inches: 36, height_inches: 84, fire_rating: null, door_material: 'hollow_metal', frame_material: 'hollow_metal', hardware_group: 'HS-1', panic: 0 }, { mark: '106', width_inches: 36, height_inches: 84, fire_rating: '20', door_material: 'hollow_metal', frame_material: 'hollow_metal', hardware_group: 'HS-2', panic: 0 }, ], hardware_sets: [ { set_number: 'HS-1', set_name: 'Standard Interior', components: [{type:'closer',mfr:'LCN',model:'4041'},{type:'hinges',mfr:'Hager',model:'780-224HD'},{type:'lock',mfr:'Schlage',model:'ND80PD'}] }, { set_number: 'HS-2', set_name: 'Office/Classroom', components: [{type:'closer',mfr:'LCN',model:'4111'},{type:'hinges',mfr:'Ives',model:'5BB1'},{type:'lock',mfr:'Schlage',model:'L9050'}] }, { set_number: 'HS-3', set_name: 'Fire Exit', components: [{type:'closer',mfr:'LCN',model:'4040XP'},{type:'exit_device',mfr:'Von Duprin',model:'99EO'},{type:'hinges',mfr:'Hager',model:'BB1279'}] }, ], }); // Post-extraction notification chain showNotification('SUBMITTAL COMPLETE', 'Document processed. 6 doors, 3 hardware sets, 18 line items extracted.', 'success'); await this.delay(3000); showNotification('GENERATING TAKEOFF', 'Counting quantities, matching specs, calculating lead times...', 'working'); await this.delay(3500); showNotification('TAKEOFF COMPLETE', '127 items counted. $42,680 total hardware cost. 4-6 week lead.', 'success'); await this.delay(3000); showNotification('GENERATING PROPOSAL', 'Building client-ready proposal from takeoff data...', 'working'); await this.delay(3000); showNotificationAction('PROPOSAL COMPLETE', 'Ready to review and send.', 'success', 'View Proposal', () => { showProposal(); }); }, delay(ms) { return new Promise(r => setTimeout(r, ms)); }, stop() { this.active = false; if (this.overlay) this.overlay.style.display = 'none'; } }; // ═══════════════════════════════════════════════════════════════════ // PROJECT SWITCHER + UPLOAD // ═══════════════════════════════════════════════════════════════════ function initProjectSwitcher() { const switcher = $('project-switcher'); if (!switcher) return; $('upload-project-btn').addEventListener('click', () => $('rfp-upload').click()); $('rfp-upload').addEventListener('change', (e) => { const files = e.target.files; if (!files.length) return; const file = files[0]; const name = file.name; // Add project tab const btn = document.createElement('button'); btn.textContent = name.replace(/\.[^.]+$/, '').substring(0, 20); btn.dataset.project = 'uploaded_' + Date.now(); btn.dataset.fileName = name; switcher.insertBefore(btn, $('upload-project-btn')); // Activate it switcher.querySelectorAll('button').forEach(b => b.classList.remove('active')); btn.classList.add('active'); // Run T-800 extraction T800.start(name); // Wire click to re-enter this project btn.addEventListener('click', () => { switcher.querySelectorAll('button').forEach(b => b.classList.remove('active')); btn.classList.add('active'); }); e.target.value = ''; }); // Demo project click switcher.querySelector('[data-project="demo"]').addEventListener('click', function() { switcher.querySelectorAll('button').forEach(b => b.classList.remove('active')); this.classList.add('active'); }); } // ═══════════════════════════════════════════════════════════════════ // BOOT // ═══════════════════════════════════════════════════════════════════ async function boot() { await init(); initProjectSwitcher(); } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot); else boot(); })();