devagent-ignition 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- devagent_hmi.py +471 -0
- devagent_ignition-0.1.0.dist-info/METADATA +204 -0
- devagent_ignition-0.1.0.dist-info/RECORD +133 -0
- devagent_ignition-0.1.0.dist-info/WHEEL +4 -0
- devagent_ignition-0.1.0.dist-info/entry_points.txt +2 -0
- devagent_spatial/__init__.py +1 -0
- devagent_spatial/_resources/README.md +187 -0
- devagent_spatial/_resources/RELEASE_MANIFEST.json +312 -0
- devagent_spatial/_resources/SHA256SUMS.txt +270 -0
- devagent_spatial/_resources/SOURCE_PROVENANCE.json +186 -0
- devagent_spatial/_resources/assets/agent.js +102 -0
- devagent_spatial/_resources/assets/creator.css +247 -0
- devagent_spatial/_resources/assets/creator_2d.mjs +16 -0
- devagent_spatial/_resources/assets/creator_original_r9.mjs +58 -0
- devagent_spatial/_resources/assets/creator_original_v8.mjs +120 -0
- devagent_spatial/_resources/assets/creator_r9_diagnostics.mjs +53 -0
- devagent_spatial/_resources/assets/creator_scene.mjs +128 -0
- devagent_spatial/_resources/assets/creator_ui.mjs +60 -0
- devagent_spatial/_resources/assets/customer_documents.js +110 -0
- devagent_spatial/_resources/assets/equipment_controls.mjs +27 -0
- devagent_spatial/_resources/assets/facility2d.css +24 -0
- devagent_spatial/_resources/assets/facility2d_app.mjs +45 -0
- devagent_spatial/_resources/assets/facility2d_core.mjs +114 -0
- devagent_spatial/_resources/assets/facility2d_simulation.mjs +167 -0
- devagent_spatial/_resources/assets/facility2d_viewer.html +7 -0
- devagent_spatial/_resources/assets/fallback_scene.js +17 -0
- devagent_spatial/_resources/assets/flow_geometry.mjs +218 -0
- devagent_spatial/_resources/assets/harness.js +78 -0
- devagent_spatial/_resources/assets/live_design_review.js +129 -0
- devagent_spatial/_resources/assets/operations_ui.js +305 -0
- devagent_spatial/_resources/assets/operator_viewer.html +64 -0
- devagent_spatial/_resources/assets/project_package.js +52 -0
- devagent_spatial/_resources/assets/r9_transport.mjs +70 -0
- devagent_spatial/_resources/assets/runtime_feed.js +30 -0
- devagent_spatial/_resources/assets/studio.html +139 -0
- devagent_spatial/_resources/assets/style.css +191 -0
- devagent_spatial/_resources/assets/test_workspace.html +195 -0
- devagent_spatial/_resources/assets/three_scene.js +442 -0
- devagent_spatial/_resources/assets/unified_engineer.js +101 -0
- devagent_spatial/_resources/assets/v076_fat_ui.js +40 -0
- devagent_spatial/_resources/assets/v078_command_ui.js +44 -0
- devagent_spatial/_resources/assets/viewer.html +45 -0
- devagent_spatial/_resources/assets/viewer.js +112 -0
- devagent_spatial/_resources/design-system.json +130 -0
- devagent_spatial/_resources/examples/CV101-command-contract-PENDING.json +30 -0
- devagent_spatial/_resources/examples/cv101_guided.json +272 -0
- devagent_spatial/_resources/examples/make_examples.py +36 -0
- devagent_spatial/_resources/examples/single_floor.json +444 -0
- devagent_spatial/_resources/examples/v0771_training_wizard.json +11 -0
- devagent_spatial/_resources/examples/warehouse-3floor-standard-2d.json +708 -0
- devagent_spatial/_resources/examples/warehouse-operations-synthetic.json +1186 -0
- devagent_spatial/_resources/examples/warehouse-v07-lab-bindings-PENDING.json +372 -0
- devagent_spatial/_resources/examples/warehouse.json +708 -0
- devagent_spatial/_resources/examples/water.json +288 -0
- devagent_spatial/_resources/reference/r9-golden/REFERENCE_SHA256.json +13 -0
- devagent_spatial/_resources/reference/r9-golden/web/app.mjs +745 -0
- devagent_spatial/_resources/reference/r9-golden/web/index.html +213 -0
- devagent_spatial/_resources/reference/r9-golden/web/lab_panel.mjs +53 -0
- devagent_spatial/_resources/reference/r9-golden/web/model.mjs +367 -0
- devagent_spatial/_resources/reference/r9-golden/web/state_adapter.mjs +78 -0
- devagent_spatial/_resources/reference/r9-golden/web/styles.css +76 -0
- devagent_spatial/_resources/reference/r9-original/README_R9.md +27 -0
- devagent_spatial/_resources/reference/r9-original/SHA256SUMS.txt +63 -0
- devagent_spatial/_resources/reference/r9-original/SNAPSHOT_SHA256SUMS.txt +16 -0
- devagent_spatial/_resources/reference/r9-original/flow_routes.json +527 -0
- devagent_spatial/_resources/reference/r9-original/flow_sim.py +117 -0
- devagent_spatial/_resources/reference/r9-original/lab_control.py +73 -0
- devagent_spatial/_resources/reference/r9-original/web/app.mjs +745 -0
- devagent_spatial/_resources/reference/r9-original/web/index.html +213 -0
- devagent_spatial/_resources/reference/r9-original/web/lab_panel.mjs +53 -0
- devagent_spatial/_resources/reference/r9-original/web/model.mjs +367 -0
- devagent_spatial/_resources/reference/r9-original/web/state_adapter.mjs +78 -0
- devagent_spatial/_resources/reference/r9-original/web/styles.css +76 -0
- devagent_spatial/_resources/reference/r9-original/web/vendor/LICENSE-three.txt +21 -0
- devagent_spatial/_resources/reference/r9-original/web/vendor/SHA256SUMS.txt +4 -0
- devagent_spatial/_resources/reference/r9-original/web/vendor/addons/controls/OrbitControls.js +1860 -0
- devagent_spatial/_resources/reference/r9-original/web/vendor/three.core.js +58773 -0
- devagent_spatial/_resources/reference/r9-original/web/vendor/three.module.js +18251 -0
- devagent_spatial/_resources/reference/v8-original/README.md +36 -0
- devagent_spatial/_resources/reference/v8-original/SHA256SUMS.txt +12 -0
- devagent_spatial/_resources/reference/v8-original/model.py +124 -0
- devagent_spatial/_resources/reference/v8-original/run_demo.sh +4 -0
- devagent_spatial/_resources/reference/v8-original/server.py +100 -0
- devagent_spatial/_resources/reference/v8-original/tests/browser_smoke.py +50 -0
- devagent_spatial/_resources/reference/v8-original/tests/test_v8.py +144 -0
- devagent_spatial/_resources/reference/v8-original/topology.json +24 -0
- devagent_spatial/_resources/reference/v8-original/web/app.js +114 -0
- devagent_spatial/_resources/reference/v8-original/web/index.html +22 -0
- devagent_spatial/_resources/reference/v8-original/web/simulator.html +1 -0
- devagent_spatial/_resources/reference/v8-original/web/simulator.js +6 -0
- devagent_spatial/_resources/reference/v8-original/web/style.css +16 -0
- devagent_spatial/_resources/sample_customer_inputs/Plant_Equipment_Register.csv +4 -0
- devagent_spatial/_resources/sample_customer_inputs/Plant_Flow_Requirements.txt +5 -0
- devagent_spatial/_resources/sample_customer_inputs/Raw_PLC_Tag_Export.csv +4 -0
- devagent_spatial/_resources/templates/warehouse_connections.csv +12 -0
- devagent_spatial/_resources/templates/warehouse_equipment.csv +21 -0
- devagent_spatial/_resources/templates/warehouse_signals.csv +34 -0
- devagent_spatial/_resources/templates/water_connections.csv +7 -0
- devagent_spatial/_resources/templates/water_equipment.csv +10 -0
- devagent_spatial/_resources/templates/water_signals.csv +12 -0
- devagent_spatial/agent_harness.py +299 -0
- devagent_spatial/agent_runtime.py +314 -0
- devagent_spatial/contract_opc_fat.py +216 -0
- devagent_spatial/contract_opc_source.py +265 -0
- devagent_spatial/control_intents.py +38 -0
- devagent_spatial/customer_intake.py +235 -0
- devagent_spatial/engineering_expertise.py +114 -0
- devagent_spatial/generator.py +201 -0
- devagent_spatial/ignition_native.py +234 -0
- devagent_spatial/ignition_project.py +144 -0
- devagent_spatial/live_lab_binding.py +128 -0
- devagent_spatial/model.py +297 -0
- devagent_spatial/native_reference.py +69 -0
- devagent_spatial/opc_snapshot.py +87 -0
- devagent_spatial/operations.py +284 -0
- devagent_spatial/operations_capture.py +47 -0
- devagent_spatial/operations_lab.py +64 -0
- devagent_spatial/original_r9.py +169 -0
- devagent_spatial/original_v8.py +81 -0
- devagent_spatial/preflight.py +140 -0
- devagent_spatial/preview_simulation.py +189 -0
- devagent_spatial/project_package.py +101 -0
- devagent_spatial/qualification.py +152 -0
- devagent_spatial/strict_json.py +29 -0
- devagent_spatial/studio.py +894 -0
- devagent_spatial/studio_workflow.py +277 -0
- devagent_spatial/test_reporting.py +87 -0
- devagent_spatial/v076_opc_fat.py +180 -0
- devagent_spatial/v078_commands.py +277 -0
- devagent_spatial/v078_native_commands.py +161 -0
- devagent_spatial/v07_fat.py +110 -0
- devagent_spatial/v07_native.py +318 -0
- devagent_spatial/v083_native.py +349 -0
|
@@ -0,0 +1,745 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
|
|
3
|
+
import {EQUIPMENT,EQUIPMENT_BY_ID,FLOORS,ROUTES,INCLINE_ENGINEERING_DEMO,BASE_V1C_IDS,HUB_DESIGN,R9_STATION_IDS,gradeStats,validateModel} from './model.mjs';
|
|
4
|
+
import {extractEquipment,summarizeState,extractParcelState} from './state_adapter.mjs';
|
|
5
|
+
const $=id=>document.getElementById(id), violations=validateModel();
|
|
6
|
+
window.devagentSourceMode='SIMULATION';
|
|
7
|
+
if(violations.length)throw Error('Model contract invalid: '+violations.join('; '));
|
|
8
|
+
const COLORS={on:0x48d6c2,fault:0xf07281,blocked:0xefb65a,unknown:0x7b91a0,selected:0xd1fcff};
|
|
9
|
+
const mount=$('canvas-mount'),stage=$('stage');
|
|
10
|
+
const renderer=new THREE.WebGLRenderer({antialias:true,alpha:true,powerPreference:'high-performance'});
|
|
11
|
+
renderer.setPixelRatio(Math.min(devicePixelRatio,2));
|
|
12
|
+
renderer.setSize(mount.clientWidth,mount.clientHeight);renderer.outputColorSpace=THREE.SRGBColorSpace;
|
|
13
|
+
renderer.toneMapping=THREE.ACESFilmicToneMapping;renderer.toneMappingExposure=1.3;
|
|
14
|
+
mount.appendChild(renderer.domElement);
|
|
15
|
+
const scene=new THREE.Scene();scene.fog=new THREE.FogExp2(0x081825,.0075);
|
|
16
|
+
const camera=new THREE.PerspectiveCamera(40,1,.1,180);
|
|
17
|
+
const CAMERA_START=new THREE.Vector3(45,29,38),FOCUS_START=new THREE.Vector3(1,6.5,0);
|
|
18
|
+
camera.position.copy(CAMERA_START);
|
|
19
|
+
const controls=new OrbitControls(camera,renderer.domElement);controls.enableDamping=true;controls.dampingFactor=.075;
|
|
20
|
+
controls.minDistance=7;controls.maxDistance=120;controls.maxPolarAngle=Math.PI*.51;controls.target.copy(FOCUS_START);controls.update();
|
|
21
|
+
scene.add(new THREE.HemisphereLight(0xc3eafb,0x18232b,2.0));
|
|
22
|
+
const keyLight=new THREE.DirectionalLight(0xd8f4ff,2.5);keyLight.position.set(5,24,17);scene.add(keyLight);
|
|
23
|
+
const rimLight=new THREE.DirectionalLight(0x31aaa6,1.1);rimLight.position.set(-18,11,-9);scene.add(rimLight);
|
|
24
|
+
const warm=new THREE.PointLight(0xf1d294,36,45);warm.position.set(7,17,-8);scene.add(warm);
|
|
25
|
+
const pointGeo=new THREE.SphereGeometry(.10,9,9);
|
|
26
|
+
const reusableRollerGeo=new THREE.CylinderGeometry(.085,.085,.75,10);
|
|
27
|
+
const reusableLegGeo=new THREE.CylinderGeometry(.055,.075,1,8);
|
|
28
|
+
const parcelGeo=new THREE.BoxGeometry(.57,.43,.47);
|
|
29
|
+
const parcelMat=new THREE.MeshStandardMaterial({color:0xb98b58,roughness:.86,metalness:.08});
|
|
30
|
+
const parcelEdgeMat=new THREE.LineBasicMaterial({color:0xd9b487,transparent:true,opacity:.65});
|
|
31
|
+
const parcelEdges=new THREE.EdgesGeometry(parcelGeo);
|
|
32
|
+
const THREE_UP=new THREE.Vector3(0,1,0);
|
|
33
|
+
let viewMode='stacked',showLabels=true,selected='SPIRAL01',live=false,v1cOffline=false,liveSnapshot={},liveParcelState={count:null,positions:[]},lastLiveAt=0,connectionBusy=false,parcelTime=0,delivered=0;
|
|
34
|
+
const floorGroups={},deviceGroups={},deviceMaterials={},deviceIcons={},deviceLabels={},paths={},flowLengths={},connectors={};
|
|
35
|
+
const raycastMeshes=[],parcels=[],liveParcels=[];const peIndicators={},scannerIndicators={};let showAllPE=false;let currentSensorFeed=null;let lastSensorSequence=-1;let lastSensorReceivedAt=0;const off={1:0,2:0,3:0};
|
|
36
|
+
function defaultState(e){return e?.demoOnly?{running:null,blocked:null,fault:null,speed:null,peIn:null,peOut:null,quality:'CONCEPT'}:{running:true,blocked:false,fault:false,speed:1.2,peIn:false,peOut:false,quality:'DEMO'}}
|
|
37
|
+
const equipState=Object.fromEntries(EQUIPMENT.map(e=>[e.id,defaultState(e)]));
|
|
38
|
+
function stateFor(id){return equipState[id]||{running:null,blocked:null,fault:null,speed:null,peIn:null,peOut:null,quality:'UNKNOWN'}}
|
|
39
|
+
function assetStateColor(state){return state.fault===true?COLORS.fault:state.blocked===true?COLORS.blocked:state.running===true?COLORS.on:COLORS.unknown}
|
|
40
|
+
function makeFloor(floor,meta){
|
|
41
|
+
const g=new THREE.Group();floorGroups[floor]=g;scene.add(g);
|
|
42
|
+
const y=meta.elevation-.49;
|
|
43
|
+
const dims=floor===1?[54,25]:floor===2?[35,19]:[44,26];
|
|
44
|
+
const slab=new THREE.Mesh(new THREE.BoxGeometry(dims[0],.22,dims[1]),new THREE.MeshStandardMaterial({color:meta.tint,metalness:.52,roughness:.66,transparent:true,opacity:.42,depthWrite:false}));
|
|
45
|
+
slab.position.set(1.1,y,-.05);g.add(slab);g.userData.slab=slab;
|
|
46
|
+
// Rectangular floor grid stays inside the actual slab (no stray square grid).
|
|
47
|
+
const gridPoints=[];
|
|
48
|
+
for(let x=-dims[0]/2;x<=dims[0]/2+.01;x+=1){
|
|
49
|
+
gridPoints.push(new THREE.Vector3(x,0,-dims[1]/2),new THREE.Vector3(x,0,dims[1]/2));
|
|
50
|
+
}
|
|
51
|
+
for(let z=-dims[1]/2;z<=dims[1]/2+.01;z+=1){
|
|
52
|
+
gridPoints.push(new THREE.Vector3(-dims[0]/2,0,z),new THREE.Vector3(dims[0]/2,0,z));
|
|
53
|
+
}
|
|
54
|
+
const lines=new THREE.LineSegments(new THREE.BufferGeometry().setFromPoints(gridPoints),new THREE.LineBasicMaterial({color:0x21465b,opacity:.33,transparent:true}));
|
|
55
|
+
lines.position.set(1.1,y+.12,-.05);g.add(lines);g.userData.grid=lines;
|
|
56
|
+
const outline=new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.BoxGeometry(dims[0],.22,dims[1])),new THREE.LineBasicMaterial({color:floor===3?0x42c4b9:0x4489aa,transparent:true,opacity:.35}));outline.position.copy(slab.position);g.add(outline);g.userData.outline=outline;
|
|
57
|
+
// Vertical corner guides are architectural cues, not asserted building dimensions.
|
|
58
|
+
for(const x of [1.1-dims[0]/2,1.1+dims[0]/2])for(const z of [-dims[1]/2,dims[1]/2]){
|
|
59
|
+
const post=new THREE.Mesh(new THREE.CylinderGeometry(.055,.055,1.8,6),new THREE.MeshStandardMaterial({color:0x476777,metalness:.84,roughness:.32}));
|
|
60
|
+
post.position.set(x,meta.elevation-1.35,z);g.add(post);
|
|
61
|
+
}
|
|
62
|
+
const sign=makeTextSprite(`0${floor} ${meta.name}`,0x88cbd7,0x0a2533,27,580,72);
|
|
63
|
+
sign.position.set(-13.2,meta.elevation+.75,-dims[1]/2+.25);sign.scale.set(7.2,.88,1);g.add(sign);
|
|
64
|
+
}
|
|
65
|
+
function makeTextSprite(str,fg,bg,fontsize=29,width=330,height=70){
|
|
66
|
+
const c=document.createElement('canvas');c.width=width;c.height=height;const x=c.getContext('2d');
|
|
67
|
+
x.fillStyle='#'+bg.toString(16).padStart(6,'0');x.globalAlpha=.92;x.fillRect(0,0,width,height);x.globalAlpha=1;
|
|
68
|
+
x.lineWidth=3;x.strokeStyle='#'+fg.toString(16).padStart(6,'0');x.strokeRect(2,2,width-4,height-4);
|
|
69
|
+
x.font=`750 ${fontsize}px Arial, sans-serif`;x.fillStyle='#e4f6f8';x.textAlign='center';x.textBaseline='middle';x.fillText(str,width/2,height/2,width-26);
|
|
70
|
+
const texture=new THREE.CanvasTexture(c);texture.colorSpace=THREE.SRGBColorSpace;
|
|
71
|
+
return new THREE.Sprite(new THREE.SpriteMaterial({map:texture,transparent:true,depthTest:true,depthWrite:false}));
|
|
72
|
+
}
|
|
73
|
+
for(const [f,meta] of Object.entries(FLOORS))makeFloor(Number(f),meta);
|
|
74
|
+
// Sampling into true three-dimensional ribbon geometry, including vertical inclines and helical transport.
|
|
75
|
+
function effectivePoint(e,p,t){
|
|
76
|
+
const from=off[e.floor],to=off[e.toFloor||e.floor];
|
|
77
|
+
// Presentation-only floor explosion must preserve the incline profile's true
|
|
78
|
+
// grade distribution rather than using the sample index as height fraction.
|
|
79
|
+
const alpha=e.connector?(p[1]-e.path[0][1])/
|
|
80
|
+
(e.path.at(-1)[1]-e.path[0][1]):0;
|
|
81
|
+
return new THREE.Vector3(p[0],p[1]+(e.connector?from+(to-from)*alpha:0),p[2]);
|
|
82
|
+
}
|
|
83
|
+
function getCurve(e){
|
|
84
|
+
const pts=e.path.map((p,i)=>effectivePoint(e,p,i/(e.path.length-1)));
|
|
85
|
+
return e.graded ? (()=>{const path=new THREE.CurvePath();for(let i=1;i<pts.length;i++)path.add(new THREE.LineCurve3(pts[i-1],pts[i]));return path;})() : new THREE.CatmullRomCurve3(pts,false,'centripetal',.34);
|
|
86
|
+
}
|
|
87
|
+
function pathGeom(curve,width=.85,shift=0,stepCount=null){
|
|
88
|
+
const length=curve.getLength(),n=stepCount||Math.max(15,Math.ceil(length*9));
|
|
89
|
+
const vs=[],ns=[],uv=[],idx=[];
|
|
90
|
+
for(let i=0;i<=n;i++){
|
|
91
|
+
const t=i/n,p=curve.getPointAt(t),tan=curve.getTangentAt(t),normal=new THREE.Vector3(-tan.z,0,tan.x).normalize();
|
|
92
|
+
for(const side of [-1,1]){const v=p.clone().addScaledVector(normal,side*width*.5);vs.push(v.x,v.y+shift,v.z);ns.push(0,1,0);uv.push(t*length,side===-1?0:1);}
|
|
93
|
+
if(i<n){const j=2*i;idx.push(j,j+1,j+2,j+1,j+3,j+2)}
|
|
94
|
+
}
|
|
95
|
+
const geom=new THREE.BufferGeometry();geom.setAttribute('position',new THREE.Float32BufferAttribute(vs,3));geom.setAttribute('normal',new THREE.Float32BufferAttribute(ns,3));geom.setAttribute('uv',new THREE.Float32BufferAttribute(uv,2));geom.setIndex(idx);geom.computeVertexNormals();return geom;
|
|
96
|
+
}
|
|
97
|
+
function tube(curve,width=.035){return new THREE.TubeGeometry(curve,Math.max(12,Math.floor(curve.getLength()*5)),width,6,false)}
|
|
98
|
+
function sideCurve(curve,side,width,raise=0){
|
|
99
|
+
const out=[],n=Math.max(16,Math.ceil(curve.getLength()*6));
|
|
100
|
+
for(let i=0;i<=n;i++){
|
|
101
|
+
const t=i/n,p=curve.getPointAt(t),tan=curve.getTangentAt(t),norm=new THREE.Vector3(-tan.z,0,tan.x).normalize();
|
|
102
|
+
out.push(p.addScaledVector(norm,side*width).add(new THREE.Vector3(0,raise,0)));
|
|
103
|
+
}
|
|
104
|
+
return new THREE.CatmullRomCurve3(out,false,'centripetal');
|
|
105
|
+
}
|
|
106
|
+
// Real-world inspired compact opposed-mode PE housing: painted dark polymer,
|
|
107
|
+
// short steel L-bracket, inset optical window, and a *small* status LED.
|
|
108
|
+
// These are visual approximations only, not vendor CAD or certified I/O.
|
|
109
|
+
const peHousingMaterial=new THREE.MeshStandardMaterial({color:0x263844,metalness:.25,roughness:.68});
|
|
110
|
+
const peReceiverMaterial=new THREE.MeshStandardMaterial({color:0x384956,metalness:.25,roughness:.7});
|
|
111
|
+
const peMountMaterial=new THREE.MeshStandardMaterial({color:0x617784,metalness:.7,roughness:.42});
|
|
112
|
+
const peOpticMaterial=new THREE.MeshStandardMaterial({color:0x091b27,metalness:.05,roughness:.22});
|
|
113
|
+
const PE_LABEL_ZOOM=19;
|
|
114
|
+
function peLabelVisible(eid){return eid===selected||(showAllPE&&camera.position.distanceTo(controls.target)<PE_LABEL_ZOOM)}
|
|
115
|
+
const steelMaterial=new THREE.MeshStandardMaterial({color:0x516775,metalness:.81,roughness:.36});
|
|
116
|
+
const rollerMaterial=new THREE.MeshStandardMaterial({color:0x6f8490,metalness:.83,roughness:.32});
|
|
117
|
+
const legMaterial=new THREE.MeshStandardMaterial({color:0x405d6b,metalness:.82,roughness:.4});
|
|
118
|
+
function buildConveyor(e){
|
|
119
|
+
const g=new THREE.Group();g.userData.equipmentId=e.id;deviceGroups[e.id]=g;
|
|
120
|
+
const parent=e.connector?scene:floorGroups[e.floor];parent.add(g);if(e.connector)connectors[e.id]=g;
|
|
121
|
+
const curve=getCurve(e);paths[e.id]=curve;flowLengths[e.id]=curve.getLength();
|
|
122
|
+
const narrow=e.id.startsWith('R9_SPIRAL_')||e.id.startsWith('R9_CHUTE_')||e.id.startsWith('R9_TAKEAWAY_');
|
|
123
|
+
const under=new THREE.Mesh(pathGeom(curve,narrow?.88:1.23,-.26),new THREE.MeshStandardMaterial({color:0x456171,roughness:.35,metalness:.72,side:THREE.DoubleSide}));under.userData.equipmentId=e.id;g.add(under);raycastMeshes.push(under);
|
|
124
|
+
const beltMat=new THREE.MeshStandardMaterial({color:0x1a303a,metalness:.32,roughness:.82,side:THREE.DoubleSide,emissive:0x0e3938,emissiveIntensity:.26,transparent:true,opacity:1});
|
|
125
|
+
const belt=new THREE.Mesh(pathGeom(curve,narrow?.70:.96,.03),beltMat);belt.userData.equipmentId=e.id;g.add(belt);raycastMeshes.push(belt);deviceMaterials[e.id]=beltMat;
|
|
126
|
+
for(const side of [-1,1]){
|
|
127
|
+
const edge=new THREE.Mesh(tube(sideCurve(curve,side,narrow?.41:.56,.13),.04),steelMaterial);g.add(edge);
|
|
128
|
+
const luminous=new THREE.Mesh(tube(sideCurve(curve,side,narrow?.38:.52,.08),.018),new THREE.MeshBasicMaterial({color:0x2d7a83,transparent:true,opacity:.65}));g.add(luminous);
|
|
129
|
+
}
|
|
130
|
+
// Production-inspired rollers on straights and turns: optimized shared geometry/material.
|
|
131
|
+
const n=Math.min(54,Math.floor(curve.getLength()/.8));
|
|
132
|
+
for(let i=1;i<n;i++){
|
|
133
|
+
const t=i/n,p=curve.getPointAt(t),tan=curve.getTangentAt(t),across=new THREE.Vector3(-tan.z,0,tan.x).normalize();
|
|
134
|
+
const roller=new THREE.Mesh(reusableRollerGeo,rollerMaterial);roller.position.copy(p);roller.position.y-=.04;
|
|
135
|
+
roller.quaternion.setFromUnitVectors(THREE_UP,across);g.add(roller);
|
|
136
|
+
}
|
|
137
|
+
if(e.id==='INCL01'){
|
|
138
|
+
// Light structural support bents demonstrate actual vertical height; not certified design.
|
|
139
|
+
const ground=FLOORS[1].elevation-.51+off[1];
|
|
140
|
+
for(const t of [.12,.31,.50,.69,.88]){
|
|
141
|
+
const center=curve.getPointAt(t),tan=curve.getTangentAt(t).normalize();
|
|
142
|
+
const side=new THREE.Vector3(-tan.z,0,tan.x).normalize();
|
|
143
|
+
const supportY=center.y-.37;
|
|
144
|
+
const height=supportY-ground;
|
|
145
|
+
if(height<.55)continue;
|
|
146
|
+
for(const sign of [-1,1]){
|
|
147
|
+
const leg=new THREE.Mesh(reusableLegGeo,legMaterial);
|
|
148
|
+
leg.scale.set(1,height,1);
|
|
149
|
+
leg.position.copy(center).addScaledVector(side,sign*.48);
|
|
150
|
+
leg.position.y=ground+height/2;
|
|
151
|
+
g.add(leg);
|
|
152
|
+
}
|
|
153
|
+
const cross=new THREE.Mesh(new THREE.BoxGeometry(1.14,.085,.085),legMaterial);
|
|
154
|
+
// Align crossbar with the lateral direction.
|
|
155
|
+
cross.quaternion.setFromUnitVectors(new THREE.Vector3(1,0,0),side);
|
|
156
|
+
cross.position.copy(center);cross.position.y=supportY-.08;g.add(cross);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if(e.id==='SPIRAL01'||e.id.startsWith('DOWN_')||e.id==='R6_DOWN_F2'||e.id.startsWith('R9_SPIRAL_')){
|
|
160
|
+
const lo=e.id.startsWith('DOWN_')?FLOORS[1].elevation+off[1]:FLOORS[2].elevation+off[2];
|
|
161
|
+
const hi=e.id.startsWith('R9_SPIRAL_')?FLOORS[3].elevation-.30+off[3]:e.id==='R6_DOWN_F2'?FLOORS[3].elevation-1.0+off[3]:e.id.startsWith('DOWN_')?FLOORS[3].elevation-.5+off[3]:FLOORS[3].elevation+off[3];
|
|
162
|
+
const h=Math.abs(hi-lo);
|
|
163
|
+
const column=new THREE.Mesh(new THREE.CylinderGeometry(.13,.13,h,12),legMaterial);
|
|
164
|
+
column.position.set(e.id.startsWith('R9_SPIRAL_')?e.path[0][0]:e.id==='R6_DOWN_F2'?4:e.id.startsWith('DOWN_')?17:-9.2,(lo+hi)/2,e.id.startsWith('R9_SPIRAL_')?e.path[0][2]+.80:e.id==='R6_DOWN_F2'?-5.85:e.id==='DOWN_A'?-7:e.id==='DOWN_C'?7:0);
|
|
165
|
+
g.add(column);
|
|
166
|
+
}
|
|
167
|
+
if(!e.connector){
|
|
168
|
+
for(const t of [.12,.52,.88]){
|
|
169
|
+
const p=curve.getPointAt(t);if(p.y<.8)continue;
|
|
170
|
+
const length=.63;const leg=new THREE.Mesh(reusableLegGeo,legMaterial);leg.scale.y=length;
|
|
171
|
+
leg.position.set(p.x,p.y-.26-length/2,p.z);g.add(leg);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const p=curve.getPointAt(e.connector?.58:.51);
|
|
175
|
+
const compactR9=e.id.match(/^R9_(SORT|CHUTE)_(\d{2})$/);
|
|
176
|
+
const visualLabel=compactR9?(compactR9[1]==='SORT'?'S':'C')+compactR9[2]:e.id;
|
|
177
|
+
const label=makeTextSprite(visualLabel,0x2fa8b3,0x0d2937,29);
|
|
178
|
+
label.scale.set(e.id.length>7?2.35:1.99,.46,1);
|
|
179
|
+
label.position.set(p.x,p.y+(e.connector?.89:.69),p.z+.20);label.userData.equipmentId=e.id;label.visible=!/^R9_(COLLECT|JOIN)_/.test(e.id);g.add(label);deviceLabels[e.id]=label;raycastMeshes.push(label);
|
|
180
|
+
const indicator=new THREE.Mesh(pointGeo,new THREE.MeshBasicMaterial({color:COLORS.on}));
|
|
181
|
+
indicator.position.set(p.x,p.y+.38,p.z);g.add(indicator);deviceIcons[e.id]=indicator;
|
|
182
|
+
// Concept-only expansion avoids multiplying expensive active point lights.
|
|
183
|
+
// Preserve every original device's light and operational color behavior.
|
|
184
|
+
if(!e.demoOnly){const lamp=new THREE.PointLight(COLORS.on,.45,2.2);lamp.position.copy(indicator.position);g.add(lamp);indicator.userData.light=lamp;}
|
|
185
|
+
const selectOutline=new THREE.Mesh(new THREE.TorusGeometry(.56,.018,6,32),new THREE.MeshBasicMaterial({color:0x9be9ec,transparent:true,opacity:.9,depthWrite:false}));
|
|
186
|
+
selectOutline.rotation.x=Math.PI/2;selectOutline.position.copy(p);selectOutline.position.y+=.30;selectOutline.visible=false;g.add(selectOutline);g.userData.selectOutline=selectOutline;
|
|
187
|
+
// Paired photoeyes at inlet/outlet. Beam obstruction comes only from
|
|
188
|
+
// server-owned parcel positions; these are VIRTUAL devices, not PLC inputs.
|
|
189
|
+
// IMPORTANT: create these meshes BEFORE returning the conveyor group.
|
|
190
|
+
peIndicators[e.id]={};
|
|
191
|
+
for(const [key,t,side] of [['PE_IN',.08,-1],['PE_OUT',.92,1]]){
|
|
192
|
+
const center=curve.getPointAt(t),tangent=curve.getTangentAt(t).normalize();
|
|
193
|
+
const lateral=new THREE.Vector3(-tangent.z,0,tangent.x).normalize();
|
|
194
|
+
const normalUp=lateral.clone().cross(tangent).normalize();
|
|
195
|
+
// Mount just OUTSIDE the rail, aligned to curved or inclined conveyor path.
|
|
196
|
+
// A 0.22-unit housing is < 1/4 of the 0.96-unit belt width.
|
|
197
|
+
const sensor=new THREE.Group();
|
|
198
|
+
sensor.position.copy(center).addScaledVector(lateral,side*.72).addScaledVector(normalUp,.16);
|
|
199
|
+
sensor.quaternion.setFromRotationMatrix(new THREE.Matrix4().makeBasis(tangent,normalUp,lateral));
|
|
200
|
+
const bracket=new THREE.Mesh(new THREE.BoxGeometry(.15,.065,.14),peMountMaterial);
|
|
201
|
+
bracket.position.set(0,-.095,0);sensor.add(bracket);
|
|
202
|
+
const housing=new THREE.Mesh(new THREE.BoxGeometry(.22,.17,.16),peHousingMaterial);
|
|
203
|
+
housing.position.y=.03;sensor.add(housing);
|
|
204
|
+
// Dark optical lens faces inward; unlike R2 it never glows as a huge ball.
|
|
205
|
+
const optic=new THREE.Mesh(new THREE.BoxGeometry(.105,.072,.012),peOpticMaterial);
|
|
206
|
+
optic.position.set(0,.035,-side*.087);sensor.add(optic);
|
|
207
|
+
const ledMaterial=new THREE.MeshStandardMaterial({color:COLORS.unknown,emissive:COLORS.unknown,emissiveIntensity:.68,roughness:.35});
|
|
208
|
+
const lens=new THREE.Mesh(new THREE.BoxGeometry(.085,.023,.06),ledMaterial);
|
|
209
|
+
lens.position.set(0,.127,0);sensor.add(lens);
|
|
210
|
+
// Opposed receiver: another small rectangular device on the opposite rail.
|
|
211
|
+
const receiver=new THREE.Mesh(new THREE.BoxGeometry(.16,.14,.13),peReceiverMaterial);
|
|
212
|
+
receiver.position.set(0,0,-side*1.44);sensor.add(receiver);
|
|
213
|
+
const rxOptic=new THREE.Mesh(new THREE.BoxGeometry(.073,.05,.012),peOpticMaterial);
|
|
214
|
+
rxOptic.position.set(0,.015,-side*1.44+side*.072);sensor.add(rxOptic);
|
|
215
|
+
// Beam is an optional THIN line, not a permanent glowing bridge.
|
|
216
|
+
const beam=new THREE.Mesh(new THREE.CylinderGeometry(.007,.007,1.30,6),new THREE.MeshBasicMaterial({color:COLORS.unknown,transparent:true,opacity:0,depthWrite:false}));
|
|
217
|
+
beam.rotation.x=Math.PI/2;
|
|
218
|
+
beam.position.set(0,.035,-side*.72);beam.visible=false;sensor.add(beam);
|
|
219
|
+
const caption=makeTextSprite(key==='PE_IN'?'PE IN':'PE OUT',0x79c4d6,0x0b2531,23,240,64);
|
|
220
|
+
caption.position.set(0,.43,0);caption.scale.set(.62,.16,1);
|
|
221
|
+
caption.visible=peLabelVisible(e.id);sensor.add(caption);
|
|
222
|
+
g.add(sensor);peIndicators[e.id][key]={lens,housing,receiver,beam,caption};
|
|
223
|
+
}
|
|
224
|
+
return g;
|
|
225
|
+
}
|
|
226
|
+
for(const e of EQUIPMENT)buildConveyor(e);
|
|
227
|
+
// New equipment lives only in the isolated preview. Old flooring and cameras unchanged.
|
|
228
|
+
// R6 visually localized sorter hardware: slatted induction/sort deck and
|
|
229
|
+
// small sliding-shoe pads on EXISTING sorter zones. These are illustrative,
|
|
230
|
+
// non-actuated geometry; no vendor equipment or new PLC state is claimed.
|
|
231
|
+
function buildSorterMechanism(sortId,upstreamId){
|
|
232
|
+
const group=new THREE.Group();group.name='R6 slatted sorter '+sortId;
|
|
233
|
+
const sorter=EQUIPMENT_BY_ID[sortId];floorGroups[sorter.floor].add(group);
|
|
234
|
+
const slatMat=new THREE.MeshStandardMaterial({color:0x8ba4b0,metalness:.7,roughness:.43});
|
|
235
|
+
const shoeMat=new THREE.MeshStandardMaterial({color:0xc2a464,metalness:.52,roughness:.48});
|
|
236
|
+
const baseMat=new THREE.MeshStandardMaterial({color:0x324855,metalness:.77,roughness:.39});
|
|
237
|
+
const sections=upstreamId===sortId?[sortId]:[upstreamId,sortId];
|
|
238
|
+
for(const eid of sections){
|
|
239
|
+
const curve=paths[eid];const n=Math.max(5,Math.round(curve.getLength()/.35));
|
|
240
|
+
for(let i=1;i<n;i++){
|
|
241
|
+
const t=i/n,p=curve.getPointAt(t),tan=curve.getTangentAt(t).normalize();
|
|
242
|
+
const across=new THREE.Vector3(-tan.z,0,tan.x).normalize();
|
|
243
|
+
const slat=new THREE.Mesh(new THREE.BoxGeometry(.075,.035,1.0),slatMat);
|
|
244
|
+
slat.quaternion.setFromUnitVectors(new THREE.Vector3(0,0,1),across);
|
|
245
|
+
slat.position.copy(p).add(new THREE.Vector3(0,.075,0));group.add(slat);
|
|
246
|
+
if(i%3===0){
|
|
247
|
+
const shoe=new THREE.Mesh(new THREE.BoxGeometry(.14,.10,.25),shoeMat);
|
|
248
|
+
shoe.quaternion.setFromUnitVectors(new THREE.Vector3(0,0,1),across);
|
|
249
|
+
shoe.position.copy(p).addScaledVector(across,i%2?.22:-.22);
|
|
250
|
+
shoe.position.y+=.13;group.add(shoe);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// Independent drive housing sits off the conveyor centerline.
|
|
255
|
+
const p=paths[sortId].getPointAt(.6);
|
|
256
|
+
const housing=new THREE.Mesh(new THREE.BoxGeometry(.36,.31,.4),baseMat);
|
|
257
|
+
housing.position.set(p.x,p.y-.12,p.z-.82);group.add(housing);
|
|
258
|
+
}
|
|
259
|
+
buildSorterMechanism('SORT201','CV203');
|
|
260
|
+
buildSorterMechanism('SORT301','CV302');
|
|
261
|
+
// R9 slats/shoes are geometry on the four actual connected sorter stations.
|
|
262
|
+
// Their locations correspond to the selected parcel's discharge, not decorative lines.
|
|
263
|
+
for(const id of R9_STATION_IDS) buildSorterMechanism(id,id);
|
|
264
|
+
buildSorterMechanism('R9_SECOND_SORTER','R9_SECOND_SORTER');
|
|
265
|
+
// Visually distinguish the machine as ONE four-divert sorter rather than four
|
|
266
|
+
// unrelated conveyor tiles. Keep the moving deck clear; all gates are below
|
|
267
|
+
// belt height, off the parcel travel surface (concept-only mechanism geometry).
|
|
268
|
+
const r9Frame=new THREE.Group();floorGroups[3].add(r9Frame);
|
|
269
|
+
const r7CrossMember=new THREE.MeshStandardMaterial({color:0x617784,metalness:.86,roughness:.35});
|
|
270
|
+
const r7DivertMat=new THREE.MeshStandardMaterial({color:0xd6a35d,metalness:.50,roughness:.40});
|
|
271
|
+
for(const [i,id] of R9_STATION_IDS.entries()){
|
|
272
|
+
const e=EQUIPMENT_BY_ID[id],end=paths[id].getPointAt(1),mid=paths[id].getPointAt(.5);
|
|
273
|
+
// Small selector shoe/actuator housing on the discharge-side rail (+Z).
|
|
274
|
+
const actuator=new THREE.Mesh(new THREE.BoxGeometry(.47,.27,.36),r7DivertMat);
|
|
275
|
+
actuator.position.set(end.x-.39,end.y-.20,end.z+.71);r9Frame.add(actuator);
|
|
276
|
+
const status=makeTextSprite(`DIVERT ${String(i+1).padStart(2,'0')}`,0xe6b96f,0x1d3241,19,270,60);
|
|
277
|
+
status.position.set(end.x-.4,end.y+1.25,end.z+1.4);status.scale.set(1.9,.46,1);r9Frame.add(status);
|
|
278
|
+
// Short adjacent supports; not machine guarding or surveyed steelwork.
|
|
279
|
+
for(const xx of [mid.x-1.1,mid.x+1.1]){
|
|
280
|
+
const support=new THREE.Mesh(new THREE.BoxGeometry(.10,1.05,.10),r7CrossMember);
|
|
281
|
+
support.position.set(xx,mid.y-.82,mid.z+.65);r9Frame.add(support);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
// NON-TAGGED ARCHITECTURAL SCENE OBJECTS: These are VISUAL CONCEPTS, not
|
|
288
|
+
// source-system devices. Do not expose their materials as live PLC states.
|
|
289
|
+
const architecture=new THREE.Group();scene.add(architecture);
|
|
290
|
+
const dockSteel=new THREE.MeshStandardMaterial({color:0x718a97,metalness:.65,roughness:.4});
|
|
291
|
+
const dockDark=new THREE.MeshStandardMaterial({color:0x223945,metalness:.48,roughness:.62});
|
|
292
|
+
const safetyYellow=new THREE.MeshStandardMaterial({color:0xf2ba64,emissive:0x533613,emissiveIntensity:.14,roughness:.59});
|
|
293
|
+
const truckWhite=new THREE.MeshStandardMaterial({color:0xc8d8df,metalness:.23,roughness:.72});
|
|
294
|
+
function beam(parent,from,to,thickness,material){
|
|
295
|
+
const start=new THREE.Vector3(...from),end=new THREE.Vector3(...to);
|
|
296
|
+
const mesh=new THREE.Mesh(new THREE.BoxGeometry(thickness,start.distanceTo(end),thickness),material);
|
|
297
|
+
mesh.position.copy(start).add(end).multiplyScalar(.5);
|
|
298
|
+
mesh.quaternion.setFromUnitVectors(THREE_UP,end.clone().sub(start).normalize());parent.add(mesh);return mesh;
|
|
299
|
+
}
|
|
300
|
+
function dockArchitecture(id,x,z,inbound){
|
|
301
|
+
const y=FLOORS[1].elevation;
|
|
302
|
+
const frame=new THREE.Group();floorGroups[1].add(frame);
|
|
303
|
+
for(const dz of [-1.38,1.38])beam(frame,[x,y-.3,z+dz],[x,y+2.9,z+dz],.16,dockSteel);
|
|
304
|
+
beam(frame,[x,y+2.9,z-1.4],[x,y+2.9,z+1.4],.17,safetyYellow);
|
|
305
|
+
// Show the roll-up door in the open position above the parcel path.
|
|
306
|
+
const opening=new THREE.Mesh(new THREE.BoxGeometry(.13,.32,2.6),dockDark);
|
|
307
|
+
opening.position.set(x+(inbound?-.045:.045),y+2.7,z);frame.add(opening);
|
|
308
|
+
const slats=new THREE.MeshStandardMaterial({color:0x344f60,metalness:.52,roughness:.44});
|
|
309
|
+
for(let i=0;i<3;i++){
|
|
310
|
+
const strip=new THREE.Mesh(new THREE.BoxGeometry(.17,.036,2.52),slats);
|
|
311
|
+
strip.position.set(x,y+2.5+i*.105,z);frame.add(strip);
|
|
312
|
+
}
|
|
313
|
+
// Concept trailer body, aligned outside the warehouse loading face.
|
|
314
|
+
const body=new THREE.Mesh(new THREE.BoxGeometry(4.2,2.3,2.9),truckWhite);
|
|
315
|
+
body.position.set(x+(inbound?-3.2:3.2),y+1.0,z);frame.add(body);
|
|
316
|
+
const roof=new THREE.Mesh(new THREE.BoxGeometry(4.4,.08,3.05),dockSteel);
|
|
317
|
+
roof.position.copy(body.position);roof.position.y+=1.2;frame.add(roof);
|
|
318
|
+
for(const shift of [-1.05,1.05]){
|
|
319
|
+
const wheel=new THREE.Mesh(new THREE.CylinderGeometry(.36,.36,.17,14),dockDark);
|
|
320
|
+
wheel.rotation.x=Math.PI/2;wheel.position.set(body.position.x+shift,y-.18,z-1.55);frame.add(wheel);
|
|
321
|
+
}
|
|
322
|
+
const plate=new THREE.Mesh(new THREE.BoxGeometry(.5,.05,2.4),safetyYellow);
|
|
323
|
+
plate.position.set(x+(inbound?.28:-.28),y-.19,z);frame.add(plate);
|
|
324
|
+
const name=makeTextSprite(id+' / '+(inbound?'RECEIVE':'SHIP'),0xf2ba64,0x112a36,23,540,70);
|
|
325
|
+
name.position.set(x,y+3.65,z);name.scale.set(5.7,.66,1);frame.add(name);
|
|
326
|
+
}
|
|
327
|
+
dockArchitecture('IN 01',-21,-7,true);
|
|
328
|
+
dockArchitecture('IN 02',-21,7,true);
|
|
329
|
+
for(const [i,z] of [-7,0,7].entries())dockArchitecture('OUT 0'+(i+1),24,z,false);
|
|
330
|
+
function scannerPortal(x,y,z,id){
|
|
331
|
+
const g=new THREE.Group();(floorGroups[Number(Object.keys(FLOORS).find(k=>Math.abs(FLOORS[k].elevation-y)<.1))]||floorGroups[1]).add(g);
|
|
332
|
+
beam(g,[x,y-.05,z-.8],[x,y+1.55,z-.8],.09,safetyYellow);
|
|
333
|
+
beam(g,[x,y-.05,z+.8],[x,y+1.55,z+.8],.09,safetyYellow);
|
|
334
|
+
beam(g,[x,y+1.55,z-.8],[x,y+1.55,z+.8],.12,safetyYellow);
|
|
335
|
+
const s=makeTextSprite(id,0xf2ba64,0x0e2637,19,340,70);s.position.set(x,y+2.13,z);s.scale.set(2.6,.48,1);g.add(s);
|
|
336
|
+
const lens=new THREE.Mesh(new THREE.SphereGeometry(.19,12,8),new THREE.MeshStandardMaterial({color:COLORS.unknown,emissive:COLORS.unknown,emissiveIntensity:.65}));
|
|
337
|
+
lens.position.set(x,y+1.73,z);g.add(lens);
|
|
338
|
+
scannerIndicators[id==='DEST SCAN'?'DEST_SCAN':'DWS01']={lens,lastRevision:-1};
|
|
339
|
+
}
|
|
340
|
+
scannerPortal(5.55,FLOORS[2].elevation,0,'SCAN / DWS');
|
|
341
|
+
scannerPortal(-5.3,FLOORS[3].elevation,0,'DEST SCAN');
|
|
342
|
+
// Keep the conveyor surface unobstructed. The previous yellow upright panels
|
|
343
|
+
// crossed the material path. A low, continuous guide rail follows each curved
|
|
344
|
+
// chute edge instead; safety guarding requires separate as-built evaluation.
|
|
345
|
+
for(const e of EQUIPMENT.filter(e=>e.chute)){
|
|
346
|
+
const curve=paths[e.id], g=deviceGroups[e.id];
|
|
347
|
+
for(const sign of [-1,1]){
|
|
348
|
+
const railPath=sideCurve(curve,sign,.49,.34);
|
|
349
|
+
g.add(new THREE.Mesh(tube(railPath,.035),steelMaterial));
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
// Dock geometry and scanner portals follow their own floor layer; chute
|
|
353
|
+
// guarding follows equipment visibility in isolated and exploded views.
|
|
354
|
+
|
|
355
|
+
// Parcel movement uses curve arc length, not a set of teleporting equipment positions.
|
|
356
|
+
function makeParcel(i){
|
|
357
|
+
const grp=new THREE.Group();const b=new THREE.Mesh(parcelGeo,parcelMat.clone());b.material.color.setHex([0xc79b66,0xa98759,0xd4aa71,0xb98b58][i%4]);b.userData.parcelId=i;
|
|
358
|
+
const edges=new THREE.LineSegments(parcelEdges,parcelEdgeMat);grp.add(b,edges);
|
|
359
|
+
const stamp=new THREE.Mesh(new THREE.BoxGeometry(.38,.008,.035),new THREE.MeshStandardMaterial({color:0xe5c58f,roughness:1}));stamp.position.set(0,.22,0);grp.add(stamp);
|
|
360
|
+
grp.visible=false;scene.add(grp);return grp;
|
|
361
|
+
}
|
|
362
|
+
function ensureLiveParcels(n){while(liveParcels.length<Math.min(n,150)){const i=liveParcels.length;liveParcels.push({object:makeParcel(200+i)});}}
|
|
363
|
+
for(let i=0;i<8;i++)parcels.push({object:makeParcel(i),route:ROUTES[i%ROUTES.length],progress:i*12.7,i,destination:'DOCK_OUT_'+['A','B','C'][i%3],origin:i%2?'DOCK_IN_B':'DOCK_IN_A'});
|
|
364
|
+
const labelStrings={docks:['SHIPPING DOCKS · GROUND LEVEL','2 inbound unload points · 3 outbound trailer destinations'],incline:['GRADE-CONTROLLED INCLINE · FLOOR 01 → 02','Long powered belt concept · continuous uphill travel'],stacked:['CONNECTED PARCEL HUB · 3 LEVELS','Original geometry unchanged + compact in-floor sorter: four in-floor sliding-shoe diverts, four individual spiral descents, Floor-2 merge'],sorter:['R9 · COMPACT IN-FLOOR SORTER','4 independent chute-to-spiral transfers, four takeaways, Floor 02 merge'],exploded:['EXPLODED SPATIAL LAYERS','Elevations separated while preserving transport connectivity'],xray:['X-RAY EQUIPMENT OVERVIEW','Floors transparent · conveyors emphasized'],floor1:['FLOOR 01 · LOADING DOCKS','2 receiving dock doors · 3 trailer loading doors · incline feeding sortation'],floor2:['FLOOR 02 · SORTATION','Sorter induction accepts R6 powered return · original conveyors preserved'],floor3:['FLOOR 03 · OUTBOUND SORTING','3 outbound chutes + R6 return chute to Floor 02 (concept)']};
|
|
365
|
+
function setMode(mode){
|
|
366
|
+
if(!labelStrings[mode])return;viewMode=mode;
|
|
367
|
+
document.querySelectorAll('[data-mode]').forEach(b=>{const active=b.dataset.mode===mode;b.classList.toggle('active',active);b.setAttribute('aria-pressed',String(active))});
|
|
368
|
+
document.querySelectorAll('[data-floor]').forEach(b=>b.classList.toggle('active',mode==='floor'+b.dataset.floor));
|
|
369
|
+
const [title,subtitle]=labelStrings[mode];$('mode-description').textContent=title;$('mode-note').textContent=subtitle;
|
|
370
|
+
const offsetTarget=mode==='exploded'?{1:-4.0,2:0,3:4.0}:{1:0,2:0,3:0};
|
|
371
|
+
for(const f of [1,2,3])off[f]=offsetTarget[f];
|
|
372
|
+
for(const f of [1,2,3]){
|
|
373
|
+
floorGroups[f].position.y=off[f];
|
|
374
|
+
const visible=!mode.startsWith('floor')||mode==='floor'+f;
|
|
375
|
+
floorGroups[f].visible=visible;
|
|
376
|
+
floorGroups[f].userData.slab.material.opacity=mode==='xray'?.045:.40;
|
|
377
|
+
floorGroups[f].userData.grid.material.opacity=mode==='xray'?.12:.31;
|
|
378
|
+
}
|
|
379
|
+
for(const e of EQUIPMENT){
|
|
380
|
+
if(e.connector){
|
|
381
|
+
const visible=!mode.startsWith('floor')||e.floor===Number(mode.slice(-1))||e.toFloor===Number(mode.slice(-1));
|
|
382
|
+
deviceGroups[e.id].visible=visible;
|
|
383
|
+
}
|
|
384
|
+
deviceMaterials[e.id].opacity=mode==='xray'?.63:1;
|
|
385
|
+
}
|
|
386
|
+
// Rebuild only the two connectors after a mode switch so no stale endpoint remains.
|
|
387
|
+
for(const id of Object.keys(connectors))rebuildConnector(EQUIPMENT_BY_ID[id]);
|
|
388
|
+
const focusY=mode.startsWith('floor')?FLOORS[Number(mode.slice(-1))].elevation:6.5;
|
|
389
|
+
if(mode==='sorter'){
|
|
390
|
+
controls.target.set(-7,9.5,6);camera.position.set(8,23,18);
|
|
391
|
+
} else if(mode==='incline'){
|
|
392
|
+
controls.target.set(7,3.8,-2);camera.position.set(25,18,23);
|
|
393
|
+
} else if(mode==='docks'){
|
|
394
|
+
controls.target.set(2,1.2,0);camera.position.set(35,17,31);
|
|
395
|
+
} else {
|
|
396
|
+
controls.target.set(1,focusY,0);camera.position.set(mode.startsWith('floor')?36:45,focusY+(mode.startsWith('floor')?19:24),mode.startsWith('floor')?31:38);
|
|
397
|
+
}
|
|
398
|
+
controls.update();markSelected();record(`VIEW · ${title}`);
|
|
399
|
+
}
|
|
400
|
+
function removeGroupResources(g){g.traverse(o=>{
|
|
401
|
+
if(o.geometry && ![reusableRollerGeo,reusableLegGeo,pointGeo].includes(o.geometry))o.geometry.dispose();
|
|
402
|
+
if(o.material){const mats=Array.isArray(o.material)?o.material:[o.material];for(const mat of mats){if(mat.map)mat.map.dispose();if(![steelMaterial,rollerMaterial,legMaterial,peHousingMaterial,peReceiverMaterial,peMountMaterial,peOpticMaterial].includes(mat))mat.dispose();}}
|
|
403
|
+
});}
|
|
404
|
+
function rebuildConnector(e){
|
|
405
|
+
const old=deviceGroups[e.id];const prevState=stateFor(e.id);
|
|
406
|
+
// Keep the single renderer scene; do not create a second canvas or leak old geometries.
|
|
407
|
+
scene.remove(old);removeGroupResources(old);
|
|
408
|
+
const g=buildConveyor(e);
|
|
409
|
+
deviceMaterials[e.id].opacity=viewMode==='xray'?.63:1;
|
|
410
|
+
deviceLabels[e.id].visible=showLabels;
|
|
411
|
+
// Drop old entries from raycasting after rebuild to avoid interacting with stale geometry.
|
|
412
|
+
for(let i=raycastMeshes.length-1;i>=0;i--)if(raycastMeshes[i].userData.equipmentId===e.id&&!g.children.includes(raycastMeshes[i]))raycastMeshes.splice(i,1);
|
|
413
|
+
applyEquipmentVisual(e.id,prevState);
|
|
414
|
+
for(const key of ['PE_IN','PE_OUT']){
|
|
415
|
+
const value=currentSensorFeed?.equipment?.[e.id]?.[key];
|
|
416
|
+
colorSensor(e.id,key,typeof value==='boolean'?value:null);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
function applyEquipmentVisual(id,state){
|
|
420
|
+
const color=EQUIPMENT_BY_ID[id].demoOnly?COLORS.unknown:assetStateColor(state),mat=deviceMaterials[id];
|
|
421
|
+
mat.emissive.setHex(color);mat.emissiveIntensity=.29;
|
|
422
|
+
deviceIcons[id].material.color.setHex(color);
|
|
423
|
+
if(deviceIcons[id].userData.light){deviceIcons[id].userData.light.color.setHex(color);deviceIcons[id].userData.light.intensity=state.running===true?.55:.23;}
|
|
424
|
+
}
|
|
425
|
+
function markSelected(){
|
|
426
|
+
for(const e of EQUIPMENT){const active=e.id===selected;
|
|
427
|
+
deviceLabels[e.id].visible=showLabels && (!/^R9_(JOIN|COLLECT)_/.test(e.id) || active);
|
|
428
|
+
deviceGroups[e.id].userData.selectOutline.visible=active;
|
|
429
|
+
deviceMaterials[e.id].emissiveIntensity=active?.69:.29;
|
|
430
|
+
for(const k of ['PE_IN','PE_OUT'])if(peIndicators[e.id]?.[k])peIndicators[e.id][k].caption.visible=peLabelVisible(e.id);
|
|
431
|
+
}
|
|
432
|
+
refreshInspector();
|
|
433
|
+
}
|
|
434
|
+
function displayValue(v,formatTrue='TRUE',formatFalse='FALSE'){return v===null||v===undefined?'UNKNOWN':v?formatTrue:formatFalse}
|
|
435
|
+
function inspectorNeighbors(e){const a=e.up[0]||null,b=e.down[0]||null;
|
|
436
|
+
$('nav-upstream').disabled=!a;$('nav-downstream').disabled=!b;
|
|
437
|
+
$('nav-upstream').querySelector('strong').textContent=a||'—';$('nav-downstream').querySelector('strong').textContent=b||'—';
|
|
438
|
+
$('nav-upstream').dataset.asset=a||'';$('nav-downstream').dataset.asset=b||'';
|
|
439
|
+
$('asset-route').textContent=[a||'SOURCE',e.id,b||'DESTINATION'].join(' → ');
|
|
440
|
+
}
|
|
441
|
+
function refreshInspector(){
|
|
442
|
+
const e=EQUIPMENT_BY_ID[selected],s=stateFor(selected);if(!e)return;
|
|
443
|
+
$('asset-name').textContent=e.id;$('asset-category').textContent=e.category.toUpperCase();
|
|
444
|
+
const spec=$('transfer-details');
|
|
445
|
+
if(e.id==='INCL01'){
|
|
446
|
+
spec.hidden=false;
|
|
447
|
+
$('transfer-title').textContent='GRADED INTER-FLOOR INCLINE · CONCEPT';
|
|
448
|
+
$('transfer-grade').textContent=INCLINE_ENGINEERING_DEMO.averageAngle.toFixed(1)+'°';
|
|
449
|
+
$('transfer-rise').textContent=INCLINE_ENGINEERING_DEMO.rise.toFixed(1)+' scene units';
|
|
450
|
+
$('transfer-note').textContent='Conceptual curved modules · not a manufactured belt design.';
|
|
451
|
+
}else if(e.id==='SPIRAL01'||e.id.startsWith('DOWN_')||e.id==='R6_DOWN_F2'||e.id.startsWith('R9_SPIRAL_')){
|
|
452
|
+
spec.hidden=false;
|
|
453
|
+
const values=gradeStats(e.path);
|
|
454
|
+
$('transfer-title').textContent=(e.id==='R6_DOWN_F2'||e.id.startsWith('R9_SPIRAL_'))?'POWERED DECLINE TO FLOOR 02 · CONCEPT':e.id.startsWith('DOWN_')?'POWERED DECLINE TO GROUND DOCK · CONCEPT':'POWERED SPIRAL ASCENT · CONCEPT';
|
|
455
|
+
$('transfer-grade').textContent=values.maxSegmentAngle.toFixed(1)+'°';
|
|
456
|
+
$('transfer-rise').textContent=values.rise.toFixed(1)+' scene units';
|
|
457
|
+
$('transfer-note').textContent='Concept only: vendor checks for controls, guards, package stability and clearance needed.';
|
|
458
|
+
}else spec.hidden=true;
|
|
459
|
+
$('asset-location').textContent=e.connector?`Floor 0${e.floor} → Floor 0${e.toFloor} · Vertical transfer`:`Floor 0${e.floor} · ${FLOORS[e.floor].name}`;
|
|
460
|
+
if(e.demoOnly)$('asset-location').textContent+=' · CONCEPT ASSET';
|
|
461
|
+
const state=e.demoOnly?'CONCEPT':s.fault===true?'FAULT':s.blocked===true?'BLOCKED':s.running===true?'RUNNING':s.running===false?'STOPPED':'UNKNOWN';
|
|
462
|
+
$('asset-badge').textContent=state;$('asset-badge').className='run-badge'+(state==='FAULT'?' fault':(['UNKNOWN','STOPPED','CONCEPT'].includes(state)?' unknown':''));
|
|
463
|
+
$('asset-run').textContent=e.demoOnly?'—':displayValue(s.running);$('asset-speed').textContent=e.demoOnly?'—':s.speed===null?'UNKNOWN':s.speed.toFixed(2)+' m/s';
|
|
464
|
+
$('asset-fault').textContent=e.demoOnly?'—':s.fault===true?'ACTIVE':s.fault===false?'NONE':'UNKNOWN';
|
|
465
|
+
const sn=currentSensorFeed?.equipment?.[e.id];
|
|
466
|
+
for(const [key,virtualKey,original] of [['asset-pe-in','PE_IN',s.peIn],['asset-pe-out','PE_OUT',s.peOut]]){
|
|
467
|
+
const val=(e.id.startsWith('R6_')||e.id.startsWith('R9_'))&&typeof sn?.[virtualKey]==='boolean'?sn[virtualKey]:original;
|
|
468
|
+
const el=$(key);el.textContent=displayValue(val,'BLOCKED','CLEAR');el.dataset.signal=val===true?'blocked':val===false?'clear':'unknown';
|
|
469
|
+
}$('asset-pe-in-count').textContent=sn?String(sn.IN_COUNT):'—';$('asset-pe-out-count').textContent=sn?String(sn.OUT_COUNT):'—';
|
|
470
|
+
$('asset-quality').textContent=s.quality;$('quality-note').textContent=currentSensorFeed?(e.demoOnly?'V1J simulated concept sensor, not a real PLC input.':'V1J virtual photoeye; driven by parcel-position model.'):(e.demoOnly?'Concept only; no verified PLC I/O.':live?'Read-only V1C mapping.':'Illustrative simulation.');
|
|
471
|
+
inspectorNeighbors(e);
|
|
472
|
+
}
|
|
473
|
+
function selectAsset(id,focus=false){if(!EQUIPMENT_BY_ID[id])return;
|
|
474
|
+
const e=EQUIPMENT_BY_ID[id];
|
|
475
|
+
if(viewMode.startsWith('floor')&&viewMode!=='floor'+e.floor)setMode('stacked');
|
|
476
|
+
selected=id;markSelected();record(`INSPECT · ${id} · F0${e.floor}`);
|
|
477
|
+
if(focus){const center=paths[id].getPointAt(.5);if(!e.connector)center.y+=off[e.floor];
|
|
478
|
+
controls.target.copy(center);const dir=new THREE.Vector3(1.3,.9,1.2).normalize();camera.position.copy(center).addScaledVector(dir,e.connector?13:9);controls.update();
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
function sampleAsset(id,t){const e=EQUIPMENT_BY_ID[id],curve=paths[id];if(!curve)return null;const p=curve.getPointAt(Math.max(0,Math.min(1,t)));
|
|
482
|
+
if(!e.connector)p.y+=off[e.floor];return p;
|
|
483
|
+
}
|
|
484
|
+
// Align the parcel bottom with the actual uphill conveyor tangent.
|
|
485
|
+
// The previous V1D yaw-only rotation made boxes slide upright through slopes.
|
|
486
|
+
const BOX_X=new THREE.Vector3(1,0,0);
|
|
487
|
+
const parcelTransformMat=new THREE.Matrix4();
|
|
488
|
+
function placeParcel(mesh,id,progress){
|
|
489
|
+
const p=sampleAsset(id,progress);if(!p)return false;
|
|
490
|
+
const tangent=paths[id].getTangentAt(Math.max(0,Math.min(1,progress))).normalize();
|
|
491
|
+
const across=new THREE.Vector3(-tangent.z,0,tangent.x).normalize();
|
|
492
|
+
if(across.lengthSq()<.001)across.set(0,0,1);
|
|
493
|
+
const surfaceUp=new THREE.Vector3().crossVectors(across,tangent).normalize();
|
|
494
|
+
parcelTransformMat.makeBasis(tangent,surfaceUp,across);
|
|
495
|
+
mesh.quaternion.setFromRotationMatrix(parcelTransformMat);
|
|
496
|
+
mesh.position.copy(p).addScaledVector(surfaceUp,.265);
|
|
497
|
+
return true;
|
|
498
|
+
}
|
|
499
|
+
function updateParcels(dt){
|
|
500
|
+
if(v1cOffline){
|
|
501
|
+
for(const p of parcels)p.object.visible=false;
|
|
502
|
+
for(const p of liveParcels)p.object.visible=false;
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
if(live){
|
|
506
|
+
for(const p of parcels)p.object.visible=false;
|
|
507
|
+
for(let i=0;i<liveParcels.length;i++){
|
|
508
|
+
const packet=liveParcelState.positions[i],mesh=liveParcels[i].object;
|
|
509
|
+
mesh.visible=!!packet;
|
|
510
|
+
if(packet){if(!placeParcel(mesh,packet.equipmentId,packet.progress))mesh.visible=false;}
|
|
511
|
+
}
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
for(const p of liveParcels)p.object.visible=false;
|
|
515
|
+
const lab=window.devagentLabState;
|
|
516
|
+
if(lab?.enabled && currentSensorFeed){
|
|
517
|
+
for(const p of parcels)p.object.visible=false;
|
|
518
|
+
const positions=currentSensorFeed.parcels||[];ensureLiveParcels(positions.length);
|
|
519
|
+
const elapsed=Math.min(.28,(performance.now()-lastSensorReceivedAt)/1000);
|
|
520
|
+
for(let i=0;i<liveParcels.length;i++){
|
|
521
|
+
const packet=positions[i],mesh=liveParcels[i].object;
|
|
522
|
+
if(!packet){mesh.visible=false;continue}
|
|
523
|
+
let id=packet.equipmentId,t=packet.progress;
|
|
524
|
+
if(lab.run_state==='RUNNING' && flowLengths[id]){
|
|
525
|
+
// Client interpolation is capped between authoritative snapshots; never generates sensor events.
|
|
526
|
+
let delta=elapsed*2.05;
|
|
527
|
+
while(delta>0 && id){
|
|
528
|
+
const available=(1-t)*flowLengths[id];
|
|
529
|
+
if(delta<=available){t+=delta/flowLengths[id];break}
|
|
530
|
+
// Follow the server-selected next hop: the sorter has multiple exits.
|
|
531
|
+
const options=EQUIPMENT_BY_ID[id]?.down||[];
|
|
532
|
+
const next=(typeof packet.nextEquipmentId==='string'&&EQUIPMENT_BY_ID[packet.nextEquipmentId])
|
|
533
|
+
?packet.nextEquipmentId:(options.length===1?options[0]:null);
|
|
534
|
+
delta-=available;id=next;
|
|
535
|
+
t=id&&flowLengths[id]?Math.min(1,delta/flowLengths[id]):0;break;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
mesh.visible=!!id && placeParcel(mesh,id,t);
|
|
539
|
+
}
|
|
540
|
+
$('metric-parcels').textContent=String(currentSensorFeed.parcels_in_transit);
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
const advancing=!lab?.enabled||lab.run_state==='RUNNING';
|
|
544
|
+
for(const p of parcels){
|
|
545
|
+
p.object.visible=true;
|
|
546
|
+
if(advancing)p.progress+=dt*2.05;
|
|
547
|
+
const lens=p.route.map(id=>flowLengths[id]);const total=lens.reduce((a,b)=>a+b,0);
|
|
548
|
+
if(p.progress>=total){p.progress-=total+18;delivered++;$('activity').textContent=`SIMULATION · P-${String(p.i+1).padStart(3,'0')} ${p.origin} → ${p.destination} DELIVERED · ${delivered} demo deliveries`;}
|
|
549
|
+
let pos=p.progress;if(pos<0){p.object.visible=false;continue}
|
|
550
|
+
for(let i=0;i<p.route.length;i++){
|
|
551
|
+
if(pos<=lens[i]||i===lens.length-1){const id=p.route[i],t=Math.min(1,pos/lens[i]);placeParcel(p.object,id,t);
|
|
552
|
+
break;
|
|
553
|
+
}pos-=lens[i];
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
$('metric-parcels').textContent=parcels.filter(p=>p.object.visible).length;
|
|
557
|
+
}
|
|
558
|
+
function record(message){$('activity').textContent=message;}
|
|
559
|
+
function refreshMetrics(){
|
|
560
|
+
if(live){const s=summarizeState(liveSnapshot),unmatched=BASE_V1C_IDS.length-s.matched;
|
|
561
|
+
$('metric-online').textContent=s.online;$('online-caption').textContent=`${s.matched} V1C mapped · ${unmatched} unmapped · 12 concept nodes`;
|
|
562
|
+
$('metric-faults').textContent=s.faults;$('fault-caption').textContent=`From ${s.matched} mapped devices`;
|
|
563
|
+
$('metric-parcels').textContent=liveParcelState.count===null?'—':String(liveParcelState.count);$('parcel-unit').textContent=liveParcelState.count===null?'unverified':'reported';$('parcel-caption').textContent=liveParcelState.positions.length?`${liveParcelState.positions.length} parcels spatially mapped`:'Parcel positions unavailable';
|
|
564
|
+
$('status-summary').textContent=`V1C read-only: ${s.matched}/${BASE_V1C_IDS.length} original tags · 12 visualization-only nodes`;
|
|
565
|
+
}else if(v1cOffline){
|
|
566
|
+
$('metric-online').textContent='—';$('online-caption').textContent='V1C FEED UNAVAILABLE · statuses unknown';
|
|
567
|
+
$('metric-faults').textContent='—';$('fault-caption').textContent='Cannot infer alarm state';
|
|
568
|
+
$('metric-parcels').textContent='—';$('parcel-unit').textContent='unavailable';$('parcel-caption').textContent='Not animating demo parcels while source is offline';
|
|
569
|
+
$('status-summary').textContent='V1C OFFLINE · all original equipment signals unknown · concept assets unmapped';
|
|
570
|
+
}else{
|
|
571
|
+
const lab=window.devagentLabState;
|
|
572
|
+
$('metric-online').textContent=String(lab?.enabled?(lab.run_state==='RUNNING'?BASE_V1C_IDS.length:0):BASE_V1C_IDS.length);
|
|
573
|
+
$('online-caption').textContent=lab?.enabled?`ISOLATED ${lab.run_state} · 22 simulated assets`:'22 original simulated · 15 conceptual';
|
|
574
|
+
$('metric-faults').textContent=String(lab?.enabled&&lab.fault?1:0);
|
|
575
|
+
$('fault-caption').textContent=lab?.fault?'SIMULATED JAM · CV208':'No simulated exceptions';
|
|
576
|
+
$('parcel-unit').textContent='simulated';$('parcel-caption').textContent=currentSensorFeed?'Server-tracked parcel positions · virtual PE':'Animated demonstration flow';
|
|
577
|
+
$('status-summary').textContent='22 original V1C assets + 15 concept assets · 2 inbound / 3 outbound docks · DEMO';
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
function markSource(mode,message){
|
|
581
|
+
window.devagentSourceMode=mode==='V1C MAPPED'?'V1C':mode==='V1C OFFLINE'?'OFFLINE':'SIMULATION';
|
|
582
|
+
window.dispatchEvent(new CustomEvent('devagent:source',{detail:window.devagentSourceMode}));
|
|
583
|
+
$('data-source').textContent=mode;
|
|
584
|
+
$('source-caption').textContent=message;
|
|
585
|
+
$('source-led').style.background=mode==='V1C MAPPED'?'#38d7bc':mode==='V1C OFFLINE'?'#f07281':'#e3b35d';
|
|
586
|
+
$('viewport-mode').textContent=mode==='V1C MAPPED'?'LIVE DEVICE SIGNALS / PARCELS DISABLED':mode==='V1C OFFLINE'?'V1C FEED LOST / STATUS UNKNOWN':'SIMULATED / READ ONLY';
|
|
587
|
+
$('btn-connect').textContent=mode==='V1C MAPPED'?'✓ V1C CONNECTED':mode==='V1C OFFLINE'?'↻ RETRY V1C':'↗ CONNECT V1C';
|
|
588
|
+
$('btn-demo-wave').textContent=mode==='V1C OFFLINE'?'▶ SWITCH TO DEMO':'▶ RESTART DEMO FLOW';
|
|
589
|
+
}
|
|
590
|
+
async function connectV1C(){
|
|
591
|
+
if(connectionBusy)return;connectionBusy=true;$('btn-connect').disabled=true;
|
|
592
|
+
try{
|
|
593
|
+
const r=await fetch('/api/bridge/facility/state',{cache:'no-store',signal:AbortSignal.timeout(3500)});
|
|
594
|
+
if(!r.ok)throw Error('API HTTP '+r.status);
|
|
595
|
+
const data=await r.json(),mapped=extractEquipment(data);
|
|
596
|
+
const count=Object.keys(mapped).length;
|
|
597
|
+
if(!count)throw Error('No recognizable V1C equipment signals in V1C response; exact mapping required.');
|
|
598
|
+
// Fail closed: unknown equipment and unknown fields remain unknown, never synthesized.
|
|
599
|
+
live=true;v1cOffline=false;liveSnapshot=mapped;liveParcelState=extractParcelState(data);ensureLiveParcels(liveParcelState.positions.length);lastLiveAt=Date.now();
|
|
600
|
+
for(const e of EQUIPMENT){equipState[e.id]=e.demoOnly?{running:null,blocked:null,fault:null,speed:null,peIn:null,peOut:null,quality:'CONCEPT'}:mapped[e.id]||{running:null,blocked:null,fault:null,speed:null,peIn:null,peOut:null,quality:'NOT MAPPED'};applyEquipmentVisual(e.id,equipState[e.id]);}
|
|
601
|
+
markSource('V1C MAPPED',`${count}/${BASE_V1C_IDS.length} original V1C assets recognized`);
|
|
602
|
+
refreshMetrics();markSelected();record(`READ-ONLY V1C · ${count}/22 assets mapped · ${liveParcelState.positions.length} parcels with positions`);
|
|
603
|
+
}catch(err){
|
|
604
|
+
// Industrial fail-closed behavior: a missing feed NEVER turns every belt green.
|
|
605
|
+
live=false;v1cOffline=true;liveSnapshot={};liveParcelState={count:null,positions:[]};
|
|
606
|
+
for(const e of EQUIPMENT){equipState[e.id]={running:null,blocked:null,fault:null,speed:null,peIn:null,peOut:null,quality:e.demoOnly?'CONCEPT':'OFFLINE'};applyEquipmentVisual(e.id,equipState[e.id]);}
|
|
607
|
+
markSource('V1C OFFLINE','Source unreachable / unmapped; all equipment statuses unknown');
|
|
608
|
+
refreshMetrics();markSelected();record(`V1C READ-ONLY FEED UNAVAILABLE · ${err.message}`);
|
|
609
|
+
}finally{$('btn-connect').disabled=false;connectionBusy=false;}
|
|
610
|
+
}
|
|
611
|
+
function disconnectToDemo(reason='Demo resumed'){live=false;v1cOffline=false;liveSnapshot={};liveParcelState={count:null,positions:[]};
|
|
612
|
+
for(const e of EQUIPMENT){equipState[e.id]=defaultState(e);applyEquipmentVisual(e.id,equipState[e.id]);}
|
|
613
|
+
lastLabRevision=-1;
|
|
614
|
+
markSource('SIMULATION','Demo until verified mapping');refreshMetrics();markSelected();record(reason);
|
|
615
|
+
}
|
|
616
|
+
function restartDemo(){if(window.devagentLabState?.enabled){record('LAB CONTROLS ENABLED · use the RESET operator command, not visual restart.');return}
|
|
617
|
+
if(live){record('Read-only live mode: cannot change upstream simulator or inject parcels.');return}
|
|
618
|
+
if(v1cOffline)disconnectToDemo('Operator explicitly switched to illustrative simulation.');
|
|
619
|
+
delivered=0;for(let i=0;i<parcels.length;i++)parcels[i].progress=i*12.7;record('DEMO · Parcel wave restarted · does not write to OPC UA.');}
|
|
620
|
+
|
|
621
|
+
// Isolated lab commands are *not* written upstream. In simulation mode they
|
|
622
|
+
// animate the same model and demonstrate stop/jam/ack/reset transitions.
|
|
623
|
+
let lastLabWave=-1,lastLabRevision=-1;
|
|
624
|
+
function colorSensor(eid,key,blocked){
|
|
625
|
+
const icon=peIndicators[eid]?.[key];if(!icon)return;
|
|
626
|
+
const col=blocked===null?COLORS.unknown:blocked===true?COLORS.blocked:COLORS.on;
|
|
627
|
+
// Keep the enclosure a dark, realistic shape. Only the tiny LED changes state.
|
|
628
|
+
icon.lens.material.color.setHex(col);icon.lens.material.emissive.setHex(col);
|
|
629
|
+
icon.beam.material.color.setHex(col);
|
|
630
|
+
icon.beam.visible=blocked===true;
|
|
631
|
+
icon.beam.material.opacity=blocked===true?.46:0;
|
|
632
|
+
icon.caption.visible=peLabelVisible(eid);
|
|
633
|
+
}
|
|
634
|
+
function clearSensorFeed(){
|
|
635
|
+
currentSensorFeed=null;
|
|
636
|
+
for(const e of EQUIPMENT){
|
|
637
|
+
for(const key of ['PE_IN','PE_OUT'])colorSensor(e.id,key,null);
|
|
638
|
+
if(!live&&!v1cOffline){const st=stateFor(e.id);st.peIn=null;st.peOut=null;st.quality='SENSOR FEED OFFLINE';}
|
|
639
|
+
}
|
|
640
|
+
$('sensor-summary').textContent='PE SIGNALS UNAVAILABLE · CHECK ISOLATED LAB CONNECTION';
|
|
641
|
+
if(!live&&!v1cOffline)refreshInspector();
|
|
642
|
+
}
|
|
643
|
+
function applySensorFeed(feed){
|
|
644
|
+
if(!feed||feed.sensor_quality!=='ISOLATED_SIMULATOR'){clearSensorFeed();return}
|
|
645
|
+
currentSensorFeed=feed;lastSensorReceivedAt=performance.now();
|
|
646
|
+
for(const e of EQUIPMENT){
|
|
647
|
+
const sig=feed.equipment?.[e.id];
|
|
648
|
+
for(const key of ['PE_IN','PE_OUT'])colorSensor(e.id,key,typeof sig?.[key]==='boolean'?sig[key]:null);
|
|
649
|
+
if(!live&&!v1cOffline){
|
|
650
|
+
const prior=stateFor(e.id);
|
|
651
|
+
prior.peIn=typeof sig?.PE_IN==='boolean'?sig.PE_IN:null;
|
|
652
|
+
prior.peOut=typeof sig?.PE_OUT==='boolean'?sig.PE_OUT:null;
|
|
653
|
+
prior.quality=sig?(e.demoOnly?'VIRTUAL CONCEPT':'VIRTUAL PE'):'SENSOR NODE MISSING';
|
|
654
|
+
equipState[e.id]=prior;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
const dws=feed.scanners?.DWS01, dest=feed.scanners?.DEST_SCAN;
|
|
658
|
+
if(dws){$('scan-dws-count').textContent=String(dws.ScanCount);$('scan-dws-result').textContent=dws.LastResult;}
|
|
659
|
+
if(dest){$('scan-dest-count').textContent=String(dest.ScanCount);$('scan-dest-result').textContent=dest.LastResult;}
|
|
660
|
+
for(const [sid,scan] of Object.entries(feed.scanners||{})){
|
|
661
|
+
const icon=scannerIndicators[sid];if(!icon)continue;
|
|
662
|
+
const color=scan.LastResult==='NO_READ'?COLORS.blocked:scan.LastResult==='GOOD_READ'?COLORS.on:COLORS.unknown;
|
|
663
|
+
icon.lens.material.color.setHex(color);icon.lens.material.emissive.setHex(color);
|
|
664
|
+
if(icon.lastRevision!==scan.Revision){icon.lastRevision=scan.Revision;icon.lens.scale.setScalar(1.55);}
|
|
665
|
+
}
|
|
666
|
+
$('scan-last-id').textContent=(dest?.LastParcelId||dws?.LastParcelId||'—');
|
|
667
|
+
$('scan-no-read').textContent=String((dws?.NoReadCount||0)+(dest?.NoReadCount||0));
|
|
668
|
+
$('sensor-summary').textContent=`PE BLOCKED ${feed.blocked_sensors} / ${EQUIPMENT.length*2} · ${feed.parcels_in_transit} LIVE PARCELS · ${feed.delivered} DELIVERED`;
|
|
669
|
+
if(feed.sensor_events?.length){const last=feed.sensor_events.at(-1);
|
|
670
|
+
if(last.sequence!==lastSensorSequence){lastSensorSequence=last.sequence;
|
|
671
|
+
$('sensor-event').textContent=`${last.kind} · ${last.equipmentId} · ${last.parcelId}`;}
|
|
672
|
+
}
|
|
673
|
+
if(!live&&!v1cOffline)markSelected();
|
|
674
|
+
}
|
|
675
|
+
window.addEventListener('devagent:lab-state',({detail:s})=>{
|
|
676
|
+
if(s.enabled)applySensorFeed(s.sensor_data);
|
|
677
|
+
if(!s.enabled||s.revision===lastLabRevision)return;
|
|
678
|
+
lastLabRevision=s.revision;
|
|
679
|
+
if(s.wave!==lastLabWave){
|
|
680
|
+
if(lastLabWave!==-1){delivered=0;parcels.forEach((p,i)=>p.progress=i*12.7);}
|
|
681
|
+
lastLabWave=s.wave;
|
|
682
|
+
}
|
|
683
|
+
if(live||v1cOffline)return;
|
|
684
|
+
for(const e of EQUIPMENT){
|
|
685
|
+
const jam=s.fault&&e.id==='CV208';
|
|
686
|
+
equipState[e.id]={running:s.run_state==='RUNNING',blocked:!!jam,fault:!!jam,
|
|
687
|
+
speed:s.run_state==='RUNNING'?1.2:0,peIn:currentSensorFeed?.equipment?.[e.id]?.PE_IN??null,peOut:currentSensorFeed?.equipment?.[e.id]?.PE_OUT??null,quality:e.demoOnly?'VIRTUAL CONCEPT':'VIRTUAL PE'};
|
|
688
|
+
applyEquipmentVisual(e.id,equipState[e.id]);
|
|
689
|
+
}
|
|
690
|
+
refreshMetrics();markSelected();
|
|
691
|
+
record(`LAB ${s.run_state} · rev ${s.revision} · ${s.fault||'no alarm'} · no V1C/OPC UA writes`);
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
let down=null;
|
|
695
|
+
renderer.domElement.addEventListener('pointerdown',e=>{down={x:e.clientX,y:e.clientY,time:performance.now()}});
|
|
696
|
+
renderer.domElement.addEventListener('pointerup',e=>{
|
|
697
|
+
if(!down||Math.hypot(e.clientX-down.x,e.clientY-down.y)>5){down=null;return}down=null;
|
|
698
|
+
const rect=renderer.domElement.getBoundingClientRect(),mouse=new THREE.Vector2((e.clientX-rect.left)/rect.width*2-1,-(e.clientY-rect.top)/rect.height*2+1);
|
|
699
|
+
const ray=new THREE.Raycaster();ray.setFromCamera(mouse,camera);const hits=ray.intersectObjects(raycastMeshes,false).filter(hit=>{const id=hit.object.userData.equipmentId;const asset=EQUIPMENT_BY_ID[id];return asset && deviceGroups[id].visible && (asset.connector || floorGroups[asset.floor].visible);});
|
|
700
|
+
if(hits.length){const id=hits[0].object.userData.equipmentId;if(id)selectAsset(id,e.detail===2)}
|
|
701
|
+
});
|
|
702
|
+
renderer.domElement.addEventListener('dblclick',e=>e.preventDefault());
|
|
703
|
+
document.querySelectorAll('[data-mode]').forEach(btn=>btn.addEventListener('click',()=>setMode(btn.dataset.mode)));
|
|
704
|
+
document.querySelectorAll('[data-floor]').forEach(btn=>btn.addEventListener('click',()=>setMode('floor'+btn.dataset.floor)));
|
|
705
|
+
$('reset-view').addEventListener('click',()=>setMode('stacked'));
|
|
706
|
+
$('rail-overview').addEventListener('click',()=>setMode('stacked'));
|
|
707
|
+
$('rail-faults').addEventListener('click',()=>{const bad=EQUIPMENT.find(e=>stateFor(e.id).fault===true);if(bad)selectAsset(bad.id,true);else record('FAULT FIND · No mapped fault found');});
|
|
708
|
+
$('toggle-sensors').addEventListener('click',()=>{showAllPE=!showAllPE;$('toggle-sensors').setAttribute('aria-pressed',String(showAllPE));markSelected();});
|
|
709
|
+
$('toggle-labels').addEventListener('click',()=>{showLabels=!showLabels;markSelected();$('toggle-labels').setAttribute('aria-pressed',String(showLabels));});
|
|
710
|
+
$('btn-demo-wave').addEventListener('click',restartDemo);
|
|
711
|
+
$('btn-connect').addEventListener('click',()=>live?disconnectToDemo('Operator returned to local demonstration.'):connectV1C());
|
|
712
|
+
$('nav-upstream').addEventListener('click',()=>selectAsset($('nav-upstream').dataset.asset,true));
|
|
713
|
+
$('nav-downstream').addEventListener('click',()=>selectAsset($('nav-downstream').dataset.asset,true));
|
|
714
|
+
function doSearch(){const id=$('asset-search').value.trim().toUpperCase();if(EQUIPMENT_BY_ID[id]){$('search-error').textContent='';selectAsset(id,true)}else{$('search-error').textContent='Asset ID not found in V1D demonstration topology.';}}
|
|
715
|
+
$('search-go').addEventListener('click',doSearch);$('asset-search').addEventListener('keydown',e=>{if(e.key==='Enter')doSearch()});
|
|
716
|
+
window.addEventListener('keydown',e=>{if(['INPUT','TEXTAREA'].includes(document.activeElement?.tagName))return;
|
|
717
|
+
if(['1','2','3'].includes(e.key))setMode('floor'+e.key);
|
|
718
|
+
if(e.key.toLowerCase()==='e')setMode('exploded');
|
|
719
|
+
if(e.key.toLowerCase()==='x')setMode('xray');
|
|
720
|
+
if(e.key.toLowerCase()==='a')setMode('stacked');
|
|
721
|
+
if(e.key.toLowerCase()==='i')setMode('incline');
|
|
722
|
+
if(e.key.toLowerCase()==='d')setMode('docks');
|
|
723
|
+
if(e.key.toLowerCase()==='s')setMode('sorter');
|
|
724
|
+
});
|
|
725
|
+
function resize(){const w=mount.clientWidth,h=mount.clientHeight;if(!w||!h)return;camera.aspect=w/h;camera.updateProjectionMatrix();renderer.setSize(w,h,false);}
|
|
726
|
+
new ResizeObserver(resize).observe(mount);resize();
|
|
727
|
+
let prev=performance.now(),fpsCount=0,fpsStart=prev;
|
|
728
|
+
function tick(now){requestAnimationFrame(tick);const dt=Math.min(.06,(now-prev)/1000);prev=now;
|
|
729
|
+
controls.update();updateParcels(dt);if(currentSensorFeed&&performance.now()-lastSensorReceivedAt>4000)clearSensorFeed();for(const sid of Object.keys(scannerIndicators)){const lens=scannerIndicators[sid].lens; if(lens.scale.x>1)lens.scale.setScalar(Math.max(1,lens.scale.x-dt*2));}
|
|
730
|
+
for(const e of EQUIPMENT)for(const k of ['PE_IN','PE_OUT']){const cap=peIndicators[e.id]?.[k]?.caption;if(cap)cap.visible=peLabelVisible(e.id);}
|
|
731
|
+
renderer.render(scene,camera);
|
|
732
|
+
fpsCount++;if(now-fpsStart>1500){$('fps-indicator').textContent=`WEBGL · ${Math.round(fpsCount*1000/(now-fpsStart))} FPS`;fpsStart=now;fpsCount=0;}
|
|
733
|
+
}
|
|
734
|
+
for(const e of EQUIPMENT)applyEquipmentVisual(e.id,stateFor(e.id));markSelected();refreshMetrics();
|
|
735
|
+
$('scene-loading').classList.add('hidden');window.devagentEngineLoaded?.();$('render-status').textContent='WEBGL ONLINE';
|
|
736
|
+
markSource('SIMULATION','Demo until verified mapping');record('R9 PREVIEW · four in-floor outlets, four individual spiral descents, Floor-2 merge · CONCEPT ONLY');
|
|
737
|
+
requestAnimationFrame(tick);
|
|
738
|
+
// Embedding uses same-origin requests to the read-only server; no browser OPC UA
|
|
739
|
+
// write capability and no credentials cross the frame boundary.
|
|
740
|
+
const URL_FLAGS=new URLSearchParams(window.location.search);
|
|
741
|
+
if(URL_FLAGS.get('embed')==='1')document.documentElement.classList.add('ignition-embedded');
|
|
742
|
+
if(URL_FLAGS.get('native')==='1')document.documentElement.classList.add('native-operator');
|
|
743
|
+
if(URL_FLAGS.get('autoconnect')==='1' && URL_FLAGS.get('native')!=='1')setTimeout(connectV1C,300);
|
|
744
|
+
// A mapped feed is checked periodically; it never silently becomes a claimed live feed.
|
|
745
|
+
setInterval(()=>{if((live||v1cOffline)&&!connectionBusy)connectV1C()},2500);
|