1import * as THREE from 'three'; 2import { clamp, mix, smooth, cycle, earthCycle, earthHeight, earthProfile, trenchStages, trenchX, trenchDepth, frontFoundationDepth, armAngles, parkingBay, arrivalPose, workLocation, pedestrianRoute, pathPoint, roadPoint } from './jobsite-campus-activity.mjs'; 3import { PedestrianNetwork, closestPoint, sweptDistance, footprintBox } from './jobsite-campus-spatial.mjs'; 4import { deliveryPose } from './jobsite-campus-logistics.mjs'; 5const C = window.JobsiteCampus; 6const DIRT = 0x92764e, STEEL = 0x414b4e, YELLOW = 0xe5b548; 7const progress = (s, id) => s.tasks[id]?.progress || 0; 8 9export class CampusActivity { 10 constructor(view, state) { 11 this.v = view; this.clock = state.day * 12; this.workIndex = 0; this.terrainKey = ''; this.people = []; 12 this.terrain = view.shape(new THREE.PlaneGeometry(154, 122, 77, 61).rotateX(-Math.PI / 2), DIRT, [0, 0, 0]); 13 this.terrain.material = view.groundMaterial; this.terrain.castShadow = false; 14 this.buildParking(); this.buildPeople(); 15 this.excavators = [this.excavator(), this.excavator()]; 16 this.trucks = [this.truck(), this.truck(), this.truck(), this.truck()]; 17 this.dozers = [this.dozer(), this.dozer()]; this.roller = this.dozer(true); 18 this.trenchCompactor=this.dozer(true);this.utilityExcavator = this.excavator(); this.backhoe = this.excavator(.78); this.pipeCarrier = this.group(); 19 const pipe = view.cyl(.3, 4.3, [0, 0, 0], 0x5f858d, this.pipeCarrier); pipe.rotation.z = Math.PI / 2; 20 this.trenchBox = this.group(); 21 for (const z of [-1.32, 1.32]) view.box([8, 1.9, .18], [0, -.35, z], 0x9badae, this.trenchBox, .5); 22 for (const x of [-3, 3]) view.beam([x,.4,-1.32],[x,.4,1.32],.18,STEEL,this.trenchBox); 23 this.spoil = Array.from({length:24},(_,i)=>{const g=this.group();const m=view.shape(new THREE.ConeGeometry(2.1,1.4,7),DIRT,[0,.55,0],g);m.scale.set(1.2,1,.65);g.position.set(-55.6+i*4.8,0,35);return g;}); 24 this.bedding = Array.from({length:24},(_,i)=>view.box([4.6,.15,2.6],[-55.6+i*4.8,-1.5,30],0xb9a67f)); 25 this.pipes = [30,33.5].map(z=>Array.from({length:24},(_,i)=>{const m=view.cyl(z===30?.32:.23,4.7,[-55.6+i*4.8,-1.05,z],z===30?0x628c95:0xbaa674,view.root);m.rotation.z=Math.PI/2;return m;})); 26 this.pumps = Array.from({length:3},()=>this.pump()); 27 this.lifts = Array.from({length:6},()=>this.lift()); 28 this.survey = this.group(); 29 for(const x of [-.7,.7])view.beam([x,0,.6],[0,1.7,0],.07,0xe8bd59,this.survey); 30 view.beam([0,0,-.7],[0,1.7,0],.07,0xe8bd59,this.survey);view.box([.45,.4,.35],[0,1.9,0],0xe5dfd0,this.survey); 31 this.rod = this.group();view.box([.09,3,.09],[0,1.5,0],0xf2e1c0,this.rod); 32 for(let y=.25;y<3;y+=.5)view.box([.1,.18,.1],[0,y,0],0xb94f32,this.rod); 33 this.erosion=this.group();for(let i=0;i<30;i++){view.box([4.8,.7,.08],[-72+i*5,.2,58],0x473f33,this.erosion);} 34 this.cureSigns=view.halls.map(h=>({key:h.key,sign:this.label('CURING / TEST WAIT',h.g.position.x,h.g.position.z+18*h.g.scale.z,17)})); 35 this.testRig=this.group();for(let i=0;i<3;i++){const unit=this.group(this.testRig,-9+i*3,0,-31);view.box([2,2,1.3],[0,1,0],0x657076,unit);view.box([1.5,.8,.06],[0,1.3,.7],0x24343a,unit);view.beam([0,.1,0],[0,.1,-4],.08,0x32302b,unit);} 36 this.testSign=this.label('SYSTEM VERIFICATION',-6,-28,15); 37 this.buildRoadWork(); this.update(state, 0, C.allocation(state).active); 38 } 39 workLocation(task,p,worker=0) { return workLocation(task,p,worker,this.v.model); } 40 roadPoint(p) { return roadPoint(p,this.v.model); } 41 group(parent = this.v.root, x = 0, y = 0, z = 0) { return this.v.group(parent,x,y,z); } 42 label(text, x, z, width = 14) { 43 const c=document.createElement('canvas');c.width=512;c.height=96;const ctx=c.getContext('2d'); 44 ctx.fillStyle='#1d1914';ctx.fillRect(0,0,512,96);ctx.strokeStyle='#bb9871';ctx.lineWidth=4;ctx.strokeRect(2,2,508,92);ctx.fillStyle='#efe8dd';ctx.font='600 25px system-ui';ctx.textAlign='center';ctx.fillText(text,256,59);
45 const texture=new THREE.CanvasTexture(c);texture.colorSpace=THREE.SRGBColorSpace; 46 const material=new THREE.SpriteMaterial({map:texture,depthTest:true});const sign=new THREE.Sprite(material);sign.position.set(x,4,z);sign.scale.set(width,width*96/512,1);this.v.root.add(sign); 47 (this.labels ||= []).push({texture,material});return sign; 48 } 49 buildParking() { 50 const v=this.v; 51 v.box([77,.25,34],[-39,.01,84],0x67635b);v.box([170,.14,7],[12,-.15,111],0x666963); 52 v.box([7,.15,33],[-2,-.12,97],0x67635b); 53 // Visitor parking and the pedestrian gate are separated from the east haul entrance. 54 for(let i=0;i<32;i++){ 55 const [x,,z]=parkingBay(i);v.box([.1,.03,7],[x-1.95,.16,z],0xe1d5b5);v.box([3.9,.03,.1],[x,.16,z+(i<16?-3.5:3.5)],0xe1d5b5); 56 v.box([2,.18,.3],[x,.2,z+(i<16?-2.5:2.5)],0xbeb7a3); 57 } 58 v.box([3,.14,42],[-77,.12,77],0xc9c0a4);v.box([31,.14,3],[-62,.12,64],0xc9c0a4);v.box([3,.14,20],[-48,.12,53],0xc9c0a4); 59 for(let z=49;z<57;z+=1.4)v.box([4,.025,.65],[-48,.17,z],0xe6dbb6); 60 for(let z=65;z<100;z+=5)v.cyl(.15,1.1,[-74.7,.55,z],YELLOW); 61 this.label('CREW PARKING / 32 BAYS',-37,100,12);this.label('CHECK IN',-49,44,9); 62 for(let i=0;i<3;i++){const x=-46+i*10;v.box([7,.22,4],[x,2.4,47],0x8d9486);for(const dx of [-3,3])v.box([.1,2.5,.1],[x+dx,1.2,47],0x5a605b);v.box([5,.15,1.2],[x,.85,47],0xb2a27b);} 63 this.cars=Array.from({length:32},(_,i)=>this.truck('car',i)); 64 } 65 buildRoadWork() { 66 this.roadTiles=Array.from({length:64},(_,i)=>{ 67 const p=this.roadPoint((i+.5)/64),q=this.roadPoint(Math.min(1,(i+.501)/64));const tile=this.v.box([7,.13,7.8],[p[0],.025,p[2]],0x928978);tile.rotation.y=Math.atan2(q[0]-p[0],q[2]-p[2]);return tile; 68 }); 69 this.dust=new THREE.InstancedMesh(new THREE.SphereGeometry(.35,5,4),new THREE.MeshBasicMaterial({color:0xc3af84,transparent:true,opacity:.24,depthWrite:false}),18);this.dust.frustumCulled=false;this.v.root.add(this.dust);this.dustMatrix=new THREE.Object3D(); 70 } 71 truck(kind = 'haul', index = 0) {
72 const v=this.v,g=this.group(),wheels=[];const car=kind==='car',sedan=car&&index%4!==0; 73 const color=car?[0xe0d9c9,0x455261,0xb8b3a5,0x775546,0x8d9398][index%5]:0xe2d5aa; 74 v.box([car?2:2.8,.5,car?4.3:6.6],[0,.7,0],car?color:STEEL,g,.3); 75 v.box([car?1.85:2.65,sedan?.75:1.2,2.4],[0,sedan?1.25:1.45,car?-.25:-2],color,g,.3); 76 v.box([car?1.5:2.3,sedan?.48:.72,.12],[0,sedan?1.3:1.7,car?-1.5:-3.25],0x31454d,g,.4); 77 v.box([car?1.7:2.8,.12,car?2:2.7],[0,sedan?1.66:2.1,car?-.1:-2],car?color:0xe6dfcd,g); 78 for(const x of [car?-1.03:-1.53,car?1.03:1.53])for(const z of [car?-1.3:-2.3,car?1.3:2.3]){const tire=v.cyl(car?.43:.65,.35,[x,.52,z],0x262b2d,g);tire.rotation.z=Math.PI/2;wheels.push(tire);v.cyl(.2,.37,[x,.52,z],0x9b9a91,g).rotation.z=Math.PI/2;} 79 const bed=this.group(g,0,1.1,3.1); 80 if(!car){v.box([2.7,.25,4.7],[0,0,-2.3],0xc3b393,bed);for(const x of [-1.4,1.4])v.box([.17,1.3,4.7],[x,.55,-2.3],0xc3b393,bed);v.box([2.8,1.3,.16],[0,.55,0],0xc3b393,bed);} 81 const load=car?null:v.box([2.5,.75,4.3],[0,.5,-2.3],DIRT,bed); 82 return {g,bed,load,wheels,kind}; 83 } 84 excavator(scale = 1) {
85 const v=this.v,g=this.group(),upper=this.group(g,0,1.2,0); 86 for(const x of [-1.55,1.55]){v.box([.85,1.05,5.3],[x,.6,0],0x343b3a,g);for(let z=-2.2;z<=2.2;z+=.55){const wheel=v.cyl(.42,.9,[x,.58,z],0x717467,g);wheel.rotation.z=Math.PI/2;v.box([.93,.08,.22],[x,1.12,z],0x96917c,g);}} 87 v.cyl(1.45,.45,[0,.1,0],0x525b55,upper);v.box([3.2,1.5,3.8],[0,1,0],YELLOW,upper);v.box([1.35,1.8,1.8],[.8,2,-.8],0xc4cfca,upper);v.box([1.2,1.1,.1],[.8,2.15,-1.76],0x324e5a,upper);v.box([.12,.8,.8],[1.51,2.15,-.8],0x324e5a,upper); 88 const boom=this.group(upper,-.55,1.65,-.4),stick=this.group(boom,0,0,-5),bucket=this.group(stick,0,0,-4); 89 v.box([.65,.65,5],[0,0,-2.5],YELLOW,boom);v.box([.44,.48,4],[0,0,-2],0xe7bb51,stick); 90 v.box([.17,.18,3.8],[0,.48,-2.2],0xd0cbb7,boom,.8);v.box([.12,.14,2.5],[0,.36,-1.5],0xd0cbb7,stick,.8); 91 v.box([1.55,.2,1.25],[0,-.32,-.2],0x59625b,bucket);for(const x of [-.7,.7])v.box([.14,.8,1.25],[x,0,-.2],0x59625b,bucket); 92 for(let x=-.6;x<=.6;x+=.3)v.box([.13,.18,.45],[x,-.3,-.95],0xb0ad91,bucket); 93 const soil=v.box([1.15,.55,.95],[0,-.06,-.2],DIRT,bucket);g.scale.setScalar(scale); 94 return {g,upper,boom,stick,bucket,soil,scale}; 95 } 96 poseExcavator(m, position, target, filled, tip = 0) { 97 m.g.position.set(...position);const dx=(target[0]-position[0])/m.scale,dz=(target[2]-position[2])/m.scale; 98 m.upper.rotation.y=Math.atan2(-dx,-dz);const angles=armAngles(Math.hypot(dx,dz)-.4,(target[1]-position[1])/m.scale-2.85); 99 m.boom.rotation.x=angles.shoulder;m.stick.rotation.x=angles.elbow;m.bucket.rotation.x=-angles.shoulder-angles.elbow+tip;m.soil.visible=filled; 100 } 101 dozer(roller = false) {
102 const v=this.v,g=this.group();v.box([3.1,.8,4.8],[0,.7,0],STEEL,g);v.box([2.5,1.1,2.8],[0,1.6,0],YELLOW,g);v.box([1.8,1.6,1.7],[0,2.7,.4],0xbfc9c1,g);v.box([1.7,.9,.1],[0,2.9,-.5],0x35515c,g); 103 const blade=v.box([4.2,1.35,.4],[0,.7,-2.6],0xe0bf76,g); 104 for(const x of [-1.65,1.65])v.box([.7,1,4.6],[x,.6,0],0x3a403e,g); 105 if(roller){blade.visible=false;v.cyl(1,3.8,[0,.9,-2.3],0x858678,g,20).rotation.z=Math.PI/2;} 106 const dirt=v.shape(new THREE.ConeGeometry(1.5,.8,7),DIRT,[0,.25,-3.4],g);dirt.scale.set(1.5,1,.45); 107 return {g,blade,dirt}; 108 } 109 pump() { 110 const v=this.v,truck=this.truck(),mixer=this.truck(),boom=this.group(truck.g); 111 truck.bed.visible=false;mixer.bed.visible=false; 112 const drum=v.shape(new THREE.CylinderGeometry(1.35,.95,3.4,16).rotateX(Math.PI/2),0xdfd8c8,[0,2.1,.5],mixer.g); 113 v.box([.14,.18,3.1],[1.22,0,0],YELLOW,drum); 114 const arms=[0,1,2].map(()=>v.box([.2,.2,1],[0,0,0],0xd0b570,boom)); 115 const hose=v.box([.12,1,.12],[0,0,0],0x343b3c,boom); 116 for(const x of [-2.5,2.5])v.box([2.7,.18,.25],[x,.45,0],0x78807a,truck.g); 117 return {truck,mixer,boom,drum,arms,hose}; 118 } 119 lift() {
120 const v=this.v,g=this.group();v.box([2,.65,3],[0,.4,0],YELLOW,g);const arm=v.box([.2,5,.2],[0,2.8,0],STEEL,g);const platform=this.group(g,0,5.4,0);v.box([2.1,.15,1.5],[0,0,0],0x8a8e7a,platform);for(const x of [-1,1])v.box([.06,1,.06],[x,.5,0],YELLOW,platform);v.box([2.1,.06,.06],[0,1,.7],YELLOW,platform);return {g,arm,platform}; 121 } 122 buildPeople() { 123 const v=this.v, max=225; 124 this.parts={}; 125 for(const [key,geometry,color] of [ 126 ['body',new THREE.BoxGeometry(.52,.7,.32),0xe8b23f],['band',new THREE.BoxGeometry(.55,.12,.34),0xe4e1ba], 127 ['head',new THREE.SphereGeometry(.21,8,6),0xbe916f],['hat',new THREE.CylinderGeometry(.24,.28,.17,10),0xf0df9b], 128 ['leftLeg',new THREE.BoxGeometry(.19,.72,.22),0x33444e],['rightLeg',new THREE.BoxGeometry(.19,.72,.22),0x33444e], 129 ['leftArm',new THREE.BoxGeometry(.15,.63,.18),0xd6a341],['rightArm',new THREE.BoxGeometry(.15,.63,.18),0xd6a341] 130 ]){const mesh=new THREE.InstancedMesh(geometry,v.mat(color),max);mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);mesh.castShadow=true;mesh.frustumCulled=false;v.root.add(mesh);this.parts[key]=mesh;} 131 this.dummy=new THREE.Object3D();this.person=new THREE.Object3D();this.limb=new THREE.Object3D(); 132 let id=0; 133 for(const [trade,info] of Object.entries(C.TRADES))for(let crew=0;crew<3;crew++)for(let member=0;member<info.people;member++)this.people.push({id:id++,trade,crew,member,position:null,target:null,route:null,walked:0,key:''}); 134 } 135 motionPlans(s, active, days=0) { 136 const plans=[]; 137 for(const task of active){ 138 const p=progress(s,task.id),next=Math.min(C.workPhase(s,task).end,p+days/task.days*(task.outdoor?C.weather(s).factor:1)); 139 const add=(name,a,b,radius=2.5)=>plans.push({key:task.id+':'+name,task:task.id,name,a,b,radius}); 140 if(task.id==='grade')for(let lane=0;lane<2;lane++){ 141 const a=earthCycle(p,lane),b=earthCycle(next,lane);add('Haul truck '+(lane+1),a.truck,b.truck,2.5);add('Excavator '+(lane+1),a.machine,b.machine,2.4); 142 } 143 else if(task.equipment==='earth'){ 144 if(task.id.endsWith('-prep')){const a=this.workLocation(task,p),b=this.workLocation(task,next);add('Foundation excavator',[a[0]+6,0,a[2]],[b[0]+6,0,b[2]],2.4);} 145 else add('Dozer',this.roadPoint(p),this.roadPoint(next),2.7); 146 } 147 if(task.reachWork){const z=task.id==='duct'?33.5:30,a=trenchStages(p),b=trenchStages(next);add('Utility excavator',[trenchX(a.dig),0,z-6],[trenchX(b.dig),0,z-6],2.4);add('Backfill backhoe',[trenchX(a.fill)-2,0,z-6],[trenchX(b.fill)-2,0,z-6],2.2);} 148 if(this.v.componentGroup(task)){ 149 const job=this.v.craneAssignment(task,s);if(!job)continue; 150 const a=deliveryPose(p,job.group.children.length,job.base,job.target),b=deliveryPose(next,job.group.children.length,job.base,job.target); 151 add('Material carrier',a.vehicle,b.vehicle,3); 152 if(a.suspended||b.suspended)add('Suspended load',a.cargo,b.cargo,2.6); 153 } 154 } 155 return plans; 156 } 157 prepare(s, days) { 158 if(s.safety.stage){C.siteHolds(s,{});return;} 159 const active=C.allocation(s,true).active,plans=this.motionPlans(s,active,days),holds={};this.traffic=plans; 160 const extras=this.obstacleBoxes(s,plans); 161 this.v.navigation.setExtras(extras); 162 const envelopes=plans.flatMap(v=>{const count=Math.max(1,Math.ceil(Math.hypot(v.b[0]-v.a[0],v.b[2]-v.a[2])/2));return Array.from({length:count+1},(_,i)=>{const x=mix(v.a[0],v.b[0],i/count),z=mix(v.a[2],v.b[2],i/count),r=v.radius+1.3;return {x0:x-r,x1:x+r,z0:z-r,z1:z+r};});}); 163 this.trafficNavigation=new PedestrianNetwork([...this.v.navigation.boxes,...extras,...envelopes]); 164 for(const vehicle of plans)for(const person of this.people){ 165 if(!person.position||person.crew>=s.crews[person.trade]||person.seated)continue; 166 const distance=sweptDistance(vehicle.a,vehicle.b,person.position); 167 if(s.safety.controls){if(distance<vehicle.radius+1.5)holds[vehicle.task]='Traffic hold / pedestrian crossing';} 168 else if(distance<vehicle.radius+.3){C.incident(s,vehicle.name,person.trade+' crew member');this.incidentPoint=person.position.slice();C.siteHolds(s,{});return;} 169 } 170 C.siteHolds(s,holds); 171 } 172 regroup() { 173 for(const person of this.people){person.position=null;person.route=null;person.key='';person.seated=false;} 174 this.regrouping=true; 175 } 176 obstacleBoxes(s, plans = this.traffic) { 177 const moving=new Set((plans||[]).map(v=>
177v.name)),boxes=[]; 178 const add=(name,pos,rx,rz)=>{if(!pos||moving.has(name))return;boxes.push(footprintBox(pos[0]??pos.x,pos[2]??pos.z,rx,rz));}; 179 const point=o=>o?.position?o.position.toArray():null; 180 this.cars?.forEach((car,i)=>{if(car.g.visible&&arrivalPose(i,s.day).parked)add('Crew vehicle '+i,point(car.g),1.2,2.3);}); 181 this.excavators?.forEach((m,i)=>{if(m.g.visible)add('Excavator '+(i+1),point(m.g),2.1,2.6);}); 182 this.trucks?.forEach((m,i)=>{if(m.g.visible)add('Haul truck '+(i+1),point(m.g),1.7,3);}); 183 this.dozers?.forEach((m,i)=>{if(m.g.visible)add(i?'Dozer '+(i+1):'Dozer',point(m.g),2,2.4);}); 184 if(this.roller?.g.visible)add('Roller',point(this.roller.g),2,2.4); 185 if(this.utilityExcavator?.g.visible)add('Utility excavator',point(this.utilityExcavator.g),2.1,2.6); 186 if(this.backhoe?.g.visible)add('Backfill backhoe',point(this.backhoe.g),2,2.4); 187 if(this.trenchCompactor?.g.visible)add('Trench compact',point(this.trenchCompactor.g),1.8,2.2); 188 this.lifts?.forEach((m,i)=>{if(m.g.visible)add('Access lift '+i,point(m.g),1.3,1.7);}); 189 this.pumps?.forEach((m,i)=>{if(m.truck.g.visible){add('Concrete pump '+i,point(m.truck.g),2,3);add('Mixer '+i,point(m.mixer.g),2,3);}}); 190 this.v.cranes?.forEach((crane,i)=>{ 191 if(crane.g.visible)add('Crane '+i,point(crane.g),2.3,3); 192 if(crane.carrier?.g.visible)add('Material carrier',point(crane.carrier.g),2,3.1); 193 if(crane.payload?.visible&&crane.cable?.visible)add('Suspended load',point(crane.payload),2.4,2.4); 194 }); 195 return boxes; 196 } 197 movePerson(person, target, key, dt, s) { 198 const network=this.v.navigation; 199 if(!person.position){ 200 const origin=this.regrouping?[-48+person.id%13*2.4,0,45+Math.floor(person.id%39/13)*2.1]:s.day>1.4||person.trade==='survey'?target:parkingBay(person.car||0).map((v,i)=>i===0?v+1.5:v); 201 person.position=network.nearest(origin,false)||[-48,0,46];person.target=person.position.slice();person.key='arrival'; 202 } 203 // A newly placed component can occupy an old workstation. Reassign to its nearest clear service face. 204 if(!network.staticFree(person.position)){person.position=network.nearest(person.position,false)||[-48,0,46];person.route=null;person.navKey=null;} 205 let routeNetwork=s.safety.controls&&this.trafficNavigation?this.trafficNavigation:network; 206 if(!routeNetwork.free(person.position)&&network.staticFree(person.position)){ 207 const exit=routeNetwork.nearest(person.position); 208 if(exit){person.position=exit;person.route=null;person.navKey=null;} 209 } 210 let safe=routeNetwork.nearest(target)||network.nearest(target,false);if(!safe)return false; 211 if(person.key!==key||person.navKey!==this.v.navigationKey||Math.hypot(safe[0]-person.target[0],safe[2]-person.target[2])>2||!person.route&&Math.hypot(safe[0]-person.position[0],safe[2]-person.position[2])>.8&&this.clock-(person.routeAttempt??-10)>.5){ 212 person.routeAttempt=this.clock; 213 const waypoints=pedestrianRoute(person.position,safe),route=[person.position.slice()]; 214 // Preserve the separated parking walkway, then route around the actual installed geometry. 215 const goals=person.position[2]>61?waypoints.slice(1):[safe];let reachable=true; 216 for(const goal of goals){const leg=routeNetwork.route(route.at(-1),goal);if(!leg){reachable=false;break;}route.push(...leg.slice(1));} 217 person.route=reachable&&route.length>1?route:null;person.target=safe.slice();person.key=key;person.navKey=this.v.navigationKey; 218 } 219 if(!s.running||s.safety.stage)return false; 220 if(s.safety.controls){ 221 const hazards=(this.traffic||[]).filter(v=>sweptDistance(v.a,v.b,person.position)<v.radius+2); 222 if(!routeNetwork.free(person.position)){ 223 const exit=routeNetwork.nearest(person.position); 224 const detour=exit&&routeNetwork.route(person.position,exit); 225 if(detour){person.route=detour;person.navKey=null;} 226 else return false;
227 } 228 if(hazards.length){ 229 const hazard=hazards[0],near=closestPoint(hazard.a,hazard.b,person.position),dx=person.position[0]-near[0],dz=person.position[2]-near[2],angle=Math.hypot(dx,dz)>.01?Math.atan2(dz,dx):Math.atan2(hazard.b[2]-hazard.a[2],hazard.b[0]-hazard.a[0])+Math.PI/2; 230 // Leave a yielding machine's envelope by a routed detour; wait if every exit is blocked. 231 let escaped=false; 232 for(const offset of [0,.5,-.5,1,-1,Math.PI]){ 233 const a=angle+offset,exit=[near[0]+Math.cos(a)*(hazard.radius+4),person.position[1],near[2]+Math.sin(a)*(hazard.radius+4)]; 234 const detour=routeNetwork.route(person.position,exit); 235 if(detour){person.route=detour;person.navKey=null;escaped=true;break;} 236 } 237 if(!escaped)return false; 238 } 239 } 240 if(!person.route)return false; 241 const remaining=Math.hypot(...person.route[1].map((v,i)=>v-person.position[i])),step=dt*s.speed*5.5; 242 const next=remaining<=step?person.route[1].slice():person.position.map((v,i)=>v+(person.route[1][i]-v)*step/remaining); 243 if(!network.clear(person.position,next)){person.navKey=null;return false;} 244 for(const v of this.traffic||[]){ 245 if(s.safety.controls&&sweptDistance(v.a,v.b,person.position,next)<v.radius+1.2&&sweptDistance(v.a,v.b,next)<=sweptDistance(v.a,v.b,person.position)+.001){ 246 const detour=routeNetwork.route(person.position,safe);if(detour)person.route=detour;return false; 247 } 248 if(!s.safety.controls&&sweptDistance(v.a,v.b,person.position,next)<v.radius+.3){C.incident(s,v.name,person.trade+' crew member');this.incidentPoint=person.position.slice();return false;} 249 } 250 person.yaw=Math.atan2(next[0]-person.position[0],next[2]-person.position[2]);person.position=next; 251 if(!network.staticFree(person.position)){ 252 person.position=network.nearest(person.position,false)||person.position; 253 person.route=null;person.navKey=null;return false; 254 } 255 if(remaining<=step){person.route.shift();if(person.route.length<2)person.route=null;} 256 return remaining>.001; 257 } 258 updatePeople(s,dt,active) { 259 let count=0,walkingCount=0,workingCount=0,employee=0; 260 const assigned={};for(const t of active)(assigned[t.trade] ||= []).push(t); 261 for(const person of this.people){ 262 if(person.crew>=s.crews[person.trade])continue; 263 person.car=Math.min(31,Math.floor(employee++/4));const task=assigned[person.trade]?.[person.crew];const arrived=arrivalPose(person.car,s.day).parked || person.trade==='survey'; 264 if(!arrived)continue; 265 let target=task?this.workLocation(task,progress(s,task.id),person.member):[-48+person.id%13*2.4,0,45+Math.floor(person.id%39/13)*2.1]; 266 if(task){target=[target[0]+(person.member%3-1)*1.1,target[1],target[2]+Math.floor(person.member/3)*1.1];if(task.id==='grade'){const e=this.earth[person.member%2];if(person.member<2)target=[e.machine[0]+.8,1.7,e.machine[2]-.6];else if(person.member<4)target=[e.truck[0],.2,e.truck[2]-2];}} 267 const seated=task?.id==='grade'&&person.member<4;person.seated=seated; 268 if(seated){person.position=target.slice();person.target=target.slice();person.route=null;person.key=task.id;} 269 const walking=seated?false:this.movePerson(person,target,task?.id||'break',dt,s);if(walking)walkingCount++;if(task)workingCount++; 270 if(person.position&&!seated&&!this.v.navigation.staticFree(person.position)){ 271 person.position=this.v.navigation.nearest(person.position,false)||person.position;person.route=null; 272 } 273 const p=person.position.slice();if(!seated&&p[1]>=0&&p[2]<40&&Math.abs(p[0])<76){p[1]=Math.max(p[1],earthHeight(p[0],p[2],progress(s,'grade')));} 274 this.person.position.set(...p);this.person.rotation.set(0,person.yaw||0,0);this.person.updateMatrix(); 275 if(s.running){person.gait=walking?Math.sin(this.clock*8+person.id)*.6:0;person.work=task&&!walking&&!seated?Math.sin(this.clock*4+person.id)*.25:0;}const gait=person.gait||0,work=person.work||0; 276 const poses={body:[0,1.13,0,0],band:[0,1.2,0,0],head:[0,1.69,0,0],hat:[0,1.89,0,0],leftLeg:[-.15,.42,0,gait],rightLeg:[.15,.42,0,-gait],leftArm:[-.36,1.1,0,-gait+work],rightArm:[.36,1.1,0,gait-work]}; 277 for(const [key,[x,y,z,angle]] of Object.entries(poses)){this.limb.position.set(x,y,z);
277this.limb.rotation.set(angle,0,0);this.limb.updateMatrix();this.dummy.matrix.multiplyMatrices(this.person.matrix,this.limb.matrix);this.parts[key].setMatrixAt(count,this.dummy.matrix);} 278 count++; 279 } 280 for(const mesh of Object.values(this.parts)){mesh.count=count;mesh.instanceMatrix.needsUpdate=true;} 281 this.regrouping=false; 282 this.workerCount=count;this.walkingCount=walkingCount;this.workingCount=workingCount; 283 } 284 placeVehicle(model, point, ahead) { 285 model.g.position.set(...point);if(ahead&&Math.hypot(ahead[0]-point[0],ahead[2]-point[2])>.001)model.g.rotation.y=Math.atan2(-(ahead[0]-point[0]),-(ahead[2]-point[2])); 286 for(const wheel of model.wheels||[])wheel.rotation.x=this.clock*2; 287 } 288 terrainUpdate(s) { 289 const grade=progress(s,'grade'),drain=progress(s,'drain'),duct=progress(s,'duct'),a=progress(s,'a-prep'),b=progress(s,'b-prep'),ap=progress(s,'a-slab'),bp=progress(s,'b-slab'),key=[grade,drain,duct,a,b,ap,bp].map(p=>p.toFixed(3)).join(':');if(key===this.terrainKey)return;this.terrainKey=key; 290 const vertices=this.terrain.geometry.attributes.position,profile=earthProfile(grade); 291 if(!this.terrainWeights)this.terrainWeights=Array.from({length:vertices.count},(_,i)=>profile.map(c=>[Math.exp(-((vertices.getX(i)-c.cut[0])**2/75+(vertices.getZ(i)-c.cut[1])**2/30))*1.8,Math.exp(-((vertices.getX(i)-c.fill[0])**2/100+(vertices.getZ(i)-c.fill[1])**2/36))*1.05])); 292 for(let i=0;i<vertices.count;i++){const x=vertices.getX(i),z=vertices.getZ(i),height=profile.reduce((h,c,j)=>h+this.terrainWeights[i][j][0]*c.cutRemaining-this.terrainWeights[i][j][1]*c.fillRemaining,-.08);vertices.setY(i,height+trenchDepth(x,z,drain,30)+trenchDepth(x,z,duct,33.5)+frontFoundationDepth(x,z,a,ap,this.v.model.fronts[0])+frontFoundationDepth(x,z,b,bp,this.v.model.fronts[1]));} 293 vertices.needsUpdate=true;this.terrain.geometry.computeVertexNormals();this.terrain.geometry.computeBoundingSphere(); 294 } 295 update(s,dt,active) { 296 if(s.running)this.clock+=dt*s.speed; 297 if(!this.traffic)this.traffic=this.motionPlans(s,active); 298 const extras=this.obstacleBoxes(s,this.traffic); 299 this.v.navigation.setExtras(extras); 300 if(this.trafficNavigation)this.trafficNavigation.setExtras(extras); 301 this.active=active;this.terrainUpdate(s);const v=this.v; 302 const earthTask=active.find(t=>t.equipment==='earth'),trenchTask=active.find(t=>t.reachWork); 303 this.earth=[earthCycle(progress(s,'grade'),0),earthCycle(progress(s,'grade'),1)]; 304 this.excavators.forEach((m,i)=>{const e=this.earth[i],working=earthTask?.id==='grade';m.g.visible=s.equipment.earth>0;if(i===0&&earthTask?.id.endsWith('-prep')){const p=this.workLocation(earthTask,progress(s,earthTask.id)),f=(progress(s,earthTask.id)*10)%1;this.poseExcavator(m,[p[0]+6,0,p[2]],[p[0],-Math.sin(f*Math.PI)*.8,p[2]],f>.3&&f<.7);return;}this.poseExcavator(m,working?[e.machine[0],earthHeight(e.machine[0],e.machine[2],progress(s,'grade')),e.machine[2]]:[57+i*7,0,34],working?e.bucket:[53+i*7,1,30],working&&e.soil,e.phase>.43&&e.phase<.5?-.8:0);}); 305 this.trucks.forEach((truck,i)=>{truck.g.visible=s.equipment.earth>0;const e=this.earth[i%2],working=earthTask?.id==='grade'&&i<2;const point=working?e.truck:[57+(i%2)*7,0,17+Math.floor(i/2)*10];
305const ahead=working?earthCycle(Math.min(1,progress(s,'grade')+.0002),i%2).truck:null;if(working)point[1]=earthHeight(point[0],point[2],progress(s,'grade'));this.placeVehicle(truck,point,ahead);truck.bed.rotation.x=working?e.tip:0;truck.load.visible=working&&e.payload>0;}); 306 [...this.dozers,this.roller].forEach((m,i)=>{m.g.visible=s.equipment.earth>0;if(earthTask?.id==='grade'){const e=this.earth[i%2];const pass=clamp((e.phase-.74)/.26);m.g.position.set(e.fillPoint[0]-6+pass*12,0,e.fillPoint[1]+(i===2?9:1));m.g.position.y=earthHeight(m.g.position.x,m.g.position.z,progress(s,'grade'));m.g.rotation.y=-Math.PI/2;m.dirt.visible=e.phase>.73&&e.phase<.92&&i<2;}else if(earthTask?.id.endsWith('-prep')){const p=this.workLocation(earthTask,progress(s,earthTask.id));m.g.position.set(p[0]-6-i*4,0,p[2]+4);m.dirt.visible=false;}else if(earthTask){const p=clamp(progress(s,earthTask.id)-i*.015),a=this.roadPoint(p),b=this.roadPoint(Math.min(1,p+.002));this.placeVehicle(m,a,b);m.dirt.visible=i<2&&earthTask.id==='clear';}else{m.g.position.set(57+i*6,0,45);m.dirt.visible=false;}}); 307 this.roadTiles.forEach((tile,i)=>{tile.visible=i/64<progress(s,'clear');tile.material=v.mat(i/64<progress(s,'roads')?0x44494b:0x928978);}); 308 this.erosion.children.forEach((m,i)=>m.visible=i/30<progress(s,'controls')); 309 this.cureSigns.forEach(({key,sign})=>sign.visible=C.done(s,key+'-slab')&&!C.done(s,key+'-strength')); 310 this.testRig.visible=this.testSign.visible=active.some(t=>['release','startup','functional','test'].includes(t.id)); 311 this.updateTrench(s,trenchTask);this.updatePumps(s,active);this.updateLifts(s,active); 312 const cars=Math.min(32,Math.ceil(Object.entries(s.crews).reduce((n,[k,c])=>n+c*C.TRADES[k].people,0)/4));let parked=0; 313 this.cars.forEach((car,i)=>{const pose=arrivalPose(i,s.day);car.g.visible=i<cars&&(pose.visible||s.day>1.5);if(car.g.visible){const ahead=arrivalPose(i,s.day+.001).position;this.placeVehicle(car,pose.position,ahead);if(pose.parked){car.g.rotation.y=i<16?0:Math.PI;parked++;}}});this.parkedCount=parked; 314 const survey=active.find(t=>t.trade==='survey');this.survey.visible=this.rod.visible=!!survey;if(survey){const p=this.workLocation(survey,progress(s,survey.id));this.survey.position.set(p[0]-2,0,p[2]);this.rod.position.set(p[0]+5,0,p[2]-4);} 315 this.updatePeople(s,dt,active); 316 const dustActive=s.running&&earthTask?.id==='grade';this.dust.visible=dustActive;if(dustActive)for(let i=0;i<18;i++){const e=this.earth[i%2],a=(this.clock*.8+i*.37)%1;this.dustMatrix.position.set(e.truck[0]+Math.sin(i)*1.5,.4+a*2,e.truck[2]+a*4);this.dustMatrix.scale.setScalar(.3+a);this.dustMatrix.updateMatrix();this.dust.setMatrixAt(i,this.dustMatrix.matrix);}this.dust.instanceMatrix.needsUpdate=true; 317 const focusTask=active[this.workIndex%Math.max(1,active.length)];this.focus=focusTask?this.workLocation(focusTask,progress(s,focusTask.id)):[-40,0,77]; 318 if(focusTask?.id==='grade'){const e=this.earth[0];this.focus=e.phase>.5?e.truck.slice():[e.machine[0]+3,0,e.machine[2]+3];} 319 if(focusTask&&(focusTask.equipment==='crane'||focusTask.equipment==='pump')&&['a','b'].includes(focusTask.zone)){this.focus=[this.v.model.fronts.find(f=>f.key===focusTask.zone).x+5,focusTask.equipment==='crane'?5:0,this.focus[2]];} 320 if(focusTask?.equipment==='crane'){const job=this.v.craneAssignment(focusTask,s);if(job){const pose=deliveryPose(job.p,job.group.children.length,job.base,job.target);this.focus=pose.carried?pose.vehicle:pose.cargo;}} 321 const curing=C.allocation(s).passive; if(!focusTask&&curing.length)this.focus=this.workLocation(curing[0],0); 322 this.focusTask=focusTask;this.title=focusTask?focusTask.name:curing.length?'Concrete curing / awaiting test evidence':s.day<1.5?'Morning mobilization':'Crews at the site compound'; 323 this.detail=focusTask?.id==='grade'?this.earth[0].label:trenchTask&&focusTask===trenchTask?C.workPhase(s,trenchTask).label:focusTask?.equipment==='crane'?this.materialDetail(s,focusTask):focusTask?.equipment==='pump'?'Place concrete / finish / release the pump':focusTask?'Assigned crews at the work front':curing.length?'Elapsed wait / foundation release still required':'Park, check in and walk to the work area'; 324 v.canvas.dataset.visibleWorkers=this.workerCount;v.canvas.dataset.walkingWorkers=this.walkingCount;v.canvas.dataset.parkedCars=this.parkedCount;v.canvas.dataset.activityPhase=this.clock.toFixed(3);v.canvas.dataset.terrainVersion=this.terrainKey; 325 } 326 materialDetail(s,task) { const job=this.v.craneAssignment(task,s);return job?job.c.installed+'/'+job.group.children.length+' pieces placed / '+job.c.label:'Materials at the work front'; } 327 updateTrench(s,task) { 328 const v=this.v,front=task||C.plan(s).find(t=>t.reachWork&&s.tasks[t.id].started&&progress(s,t.id)<1),p=front?progress(s,front.id):1,z=front?.id==='duct'?33.5:30,st=trenchStages(p),phase=(p*6)%1;
329 const dig=trenchX(st.dig),pipe=trenchX(st.pipe),fill=trenchX(st.fill);const working=!!front,installing=!!task&&phase<.5,backfilling=!!task&&phase>=.6&&phase<.92; 330 this.utilityExcavator.g.visible=this.backhoe.g.visible=s.equipment.trench>0; 331 this.poseExcavator(this.utilityExcavator,working?[dig,0,z-6]:[-61,0,42],working?[dig,installing?mix(.2,-1.3,Math.sin(phase*2*Math.PI)**2):1.4,z]:[-57,1,38],installing&&phase>.15&&phase<.4); 332 this.poseExcavator(this.backhoe,working?[fill-2,0,z-6]:[-64,0,34],working?[fill-2,backfilling?.4:1.5,z+(backfilling?0:4)]:[-59,1,32],backfilling); 333 this.trenchCompactor.g.visible=backfilling;this.trenchCompactor.g.position.set(fill-4,0,z); 334 this.trenchBox.visible=working&&st.pipe>0&&st.fill<24;this.trenchBox.position.set(pipe-3,0,z); 335 this.pipeCarrier.visible=installing&&st.pipe>0&&st.pipe<24;this.pipeCarrier.position.set(pipe-2,mix(.5,-1.05,smooth(phase)),z); 336 this.spoil.forEach((pile,i)=>{const state=st;pile.visible=i<state.dig&&i>=state.fill;pile.position.z=z+(front?.id==='duct'?3.2:5);pile.scale.y=Math.max(.05,clamp(state.dig-i)*clamp(i-state.fill+1));}); 337 this.bedding.forEach((bed,i)=>{bed.visible=i<st.pipe;bed.position.z=z;}); 338 this.pipes.forEach((pieces,line)=>{const stage=trenchStages(progress(s,line?'duct':'drain'));pieces.forEach((piece,i)=>piece.visible=i+1<=stage.pipe);}); 339 } 340 setBeam(mesh,a,b) { 341 const from=new THREE.Vector3(...a),to=new THREE.Vector3(...b),delta=to.clone().sub(from);mesh.position.copy(from.add(to).multiplyScalar(.5));mesh.scale.set(1,1,delta.length());mesh.quaternion.setFromUnitVectors(new THREE.Vector3(0,0,1),delta.normalize()); 342 } 343 updatePumps(s,active) { 344 const v=this.v,tasks=active.filter(t=>t.equipment==='pump'); 345 this.pumps.forEach((p,i)=>{p.truck.g.visible=p.mixer.g.visible=i<tasks.length;if(!tasks[i])return;const task=tasks[i],amount=progress(s,task.id),target=this.workLocation(task,amount),front=v.model.fronts.find(f=>f.key===task.zone),x=front?front.x+front.width/2+6:target[0]+7;this.placeVehicle(p.truck,[x,0,target[2]],null);this.placeVehicle(p.mixer,[x+1,0,target[2]+8],null);p.drum.rotation.z=this.clock*.8; 346 const local=[target[0]-x,.8,0],points=[[0,3,0],[0,13,-3],[local[0]/2,15,0],[local[0],6,0]];
346p.arms.forEach((arm,j)=>this.setBeam(arm,points[j],points[j+1]));p.hose.position.set(local[0],3.4,0);p.hose.scale.y=5.2;p.boom.visible=amount>.25; 347 }); 348 } 349 updateLifts(s,active) { 350 const tasks=active.filter(t=>t.equipment==='lift');this.lifts.forEach((lift,i)=>{const task=tasks[Math.floor(i/2)];lift.g.visible=!!task;if(!task)return;const p=this.workLocation(task,progress(s,task.id),i),height=C.site(s.site).type==='fab'?10:6;lift.g.position.set(p[0]+(i%2?4:-4),0,p[2]);lift.platform.position.y=height;lift.arm.scale.y=height/5;lift.arm.position.y=height/2;}); 351 } 352 dispose() { for(const label of this.labels||[]){label.texture.dispose();label.material.dispose();}this.dust.material.dispose(); } 353}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.