PageSourceSearch

https://openmud.ai/assets/js/jobsite-campus-actors.js

js openmud.ai collected 2026-10-05 15:02:42 UTC 35,077 bytes, 353 lines download raw bytes

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.