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,442 @@
|
|
|
1
|
+
/** DevAgent Spatial Operations — R9-inspired light-and-material rendering, concept only.
|
|
2
|
+
* SPATIAL_LIT_R042: no fake operational state, no live OPC UA, no control writes.
|
|
3
|
+
* Supports local THREE r180 only. Equipment footprint/topology are customer supplied.
|
|
4
|
+
*/
|
|
5
|
+
import {planVisualAssembly,planSensorMounts,commonFacilityBounds,sorterBranchPoints} from './flow_geometry.mjs';
|
|
6
|
+
import {buildR9Transport} from './r9_transport.mjs';
|
|
7
|
+
export async function startThree(model,host,onSelect){
|
|
8
|
+
const T=await import('./vendor/three.module.js');
|
|
9
|
+
if(T.REVISION!=='180')throw new Error('UNVERIFIED_THREE_REVISION');
|
|
10
|
+
const palettes={
|
|
11
|
+
r9_cyan:{bg:'#071827',accent:0x58c1dc,edge:0x4b9aab,fill:0x5cbadf},
|
|
12
|
+
steel_blue:{bg:'#0a1727',accent:0x85c6ff,edge:0x6289be,fill:0x7aaadd},
|
|
13
|
+
teal:{bg:'#071c23',accent:0x75dfce,edge:0x4da99f,fill:0x69cbb8},
|
|
14
|
+
warm_amber:{bg:'#191c21',accent:0xf5c77b,edge:0xb69455,fill:0xb8a07a},
|
|
15
|
+
};
|
|
16
|
+
const palette=palettes[model.palette]||palettes.r9_cyan;
|
|
17
|
+
const scene=new T.Scene();scene.background=null;scene.fog=new T.FogExp2(new T.Color(palette.bg),.0068);
|
|
18
|
+
const camera=new T.PerspectiveCamera(40,1,.1,1400);
|
|
19
|
+
let az=.88,el=.54,dist=36,target=new T.Vector3(0,1,0);
|
|
20
|
+
const renderer=new T.WebGLRenderer({antialias:true,alpha:true,powerPreference:'high-performance'});
|
|
21
|
+
renderer.setClearColor(0x000000,0); // R9 transparency lets stage atmosphere show through
|
|
22
|
+
renderer.setPixelRatio(Math.min(window.devicePixelRatio||1,1.75));
|
|
23
|
+
renderer.outputColorSpace=T.SRGBColorSpace;
|
|
24
|
+
renderer.toneMapping=T.ACESFilmicToneMapping;renderer.toneMappingExposure=1.28;
|
|
25
|
+
renderer.shadowMap.enabled=true;renderer.shadowMap.type=T.PCFSoftShadowMap;
|
|
26
|
+
host.append(renderer.domElement);renderer.domElement.style.cursor='grab';
|
|
27
|
+
scene.add(new T.HemisphereLight(0xc6eaff,0x182733,2.12));
|
|
28
|
+
scene.add(new T.AmbientLight(0x3a718d,.26));
|
|
29
|
+
const key=new T.DirectionalLight(0xd9f5ff,2.80);key.position.set(-20,32,23);key.castShadow=true;
|
|
30
|
+
key.shadow.mapSize.set(2048,2048);key.shadow.bias=-0.00012;key.shadow.normalBias=.016;
|
|
31
|
+
key.shadow.camera.near=.5;key.shadow.camera.far=260;key.shadow.camera.left=-85;key.shadow.camera.right=85;
|
|
32
|
+
key.shadow.camera.top=85;key.shadow.camera.bottom=-85;key.shadow.radius=3;
|
|
33
|
+
scene.add(key);scene.add(key.target);
|
|
34
|
+
const fill=new T.DirectionalLight(palette.fill,1.30);fill.position.set(18,14,-18);scene.add(fill);
|
|
35
|
+
const rim=new T.DirectionalLight(0x3cc0c0,1.05);rim.position.set(-10,10,-22);scene.add(rim);
|
|
36
|
+
const warm=new T.PointLight(0xf1d294,28,65);warm.position.set(7,18,-7);scene.add(warm); // R9 key light; decorative only
|
|
37
|
+
// Conceptual industrial materials, not operating-status colors.
|
|
38
|
+
const col={conveyor:0x467e94,spiral:0x4c92a5,sorter:0x4a91a3,motor:0x6a879d,vfd:0x53748e,photoeye:0x43b7ad,scanner:0xbdaa6f,tank:0x668c9e,pump:0x6e91ab,valve:0xcaa86a,robot:0x82acb7,junction:0x467e94,room:0x365e78,other:0x628ca0};
|
|
39
|
+
const mats={
|
|
40
|
+
frame:new T.MeshStandardMaterial({color:0x627e8d,metalness:.63,roughness:.41}),
|
|
41
|
+
trim:new T.MeshStandardMaterial({color:0xa5c1ca,metalness:.68,roughness:.32}),
|
|
42
|
+
deck:new T.MeshStandardMaterial({color:0x253f50,metalness:.42,roughness:.59}),
|
|
43
|
+
belt:new T.MeshStandardMaterial({color:0x223c4b,metalness:.16,roughness:.76}),
|
|
44
|
+
roller:new T.MeshStandardMaterial({color:0x759eb0,metalness:.67,roughness:.31}),
|
|
45
|
+
dark:new T.MeshStandardMaterial({color:0x1a3547,metalness:.38,roughness:.66}),
|
|
46
|
+
cyan:new T.MeshStandardMaterial({color:palette.accent,metalness:.35,roughness:.45,emissive:0x0d3241,emissiveIntensity:.32}),
|
|
47
|
+
highlight:new T.MeshStandardMaterial({color:0xd6b66e,metalness:.54,roughness:.42}),
|
|
48
|
+
};
|
|
49
|
+
const mat=(c,metal=.48,rough=.48,opacity=1)=>new T.MeshStandardMaterial({color:c,metalness:metal,roughness:rough,transparent:opacity<1,opacity,side:T.DoubleSide});
|
|
50
|
+
const assets=new Map(),pickables=[],floors=new Map();
|
|
51
|
+
const boxGeometry=new T.BoxGeometry(1,1,1);
|
|
52
|
+
function box(g,x,y,z,dx,dy,dz,m){const o=new T.Mesh(boxGeometry,m);o.position.set(x,y,z);o.scale.set(Math.max(.03,dx),Math.max(.03,dy),Math.max(.03,dz));o.castShadow=true;o.receiveShadow=true;g.add(o);return o;}
|
|
53
|
+
function cylinder(g,x,y,z,rad,height,m,segments=20){const o=new T.Mesh(new T.CylinderGeometry(rad,rad,height,segments),m);o.position.set(x,y,z);o.castShadow=true;o.receiveShadow=true;g.add(o);return o;}
|
|
54
|
+
function rail(g,sx,y,z){box(g,0,y,z,sx,.12,.10,mats.trim);box(g,0,y+.12,z,sx,.065,.08,mats.frame);}
|
|
55
|
+
function label(text){const cv=document.createElement('canvas');cv.width=384;cv.height=90;const ctx=cv.getContext('2d');ctx.fillStyle='rgba(7,24,37,.94)';ctx.fillRect(2,4,380,82);ctx.strokeStyle='#7494a4';ctx.lineWidth=2;ctx.strokeRect(2,4,380,82);ctx.fillStyle='#e1f7ff';ctx.font='bold 37px system-ui';ctx.textAlign='center';ctx.textBaseline='middle';ctx.fillText(text.slice(0,20),192,46);const tex=new T.CanvasTexture(cv);tex.colorSpace=T.SRGBColorSpace;const sprite=new T.Sprite(new T.SpriteMaterial({map:tex,transparent:true,depthWrite:false}));sprite.scale.set(3.0,.72,1);return sprite;}
|
|
56
|
+
function line(a,b,color=palette.edge){const geom=new T.BufferGeometry().setFromPoints([a,b]);const obj=new T.Line(geom,new T.LineDashedMaterial({color,dashSize:.26,gapSize:.16,opacity:.74,transparent:true}));obj.computeLineDistances();return obj;}
|
|
57
|
+
const floorNs=[...new Set(model.equipment.map(e=>e.floor))].sort((a,b)=>a-b);
|
|
58
|
+
const facility=commonFacilityBounds(model);
|
|
59
|
+
const assembly=planVisualAssembly(model);
|
|
60
|
+
const plannedRoutes=assembly.routes;
|
|
61
|
+
const sensorMounts=planSensorMounts(model,assembly);
|
|
62
|
+
const allBox=new T.Box3();
|
|
63
|
+
for(const e of model.equipment){const {x,y,z}=e.position;const d=e.dimensions||{x:2,y:1,z:1};const half=new T.Vector3(Math.max(.4,d.x/2),Math.max(.5,d.y/2)+.8,Math.max(.4,d.z/2));const p=new T.Vector3(x,y+d.y*.5,z);allBox.expandByPoint(p.clone().sub(half));allBox.expandByPoint(p.clone().add(half));}
|
|
64
|
+
// Subtle contact occlusion under conceptual equipment anchors. These are visual
|
|
65
|
+
// grounding cues only: no collision, load-bearing or as-built claim.
|
|
66
|
+
const shadowCanvas=document.createElement('canvas');shadowCanvas.width=128;shadowCanvas.height=128;
|
|
67
|
+
const sx=shadowCanvas.getContext('2d'),gradient=sx.createRadialGradient(64,64,4,64,64,62);
|
|
68
|
+
gradient.addColorStop(0,'rgba(0,7,15,.74)');gradient.addColorStop(.48,'rgba(0,10,18,.38)');gradient.addColorStop(1,'rgba(0,10,18,0)');
|
|
69
|
+
sx.fillStyle=gradient;sx.fillRect(0,0,128,128);
|
|
70
|
+
const contactTexture=new T.CanvasTexture(shadowCanvas);
|
|
71
|
+
const contactMat=new T.MeshBasicMaterial({map:contactTexture,transparent:true,opacity:.54,depthWrite:false,side:T.DoubleSide,polygonOffset:true,polygonOffsetFactor:-2});
|
|
72
|
+
const extent=allBox.getSize(new T.Vector3());const shadowExtent=Math.min(125,Math.max(22,extent.x,extent.z)*.85+9);
|
|
73
|
+
key.shadow.camera.left=-shadowExtent;key.shadow.camera.right=shadowExtent;key.shadow.camera.top=shadowExtent;key.shadow.camera.bottom=-shadowExtent;
|
|
74
|
+
key.position.copy(allBox.getCenter(new T.Vector3())).add(new T.Vector3(-22,35,23));key.target.position.copy(allBox.getCenter(new T.Vector3()));key.shadow.camera.updateProjectionMatrix();
|
|
75
|
+
for(const f of floorNs){
|
|
76
|
+
const g=new T.Group();g.userData.floor=f;floors.set(f,g);scene.add(g);
|
|
77
|
+
const arr=model.equipment.filter(e=>e.floor===f);
|
|
78
|
+
// One common facility footprint for all levels: separate operating floors,
|
|
79
|
+
// NOT three unrelated panels floating at different X/Z positions.
|
|
80
|
+
// Architectural envelope is shared, but each working deck is fitted to its
|
|
81
|
+
// actual equipment and spiral landing footprint, like R9's individual levels.
|
|
82
|
+
const minX=Math.min(...arr.map(e=>e.position.x-e.dimensions.x/2))-3.0;
|
|
83
|
+
const maxX=Math.max(...arr.map(e=>e.position.x+e.dimensions.x/2))+3.0;
|
|
84
|
+
const minZ=Math.min(...arr.map(e=>e.position.z-e.dimensions.z/2))-3.0;
|
|
85
|
+
const maxZ=Math.max(...arr.map(e=>e.position.z+e.dimensions.z/2))+3.0;
|
|
86
|
+
const w=maxX-minX,d=maxZ-minZ,centerX=(minX+maxX)/2,centerZ=(minZ+maxZ)/2;
|
|
87
|
+
const floorY=Math.min(...arr.map(e=>e.position.y))-.57;
|
|
88
|
+
const surf=new T.Mesh(new T.BoxGeometry(w,.16,d),
|
|
89
|
+
mat(f===floorNs[0]?0x19435b:0x2a6577,.46,.68,floorNs.length>1?.38:1));
|
|
90
|
+
surf.material.depthWrite=false;
|
|
91
|
+
surf.position.set(centerX,floorY-.08,centerZ);surf.receiveShadow=true;g.add(surf);
|
|
92
|
+
const outline=[new T.Vector3(minX,floorY+.02,minZ),new T.Vector3(maxX,floorY+.02,minZ),new T.Vector3(maxX,floorY+.02,maxZ),new T.Vector3(minX,floorY+.02,maxZ),new T.Vector3(minX,floorY+.02,minZ)];
|
|
93
|
+
const border=new T.Line(new T.BufferGeometry().setFromPoints(outline),new T.LineBasicMaterial({color:palette.edge,opacity:.42,transparent:true,depthWrite:false}));g.add(border);
|
|
94
|
+
const gridPts=[],step=Math.max(1.4,Math.min(2.2,Math.max(w,d)/24));
|
|
95
|
+
for(let x=minX;x<=maxX;x+=step)gridPts.push(new T.Vector3(x,floorY+.025,minZ),new T.Vector3(x,floorY+.025,maxZ));
|
|
96
|
+
for(let z=minZ;z<=maxZ;z+=step)gridPts.push(new T.Vector3(minX,floorY+.025,z),new T.Vector3(maxX,floorY+.025,z));
|
|
97
|
+
g.add(new T.LineSegments(new T.BufferGeometry().setFromPoints(gridPts),
|
|
98
|
+
new T.LineBasicMaterial({color:0x3e8493,opacity:.28,transparent:true,depthWrite:false})));
|
|
99
|
+
for(const e of arr){
|
|
100
|
+
const size=Math.max(1.4,e.dimensions.x*1.6),depth=Math.max(1.3,e.dimensions.z*2.1);
|
|
101
|
+
const patch=new T.Mesh(new T.PlaneGeometry(size,depth),contactMat);
|
|
102
|
+
patch.rotation.x=-Math.PI/2;patch.position.set(e.position.x,floorY+.043,e.position.z);
|
|
103
|
+
patch.renderOrder=1;g.add(patch);
|
|
104
|
+
}
|
|
105
|
+
g.userData.deckBounds={minX,maxX,minZ,maxZ,floorY};
|
|
106
|
+
|
|
107
|
+
// Subtle static architectural fill lights. They do not indicate machine RUN/FAULT state.
|
|
108
|
+
{const light=new T.PointLight(palette.accent,.36,Math.max(w,d)*1.35,2);light.position.set(centerX,floorY+8,centerZ);g.add(light);}
|
|
109
|
+
}
|
|
110
|
+
// One unified (but recessive) building envelope; no glowing wireframe cage.
|
|
111
|
+
if(facility && floorNs.length>1){
|
|
112
|
+
const ys=floorNs.map(f=>Math.min(...model.equipment.filter(e=>e.floor===f).map(e=>e.position.y))-.57);
|
|
113
|
+
const minY=Math.min(...ys),topY=Math.max(...ys)+3.0;
|
|
114
|
+
const steel=mat(0x476b79,.76,.51,.20);
|
|
115
|
+
for(const x of [facility.minX,facility.maxX])for(const z of [facility.minZ,facility.maxZ]){
|
|
116
|
+
box(scene,x,(minY+topY)/2,z,.12,topY-minY,.12,steel);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function conveyor(g,sx,sy,sz,base,station,sorter=false){
|
|
120
|
+
// Flow-normalize orientation from the customer graph; reverse-running upper
|
|
121
|
+
// conveyors must NOT all face the +X direction.
|
|
122
|
+
const bed=new T.Group();bed.rotation.y=-Math.atan2(station.direction.z,station.direction.x);g.add(bed);
|
|
123
|
+
const frameY=1.1,length=station.length,width=Math.max(.55,sz*.79);
|
|
124
|
+
const curve=new T.LineCurve3(new T.Vector3(-length/2,frameY,0),new T.Vector3(length/2,frameY,0));
|
|
125
|
+
buildR9Transport(T,bed,curve,{width,materials:{frame:mats.frame,belt:mats.belt,rail:mats.trim,roller:mats.roller},conceptual:true});
|
|
126
|
+
// Structural bents, drive housing, longitudinal girders and wear panels.
|
|
127
|
+
for(const x of [-length*.37,length*.37])for(const z of [-sz*.38,sz*.38]){
|
|
128
|
+
box(bed,x,frameY*.50,z,.085,frameY,.085,mats.frame);
|
|
129
|
+
box(bed,x,.08,z,.26,.14,.24,mats.dark);
|
|
130
|
+
}
|
|
131
|
+
for(const z of [-sz*.5,sz*.5])box(bed,0,frameY-.30,z,length,.12,.10,mats.frame);
|
|
132
|
+
box(bed,-length*.27,frameY-.40,sz*.36,.55,.42,.33,mats.dark); // concept-only drive casing
|
|
133
|
+
if(sorter){
|
|
134
|
+
box(bed,0,frameY-.25,0,length*.48,.15,sz*.85,mats.trim);
|
|
135
|
+
for(const br of station.branches){
|
|
136
|
+
const pts=sorterBranchPoints(station,br.to);
|
|
137
|
+
if(!pts)continue;
|
|
138
|
+
const route=new T.CatmullRomCurve3(pts.map(p=>new T.Vector3(p.x-g.position.x,p.y-g.position.y,p.z-g.position.z)),false,'centripetal',.34);
|
|
139
|
+
if(route.getLength()>.15){
|
|
140
|
+
buildR9Transport(T,g,route,{width:Math.min(.76,width*.79),
|
|
141
|
+
materials:{frame:mats.frame,belt:mats.belt,rail:mats.trim,roller:mats.roller},conceptual:true,rollers:true});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
for(const e of model.equipment){
|
|
147
|
+
const g=new T.Group();g.userData.asset=e.id;
|
|
148
|
+
const visualMount=sensorMounts[e.id];
|
|
149
|
+
g.position.set(visualMount?.position.x??e.position.x,e.position.y,visualMount?.position.z??e.position.z);
|
|
150
|
+
if(visualMount)g.rotation.y=-Math.atan2(visualMount.direction.z,visualMount.direction.x);
|
|
151
|
+
const {x:sx,y:sy,z:sz}=e.dimensions,kind=e.kind,base=col[kind]||col.other;
|
|
152
|
+
const station=assembly.stations[e.id];
|
|
153
|
+
if(kind==='conveyor'||kind==='sorter'||kind==='junction')conveyor(g,sx,sy,sz,base,station,kind==='sorter');
|
|
154
|
+
else if(kind==='spiral'){
|
|
155
|
+
const helixPts=station.helix.map(p=>new T.Vector3(p.x-e.position.x,p.y-e.position.y,p.z-e.position.z));
|
|
156
|
+
const height=station.exitY-station.baseY;
|
|
157
|
+
// R9 centerline ribbon shares EXACT ports with incoming/outgoing conveyor routes.
|
|
158
|
+
cylinder(g,0,1.1+height/2,0,.14,Math.max(.3,Math.abs(height)),mats.frame);
|
|
159
|
+
for(let a=0;a<4;a++){const t=a*Math.PI/2;box(g,Math.sin(t)*station.radius*.78,1.1+height/2,Math.cos(t)*station.radius*.78,.10,Math.max(.3,Math.abs(height)),.10,mats.frame);}
|
|
160
|
+
const helix=new T.CatmullRomCurve3(helixPts,false,'centripetal',.34);
|
|
161
|
+
buildR9Transport(T,g,helix,{width:.84,materials:{frame:mats.frame,belt:mats.belt,rail:mats.trim,roller:mats.roller},conceptual:true,rollers:false});
|
|
162
|
+
for(const pt of [helixPts[0],helixPts.at(-1)]){
|
|
163
|
+
const landing=box(g,pt.x,pt.y-.17,pt.z,.93,.14,.88,mats.frame);
|
|
164
|
+
landing.rotation.y=Math.atan2(pt.z,pt.x);
|
|
165
|
+
}
|
|
166
|
+
} else if(kind==='photoeye'){
|
|
167
|
+
// Two-post optical gate, read-only and not asserted as-installed.
|
|
168
|
+
for(const side of [-1,1])box(g,0,.61,side*.63,.085,1.22,.09,mats.frame);
|
|
169
|
+
box(g,0,1.24,0,.09,.09,1.28,mats.trim);
|
|
170
|
+
box(g,0,1.14,-.54,.28,.20,.22,mat(base));
|
|
171
|
+
box(g,0,1.14,.54,.28,.20,.22,mats.dark);
|
|
172
|
+
box(g,0,1.14,-.42,.10,.08,.08,mats.cyan);
|
|
173
|
+
} else if(kind==='scanner'){
|
|
174
|
+
// Overhead scan tunnel: crossbeam, reader head and guarded stanchions.
|
|
175
|
+
for(const side of [-1,1])box(g,0,.8,side*.72,.12,1.6,.12,mats.frame);
|
|
176
|
+
box(g,0,1.62,0,.16,.18,1.63,mats.trim);
|
|
177
|
+
box(g,0,1.62,0,.42,.23,.49,mat(base));
|
|
178
|
+
box(g,0,1.48,.13,.22,.06,.27,mats.dark);
|
|
179
|
+
} else if(kind==='vfd'||kind==='motor'){
|
|
180
|
+
box(g,0,Math.max(.4,sy*.5),0,sx,Math.max(.7,sy),sz,mat(base,.55,.52));
|
|
181
|
+
box(g,0,sy*.61,sz*.53,sx*.77,sy*.44,.075,mats.dark);
|
|
182
|
+
box(g,-sx*.23,sy*.64,sz*.585,sx*.25,.11,.018,mats.cyan);
|
|
183
|
+
} else if(kind==='tank'){
|
|
184
|
+
cylinder(g,0,sy/2,0,Math.min(sx,sz)/2,sy,mat(base,.6,.43));
|
|
185
|
+
cylinder(g,0,sy,0,Math.min(sx,sz)/2+.08,.11,mats.trim);
|
|
186
|
+
for(const x of [-sx*.25,sx*.25])box(g,x,.2,0,.12,.4,.12,mats.frame);
|
|
187
|
+
} else if(kind==='pump'){
|
|
188
|
+
box(g,0,.12,0,sx,.23,sz,mats.dark);cylinder(g,0,.65,0,Math.min(.54,sx*.4),1,mat(base));
|
|
189
|
+
const motor=cylinder(g,sx*.24,.68,0,.32,.45,mats.frame);motor.rotation.z=Math.PI/2;
|
|
190
|
+
} else if(kind==='valve'){
|
|
191
|
+
cylinder(g,0,sy*.46,0,.3,.48,mat(base));cylinder(g,0,sy,0,.4,.07,mats.highlight);
|
|
192
|
+
} else if(kind==='robot'){
|
|
193
|
+
cylinder(g,0,.23,0,.65,.46,mats.dark);box(g,0,1,0,.42,1.5,.4,mat(base));const arm=box(g,.5,1.65,0,1.25,.29,.3,mats.frame);arm.rotation.z=.18;
|
|
194
|
+
} else {
|
|
195
|
+
box(g,0,Math.max(.25,sy*.5),0,sx,sy,sz,mat(base));
|
|
196
|
+
box(g,0,Math.max(.4,sy),0,sx*.86,.085,sz*.82,mats.trim);
|
|
197
|
+
}
|
|
198
|
+
const name=label(e.id);name.position.set(0,Math.max(.8,sy)+.95,0);g.add(name);
|
|
199
|
+
// Accessible click targets: industrial belts are too thin for reliable picking.
|
|
200
|
+
// Invisible proxy is local to the asset group, never rendered, never collidable.
|
|
201
|
+
const isTransport=['conveyor','sorter','junction'].includes(kind);
|
|
202
|
+
const hitHeight=kind==='spiral'?Math.max(2,Math.abs(station.exitY-station.baseY)+1):1.7;
|
|
203
|
+
const hitLength=isTransport?Math.max(1.8,station.length):kind==='spiral'?Math.max(1.5,station.radius*2.2):Math.max(.85,sx*1.45);
|
|
204
|
+
const hitWidth=kind==='spiral'?Math.max(1.5,station.radius*2.2):Math.max(1.1,sz*1.5);
|
|
205
|
+
const proxy=new T.Mesh(new T.BoxGeometry(hitLength,hitHeight,hitWidth),new T.MeshBasicMaterial({transparent:true,opacity:0,depthWrite:false,colorWrite:false}));
|
|
206
|
+
proxy.position.y=kind==='spiral'?1.1+(station.exitY-station.baseY)*.5:1.1;
|
|
207
|
+
if(isTransport)proxy.rotation.y=-Math.atan2(station.direction.z,station.direction.x);
|
|
208
|
+
proxy.userData.asset=e.id;proxy.userData.clickProxy=true;g.add(proxy);
|
|
209
|
+
g.traverse(o=>{if(o.isMesh||o.isSprite){o.userData.asset=e.id;pickables.push(o);}});
|
|
210
|
+
floors.get(e.floor).add(g);assets.set(e.id,g);
|
|
211
|
+
}
|
|
212
|
+
// Physically legible conceptual conveyor transition surfaces along the supplied
|
|
213
|
+
// material-flow graph, never an invented edge or certified OEM connection.
|
|
214
|
+
function conceptualRoute(route){
|
|
215
|
+
const pts=route.points.map(p=>new T.Vector3(p.x,p.y,p.z));
|
|
216
|
+
if(pts.length<2)return;
|
|
217
|
+
// Same R9 centerline ribbon algorithm as the equipment bodies and spirals.
|
|
218
|
+
// A proposed path only: does not certify interface tangency or roller mechanics.
|
|
219
|
+
const curve=new T.CatmullRomCurve3(pts,false,'centripetal',.34);
|
|
220
|
+
if(curve.getLength()<.12)return;
|
|
221
|
+
const routeGroup=new T.Group();
|
|
222
|
+
routeGroup.userData.conceptualConnector=true;
|
|
223
|
+
routeGroup.userData.routeFloor=route.kind==='conceptual_spiral_transfer'?route.to_floor:route.from_floor;
|
|
224
|
+
routeGroup.userData.geometry_status='CONCEPTUAL_CONNECTION_NEEDS_APPROVAL';
|
|
225
|
+
const rendered=buildR9Transport(T,routeGroup,curve,{width:.86,
|
|
226
|
+
materials:{frame:mats.frame,belt:mats.belt,rail:mats.trim,roller:mats.roller},conceptual:true});
|
|
227
|
+
rendered.userData.from=route.from;rendered.userData.to=route.to;
|
|
228
|
+
// Clicking a connecting belt opens its declared upstream equipment.
|
|
229
|
+
routeGroup.traverse(o=>{if(o.isMesh){o.userData.asset=route.from;o.userData.conceptualRoute=true;pickables.push(o);}});
|
|
230
|
+
scene.add(routeGroup);
|
|
231
|
+
}
|
|
232
|
+
for(const c of plannedRoutes){
|
|
233
|
+
if(c.kind==='conceptual_conveyor_transition'||c.kind==='conceptual_spiral_transfer'){
|
|
234
|
+
conceptualRoute(c);
|
|
235
|
+
}else{
|
|
236
|
+
// Unroutable or non-material links stay explicitly dashed logical connections.
|
|
237
|
+
const pts=c.points.map(p=>new T.Vector3(p.x,p.y,p.z));
|
|
238
|
+
const conn=line(pts[0],pts[pts.length-1]);conn.userData.floor=c.from_floor;scene.add(conn);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// Synthetic parcel visualization only. Uses EXACT declared edges, no inferred
|
|
242
|
+
// routes, no OPC data and no assertion of actual package tracking.
|
|
243
|
+
const simulatedParcels=new Map();
|
|
244
|
+
const parcelMaterial=new T.MeshStandardMaterial({color:0xffd579,roughness:.68,metalness:.08,emissive:0x62420b,emissiveIntensity:.24});
|
|
245
|
+
const parcelGeometry=new T.BoxGeometry(.48,.34,.38);
|
|
246
|
+
function demoPath(path){
|
|
247
|
+
// Render synthetic parcels on the SAME R9 generated centerlines as belts,
|
|
248
|
+
// sorter branches, spiral helixes and declared transition geometry.
|
|
249
|
+
// This is strictly a conceptual animation, not PLC positioning or speed.
|
|
250
|
+
if(!Array.isArray(path)||!path.length||path.length>model.equipment.length)return null;
|
|
251
|
+
const known=new Map(model.equipment.map(x=>[x.id,x]));
|
|
252
|
+
const track=new T.CurvePath();let last=null;
|
|
253
|
+
const V=p=>new T.Vector3(p.x,p.y+.24,p.z);
|
|
254
|
+
function addCurve(curve){
|
|
255
|
+
const start=curve.getPoint(0),end=curve.getPoint(1);
|
|
256
|
+
if(!Number.isFinite(start.x+start.y+start.z+end.x+end.y+end.z)||start.distanceTo(end)<.001)return false;
|
|
257
|
+
if(last&&last.distanceTo(start)>.015)return false;
|
|
258
|
+
track.add(curve);last=end;return true;
|
|
259
|
+
}
|
|
260
|
+
function line(a,b){const u=V(a),v=V(b);return u.distanceTo(v)<.001?true:addCurve(new T.LineCurve3(u,v));}
|
|
261
|
+
function curved(points){
|
|
262
|
+
const ps=points.map(V);
|
|
263
|
+
if(ps.length<2)return false;
|
|
264
|
+
return addCurve(new T.CatmullRomCurve3(ps,false,'centripetal',.34));
|
|
265
|
+
}
|
|
266
|
+
for(let i=0;i<path.length;i++){
|
|
267
|
+
const id=path[i],e=known.get(id),st=assembly.stations[id];if(!e||!st)return null;
|
|
268
|
+
if(st.helix){if(!curved(st.helix))return null;}
|
|
269
|
+
else{
|
|
270
|
+
if(!line(st.entry,st.center))return null;
|
|
271
|
+
const next=path[i+1],out=st.outlets?.[next]?.point||st.exit;
|
|
272
|
+
if(next&&st.kind==='sorter'&&out!==st.exit){
|
|
273
|
+
// EXACT same four control points as R9 sorter diverter ribbon.
|
|
274
|
+
const controls=sorterBranchPoints(st,next);
|
|
275
|
+
if(!controls||!curved(controls))return null;
|
|
276
|
+
}else if(!line(st.center,out))return null;
|
|
277
|
+
}
|
|
278
|
+
if(i<path.length-1){
|
|
279
|
+
const r=plannedRoutes.find(x=>x.from===id&&x.to===path[i+1]);
|
|
280
|
+
if(!r||!['conceptual_conveyor_transition','conceptual_spiral_transfer'].includes(r.kind))return null;
|
|
281
|
+
if(!curved(r.points))return null;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return track.curves.length?track:null;
|
|
285
|
+
}
|
|
286
|
+
let lastParcelRoute='',lastParcelCurve=null;
|
|
287
|
+
function parcels(rows){
|
|
288
|
+
if(!Array.isArray(rows))rows=[];
|
|
289
|
+
const liveIds=new Set();
|
|
290
|
+
for(const p of rows.slice(0,4)){
|
|
291
|
+
if(!p||typeof p.id!=='string'||!/^[A-Z0-9-]{1,24}$/.test(p.id)||typeof p.progress!=='number'||p.progress<0||p.progress>1)continue;
|
|
292
|
+
const routeKey=Array.isArray(p.path)?p.path.join('>'):'';
|
|
293
|
+
if(routeKey!==lastParcelRoute){lastParcelCurve=demoPath(p.path);lastParcelRoute=routeKey;}
|
|
294
|
+
if(!lastParcelCurve)continue;
|
|
295
|
+
let obj=simulatedParcels.get(p.id);
|
|
296
|
+
if(!obj){obj=new T.Mesh(parcelGeometry,parcelMaterial);obj.castShadow=true;scene.add(obj);simulatedParcels.set(p.id,obj);}
|
|
297
|
+
// Smoothly interpolate sampled synthetic progress between HTTP polls.
|
|
298
|
+
// This is rendering interpolation, NOT a measured position claim.
|
|
299
|
+
const oldProgress=obj.userData.demoProgress;
|
|
300
|
+
obj.userData.demoFrom=typeof oldProgress==='number'&&p.progress>=oldProgress?oldProgress:p.progress;
|
|
301
|
+
obj.userData.demoTarget=p.progress;
|
|
302
|
+
obj.userData.demoAt=performance.now();
|
|
303
|
+
obj.userData.demoCurve=lastParcelCurve;
|
|
304
|
+
obj.userData.syntheticParcel=true;
|
|
305
|
+
liveIds.add(p.id);
|
|
306
|
+
}
|
|
307
|
+
for(const [id,obj] of simulatedParcels)if(!liveIds.has(id)){scene.remove(obj);simulatedParcels.delete(id);}
|
|
308
|
+
}
|
|
309
|
+
// Selection is an inspector affordance, never RUN/FAULT state.
|
|
310
|
+
const selectRing=new T.Mesh(new T.RingGeometry(.66,.72,48),new T.MeshBasicMaterial({color:palette.accent,side:T.DoubleSide,transparent:true,opacity:.85,depthWrite:false}));
|
|
311
|
+
selectRing.rotation.x=-Math.PI/2;selectRing.visible=false;scene.add(selectRing);
|
|
312
|
+
// Simulated FAT replay markers. Separate from asset materials and never represent live PLC state.
|
|
313
|
+
const replayRings=new Map();let visibleFloor='all';
|
|
314
|
+
// Live LAB status overlays (read-only). Never recolor the immutable machine
|
|
315
|
+
// geometry or imply PLC control authority. Only signal-qualified states appear.
|
|
316
|
+
const stateRings=new Map();
|
|
317
|
+
function operatingStates(signals){
|
|
318
|
+
for(const old of stateRings.values()){scene.remove(old);old.geometry.dispose();old.material.dispose();}
|
|
319
|
+
stateRings.clear();
|
|
320
|
+
if(!Array.isArray(signals))return;
|
|
321
|
+
const byAsset=new Map();
|
|
322
|
+
for(const sig of signals){
|
|
323
|
+
if(!sig?.valid)continue;
|
|
324
|
+
if(!['run_feedback','fault','photoeye_blocked'].includes(sig.role))continue;
|
|
325
|
+
let row=byAsset.get(sig.asset_id);if(!row){row={};byAsset.set(sig.asset_id,row);}
|
|
326
|
+
// No inference if multiple signals disagree; ambiguous status is UNKNOWN.
|
|
327
|
+
if(sig.role in row && row[sig.role]!==sig.value)row.invalid=true;
|
|
328
|
+
row[sig.role]=sig.value;
|
|
329
|
+
}
|
|
330
|
+
for(const [assetId,row] of byAsset){
|
|
331
|
+
if(row.invalid)continue;
|
|
332
|
+
const e=model.equipment.find(x=>x.id===assetId);if(!e||!assets.has(assetId))continue;
|
|
333
|
+
const severity=row.fault===true?'fault':row.photoeye_blocked===true?'blocked':row.run_feedback===true?'running':null;
|
|
334
|
+
if(!severity)continue; // Unknown or stopped: no visual state claim.
|
|
335
|
+
const color=severity==='fault'?0xff6582:severity==='blocked'?0xe8b55e:0x36ce9b;
|
|
336
|
+
const ring=new T.Mesh(new T.RingGeometry(.60,.71,40),new T.MeshBasicMaterial({color,side:T.DoubleSide,transparent:true,opacity:.93,depthWrite:false}));
|
|
337
|
+
ring.rotation.x=-Math.PI/2;ring.position.set(e.position.x,e.position.y-.17,e.position.z);
|
|
338
|
+
ring.scale.setScalar(Math.max(1,Math.min(2.6,e.dimensions.x*.50)));
|
|
339
|
+
ring.visible=visibleFloor==='all'||String(e.floor)===visibleFloor;
|
|
340
|
+
ring.userData.labOnlyMarker=true;scene.add(ring);stateRings.set(assetId,ring);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function alarms(entries){
|
|
345
|
+
for(const old of replayRings.values()){scene.remove(old);old.geometry.dispose();old.material.dispose();}
|
|
346
|
+
replayRings.clear();
|
|
347
|
+
if(!Array.isArray(entries))return;
|
|
348
|
+
for(const item of entries){
|
|
349
|
+
const e=model.equipment.find(x=>x.id===item.asset_id);
|
|
350
|
+
if(!e||!assets.has(e.id))continue;
|
|
351
|
+
const isFault=item.severity==='CRITICAL';
|
|
352
|
+
const ring=new T.Mesh(new T.RingGeometry(.84,1.02,48),
|
|
353
|
+
new T.MeshBasicMaterial({color:isFault?0xff6582:0xe8b55e,side:T.DoubleSide,transparent:true,opacity:.95,depthWrite:false}));
|
|
354
|
+
ring.rotation.x=-Math.PI/2;
|
|
355
|
+
ring.position.set(e.position.x,e.position.y-.30,e.position.z);
|
|
356
|
+
ring.scale.setScalar(Math.max(1,Math.min(2.8,e.dimensions.x*.57)));
|
|
357
|
+
ring.visible=visibleFloor==='all'||String(e.floor)===visibleFloor;
|
|
358
|
+
ring.userData.isReplayMarker=true;
|
|
359
|
+
scene.add(ring);replayRings.set(e.id,ring);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
const ray=new T.Raycaster();const cursor=new T.Vector2();let selectedId=null;
|
|
363
|
+
function selected(id){selectedId=id;const g=assets.get(id);selectRing.visible=!!g;if(g){const e=model.equipment.find(x=>x.id===id);selectRing.position.set(g.position.x,e.position.y-.42,g.position.z);selectRing.scale.setScalar(Math.max(1,Math.min(2.5,e.dimensions.x*.48)));}}
|
|
364
|
+
// Frame each customer facility, independent of floor count or asset quantity.
|
|
365
|
+
const bb=new T.Box3();
|
|
366
|
+
for(const e of model.equipment){
|
|
367
|
+
const p=new T.Vector3(e.position.x,e.position.y+e.dimensions.y*.5,e.position.z);
|
|
368
|
+
const half=new T.Vector3(Math.max(e.dimensions.x,1)*.5,Math.max(e.dimensions.y,1)*.5+1.5,Math.max(e.dimensions.z,1)*.5);
|
|
369
|
+
bb.expandByPoint(p.clone().sub(half));bb.expandByPoint(p.clone().add(half));
|
|
370
|
+
}
|
|
371
|
+
if(facility){
|
|
372
|
+
bb.expandByPoint(new T.Vector3(facility.minX,-1.6,facility.minZ));
|
|
373
|
+
bb.expandByPoint(new T.Vector3(facility.maxX,Math.max(...model.equipment.map(e=>e.position.y))+4.7,facility.maxZ));
|
|
374
|
+
}
|
|
375
|
+
let activeBox=bb;
|
|
376
|
+
if(!bb.isEmpty())target.copy(bb.getCenter(new T.Vector3()));
|
|
377
|
+
function fitCamera(){
|
|
378
|
+
if(activeBox.isEmpty())return;
|
|
379
|
+
const w=Math.max(320,host.clientWidth||1100),h=Math.max(280,host.clientHeight||640);
|
|
380
|
+
const aspect=w/h,halfAngle=T.MathUtils.degToRad(camera.fov*.5),m=.85;
|
|
381
|
+
const n=new T.Vector3(Math.cos(el)*Math.sin(az),Math.sin(el),Math.cos(el)*Math.cos(az));
|
|
382
|
+
const right=new T.Vector3(Math.cos(az),0,-Math.sin(az));
|
|
383
|
+
const up=new T.Vector3().crossVectors(n,right).normalize();
|
|
384
|
+
let xr=0,yr=0,front=0;
|
|
385
|
+
for(const x of [activeBox.min.x,activeBox.max.x])for(const y of [activeBox.min.y,activeBox.max.y])for(const z of [activeBox.min.z,activeBox.max.z]){
|
|
386
|
+
const d=new T.Vector3(x-target.x,y-target.y,z-target.z);
|
|
387
|
+
xr=Math.max(xr,Math.abs(d.dot(right)));yr=Math.max(yr,Math.abs(d.dot(up)));front=Math.max(front,d.dot(n));
|
|
388
|
+
}
|
|
389
|
+
dist=Math.min(450,Math.max(7,front+Math.max(xr/(Math.tan(halfAngle)*aspect*m),yr/(Math.tan(halfAngle)*m))+1.1));
|
|
390
|
+
}
|
|
391
|
+
function updateCamera(){camera.position.set(target.x+dist*Math.cos(el)*Math.sin(az),target.y+dist*Math.sin(el),target.z+dist*Math.cos(el)*Math.cos(az));camera.lookAt(target);}
|
|
392
|
+
function view(mode){if(mode==='top'){el=1.52;az=0;}else if(mode==='side'){el=.18;az=Math.PI/2;}else{az=.88;el=.54;}fitCamera();updateCamera();}
|
|
393
|
+
function focus(id){
|
|
394
|
+
const e=model.equipment.find(x=>x.id===id);if(!e)return false;
|
|
395
|
+
if(visibleFloor!=='all'&&String(e.floor)!==visibleFloor)floor(String(e.floor));
|
|
396
|
+
const size=new T.Vector3(Math.max(3.5,e.dimensions.x*2.0),Math.max(3.5,e.dimensions.y*2.0),Math.max(3.5,e.dimensions.z*2.0));
|
|
397
|
+
const center=new T.Vector3(e.position.x,e.position.y+e.dimensions.y*.5,e.position.z);
|
|
398
|
+
activeBox=new T.Box3().setFromCenterAndSize(center,size);target.copy(center);
|
|
399
|
+
fitCamera();dist=Math.max(6.5,Math.min(dist,Math.max(8,Math.max(size.x,size.y,size.z)*2.2)));updateCamera();
|
|
400
|
+
selected(id);return true;
|
|
401
|
+
}
|
|
402
|
+
function overview(){floor(visibleFloor);view('iso');}
|
|
403
|
+
function floor(which){visibleFloor=which;for(const [id,marker] of replayRings){const e=model.equipment.find(x=>x.id===id);marker.visible=which==='all'||String(e.floor)===which;}for(const [id,marker] of stateRings){const e=model.equipment.find(x=>x.id===id);marker.visible=which==='all'||String(e.floor)===which;}for(const [f,g] of floors)g.visible=which==='all'||String(f)===which;for(const obj of scene.children){const rf=obj.userData.routeFloor??(obj.isLine?obj.userData.floor:undefined);if(rf!==undefined)obj.visible=which==='all'||String(rf)===which;}
|
|
404
|
+
// Focus the camera on the selected level instead of leaving it tiny in full-building view.
|
|
405
|
+
activeBox=which==='all'?bb:new T.Box3();
|
|
406
|
+
if(which!=='all')for(const e of model.equipment.filter(e=>String(e.floor)===which)){
|
|
407
|
+
const p=new T.Vector3(e.position.x,e.position.y+e.dimensions.y*.5,e.position.z);
|
|
408
|
+
const h=new T.Vector3(e.dimensions.x*.5,e.dimensions.y*.5+1.5,e.dimensions.z*.5);
|
|
409
|
+
activeBox.expandByPoint(p.clone().sub(h));activeBox.expandByPoint(p.clone().add(h));
|
|
410
|
+
}
|
|
411
|
+
if(!activeBox.isEmpty())target.copy(activeBox.getCenter(new T.Vector3()));
|
|
412
|
+
fitCamera();updateCamera();
|
|
413
|
+
}
|
|
414
|
+
let dragging=false,moved=false,px=0,py=0,startX=0,startY=0;
|
|
415
|
+
renderer.domElement.style.touchAction='none';
|
|
416
|
+
renderer.domElement.addEventListener('pointerdown',ev=>{if(ev.button!==0)return;dragging=true;moved=false;startX=px=ev.clientX;startY=py=ev.clientY;renderer.domElement.setPointerCapture(ev.pointerId);});
|
|
417
|
+
renderer.domElement.addEventListener('pointermove',ev=>{if(!dragging)return;const dx=ev.clientX-px,dy=ev.clientY-py;px=ev.clientX;py=ev.clientY;if(Math.hypot(ev.clientX-startX,ev.clientY-startY)>6)moved=true;if(!moved)return;az-=dx*.006;el=Math.max(.08,Math.min(1.5,el+dy*.006));updateCamera();});
|
|
418
|
+
renderer.domElement.addEventListener('pointercancel',()=>{dragging=false;});
|
|
419
|
+
renderer.domElement.addEventListener('pointerup',ev=>{if(!dragging)return;dragging=false;if(moved)return;
|
|
420
|
+
const box=renderer.domElement.getBoundingClientRect();cursor.x=((ev.clientX-box.left)/box.width)*2-1;cursor.y=-((ev.clientY-box.top)/box.height)*2+1;ray.setFromCamera(cursor,camera);
|
|
421
|
+
const isVisible=o=>{while(o&&o!==scene){if(!o.visible)return false;o=o.parent;}return true;};
|
|
422
|
+
const chosen=ray.intersectObjects(pickables,false).find(h=>isVisible(h.object)&&h.object.userData.asset);
|
|
423
|
+
if(chosen)onSelect(chosen.object.userData.asset);
|
|
424
|
+
});renderer.domElement.addEventListener('wheel',ev=>{ev.preventDefault();dist=Math.min(260,Math.max(6,dist*(ev.deltaY>0?1.11:.90)));updateCamera();},{passive:false});
|
|
425
|
+
function resize(){const r=host.getBoundingClientRect();renderer.setSize(Math.max(320,r.width),Math.max(280,r.height),false);camera.aspect=Math.max(320,r.width)/Math.max(280,r.height);camera.updateProjectionMatrix();fitCamera();updateCamera();}const obs=new ResizeObserver(resize);obs.observe(host);resize();let active=true;
|
|
426
|
+
function frame(){if(!active)return;
|
|
427
|
+
const now=performance.now();
|
|
428
|
+
for(const obj of simulatedParcels.values()){
|
|
429
|
+
const curve=obj.userData.demoCurve;if(!curve)continue;
|
|
430
|
+
const weight=Math.min(1,Math.max(0,(now-obj.userData.demoAt)/1050));
|
|
431
|
+
const progress=obj.userData.demoFrom+(obj.userData.demoTarget-obj.userData.demoFrom)*weight;
|
|
432
|
+
const at=curve.getPointAt(progress),tangent=curve.getTangentAt(progress);
|
|
433
|
+
obj.position.copy(at);obj.rotation.y=-Math.atan2(tangent.z,tangent.x);
|
|
434
|
+
obj.userData.demoProgress=progress;
|
|
435
|
+
if(visibleFloor==='all')obj.visible=true;
|
|
436
|
+
else obj.visible=model.equipment.some(e=>String(e.floor)===visibleFloor&&Math.abs(e.position.y-at.y)<3.5);
|
|
437
|
+
}
|
|
438
|
+
renderer.render(scene,camera);requestAnimationFrame(frame);
|
|
439
|
+
}requestAnimationFrame(frame);
|
|
440
|
+
window.addEventListener('pagehide',()=>{active=false;obs.disconnect();renderer.dispose();},{once:true});
|
|
441
|
+
return {select:selected,floor,view,focus,overview,alarms,operatingStates,parcels,mount:id=>sensorMounts[id]||null};
|
|
442
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/* V0.7.5 single, server-gated AI -> engineering harness -> customer review -> concept. */
|
|
2
|
+
(()=>{'use strict';
|
|
3
|
+
const el=id=>document.getElementById(id);
|
|
4
|
+
const ids=['name','industry','layout','palette','description','equipment_csv','connections_csv','signals_csv','requirements'];
|
|
5
|
+
const snapshot=()=>Object.fromEntries(ids.map(id=>[id,el(id).value]));
|
|
6
|
+
const fingerprint=()=>JSON.stringify(snapshot());
|
|
7
|
+
let reviewed=null, token=null, expected=null, busy=false;
|
|
8
|
+
let workspaceGeneration=0;
|
|
9
|
+
const out=el('engineer-results'),approve=el('engineer-approval'),confirm=el('engineer-confirm');
|
|
10
|
+
const show=(msg,hold=false)=>{out.classList.remove('hidden');out.classList.toggle('hold',hold);out.textContent=msg;};
|
|
11
|
+
const label=txt=>{el('engineer-stage-badge').textContent=txt;el('engineer-validation-state').textContent=txt;};
|
|
12
|
+
function invalidate(){token=null;reviewed=null;expected=null;approve.classList.add('hidden');confirm.checked=false;el('engineer-build').disabled=true;
|
|
13
|
+
el('engineer-equipment-state').textContent=String(Math.max(0,el('equipment_csv').value.split(/\r?\n/).filter((r,i)=>i>0&&r.trim()).length))+' supplied';
|
|
14
|
+
el('engineer-model-state').textContent=window.DevAgentAIFlow?.isConnected()?(el('ai-provider').value+' / '+el('ai-model').value):'Not connected';
|
|
15
|
+
if(!busy)label('REVIEW REQUIRED');}
|
|
16
|
+
for(const id of ids){el(id).addEventListener('input',invalidate);el(id).addEventListener('change',invalidate);}
|
|
17
|
+
document.addEventListener('devagent:changed',()=>{if(!token)invalidate();});
|
|
18
|
+
document.addEventListener('devagent:ai-connection',invalidate);
|
|
19
|
+
document.addEventListener('devagent:project-reset',()=>{
|
|
20
|
+
workspaceGeneration++;
|
|
21
|
+
const stale=token;invalidate();busy=false;el('engineer-run').disabled=false;
|
|
22
|
+
out.classList.add('hidden');out.classList.remove('hold');
|
|
23
|
+
if(stale)fetch('/api/engineer/revoke',{method:'POST',keepalive:true,
|
|
24
|
+
headers:{'Content-Type':'application/json','X-DevAgent-Request':'engineer-pipeline-v1'},
|
|
25
|
+
body:JSON.stringify({review_token:stale,confirm:'REVOKE_LOCAL_REVIEW_ONLY'})}).catch(()=>{});
|
|
26
|
+
});
|
|
27
|
+
confirm.addEventListener('change',()=>{el('engineer-build').disabled=!confirm.checked||!token||fingerprint()!==expected;});
|
|
28
|
+
const request=async(url,body)=>{const ctl=new AbortController(),timer=setTimeout(()=>ctl.abort(),140000);
|
|
29
|
+
try{const r=await fetch(url,{method:'POST',headers:{'Content-Type':'application/json','X-DevAgent-Request':'engineer-pipeline-v1'},body:JSON.stringify(body),signal:ctl.signal});
|
|
30
|
+
return {ok:r.ok,data:await r.json()};}finally{clearTimeout(timer);}};
|
|
31
|
+
el('engineer-run').addEventListener('click',async()=>{
|
|
32
|
+
if(busy||!window.DevAgentAIFlow?.requireConnection())return;
|
|
33
|
+
const generation=workspaceGeneration;
|
|
34
|
+
busy=true;el('engineer-run').disabled=true;invalidate();
|
|
35
|
+
label('AI + ENGINE CHECKS RUNNING');show('Model proposal requested. The independent engineering harness validates all returned alternatives and prepares the unexecuted FAT inventory.\nNo stage is PASS until server evidence arrives.');
|
|
36
|
+
const original=snapshot();const conn={provider:el('ai-provider').value,model:el('ai-model').value.trim(),endpoint:el('ai-endpoint').value.trim(),api_key:el('ai-key').value};
|
|
37
|
+
try{const r=await request('/api/engineer/prepare',{wizard:original,connection:conn,request:el('ai-request').value.trim(),max_attempts:2});
|
|
38
|
+
if(generation!==workspaceGeneration)return;
|
|
39
|
+
if(!r.ok){
|
|
40
|
+
const failures=(Array.isArray(r.data?.attempts)?r.data.attempts:[]).slice(0,3);
|
|
41
|
+
const detail=failures.map(t=>'Attempt '+Number(t.attempt)+': '+(t.failure_code||t.reason||t.result||'UNKNOWN')).join('\n');
|
|
42
|
+
const codes=failures.map(t=>t.failure_code||'');
|
|
43
|
+
const advice=codes.some(c=>['AI_PROPOSAL_NOT_STRICT_JSON','AI_PROPOSAL_SCHEMA_INVALID','AI_PROPOSAL_FIELDS_INVALID','AI_PROVIDER_RESPONSE_TOO_LARGE'].includes(c))
|
|
44
|
+
? 'AI did not return compact strict JSON. Customer CSV is retained by the Engineering Engine and should not be repeated in model output.'
|
|
45
|
+
: codes.some(c=>c.startsWith('MODEL_CANNOT_')||c==='CUSTOMER_SOURCE_OR_VALIDATION_GATE')
|
|
46
|
+
? 'Proposal attempted to change source data or requirements. Keep equipment, topology, mappings and customer choices unchanged.'
|
|
47
|
+
: codes.some(c=>c.startsWith('AI_PROVIDER_'))
|
|
48
|
+
? 'Check provider/model response and usage limits. Test Connection alone does not validate an engineering proposal.'
|
|
49
|
+
: 'Inspect the attempt codes; do not erase customer inputs or invent mappings to force PASS.';
|
|
50
|
+
label('HOLD');show('AI Engineering HOLD: '+(r.data?.reason||'UNKNOWN')+'\n\n'+(detail?'Rejected attempts:\n'+detail+'\n\n':'')+advice+'\nNo HMI generated. Source inputs preserved. Production HOLD.',true);return;}
|
|
51
|
+
|
|
52
|
+
const result=r.data;const proposed=result.winner?.fields;
|
|
53
|
+
if(!proposed){label('HOLD');show('No validated proposal available.',true);return;}
|
|
54
|
+
// Server receipt verifies this exact candidate at the separate build endpoint.
|
|
55
|
+
// Previewing the proposed customer-visible changes is part of the approval process.
|
|
56
|
+
const changes=[];for(const k of ids){if(typeof proposed[k]==='string'&&proposed[k]!==original[k])changes.push('• '+k+': proposed update ('+proposed[k].length+' characters)');}
|
|
57
|
+
if(fingerprint()!==JSON.stringify(original)){label('HOLD');show('Inputs changed during AI review. Run again.',true);return;}
|
|
58
|
+
reviewed=proposed;token=result.review_token;
|
|
59
|
+
// apply candidate fields with existing UI bindings; this is not a production mapping authorization.
|
|
60
|
+
for(const k of ids){if(typeof proposed[k]==='string'){
|
|
61
|
+
el(k).value=proposed[k];el(k).dispatchEvent(new Event('input',{bubbles:true}));}}
|
|
62
|
+
// input dispatch revokes local token, then restore only after all updates to the reviewed proposal.
|
|
63
|
+
token=result.review_token;reviewed=proposed;expected=fingerprint();
|
|
64
|
+
const trials=(result.attempts||[]).map(t=>'Attempt '+t.attempt+': '+t.result).join('\n');
|
|
65
|
+
const problems=(result.engineering?.findings||[]).slice(0,9).map(f=>'• '+f.category+(f.asset_id?' ['+f.asset_id+']':'')).join('\n');
|
|
66
|
+
label('STATIC-ELIGIBLE · REVIEW');
|
|
67
|
+
show('AI proposals and deterministic checks complete.\n'+trials+'\n\nCustomer proposal changes:\n'+(changes.join('\n')||'• No editable field changes')+'\n\nSource gaps / recommended checks:\n'+(problems||'None listed; physical validation still required')+'\n\nIgnition and real PLC: NOT VERIFIED · Production: HOLD');
|
|
68
|
+
el('engineer-review-summary').textContent='Customer review required. Generated preview is conceptual only. All unverified OPC mappings, real equipment associations and safety logic remain HOLD. Receipt expires in 10 minutes.';
|
|
69
|
+
approve.classList.remove('hidden');confirm.checked=false;el('engineer-build').disabled=true;
|
|
70
|
+
}catch(e){if(generation===workspaceGeneration){label('HOLD');show('AI pipeline unavailable or timed out. No project was generated.',true);}}
|
|
71
|
+
finally{if(generation===workspaceGeneration){busy=false;el('engineer-run').disabled=false;}}});
|
|
72
|
+
el('engineer-build').addEventListener('click',async()=>{
|
|
73
|
+
if(busy||!token||!confirm.checked||fingerprint()!==expected||!window.DevAgentAIFlow?.isConnected()){
|
|
74
|
+
label('HOLD');show('Review expired or inputs/model changed. Run AI Engineer again.',true);return;}
|
|
75
|
+
busy=true;el('engineer-build').disabled=true;el('engineer-run').disabled=true;
|
|
76
|
+
const generation=workspaceGeneration;
|
|
77
|
+
try{label('GENERATING VERIFIED CONCEPT');show('Revalidating reviewed AI candidate on the server and generating isolated preview…');
|
|
78
|
+
const rsp=await request('/api/engineer/build',{wizard:snapshot(),review_token:token,confirm:'CUSTOMER_APPROVED_ISOLATED_CONCEPT_ONLY'});
|
|
79
|
+
if(generation!==workspaceGeneration)return;
|
|
80
|
+
token=null;
|
|
81
|
+
if(!rsp.ok){label('HOLD');show('Build HOLD: '+(rsp.data.reason||'UNKNOWN')+'\nNo production claims.',true);return;}
|
|
82
|
+
const r=rsp.data;
|
|
83
|
+
// A successfully generated project must be visible to the customer.
|
|
84
|
+
// The success panel lives in Step 04, which may be hidden after loading
|
|
85
|
+
// a demo on Step 01. Navigate using the existing four-step handler.
|
|
86
|
+
const reviewStep=document.querySelectorAll('.step')[3];
|
|
87
|
+
if(reviewStep)reviewStep.click();
|
|
88
|
+
el('success').classList.remove('hidden');el('preview-link').href=r.url;
|
|
89
|
+
el('studio-test-link').href='/projects/'+r.project_id+'/test/index.html';
|
|
90
|
+
el('result').className='status pass';el('result').textContent=r.status+' · '+r.fat_cases+' FAT inventory cases · production HOLD';
|
|
91
|
+
label('PREVIEW GENERATED · FAT PENDING');show('AI + Engineering Server Receipt: PASS (offline only)\nProject: '+r.project_id+'\nFAT inventory: '+r.fat_cases+' unexecuted test plans\nIgnition Gateway FAT: NOT VERIFIED\nProduction release: HOLD');
|
|
92
|
+
document.dispatchEvent(new CustomEvent('devagent:project-built',{detail:{project_id:r.project_id}}));
|
|
93
|
+
el('success').scrollIntoView({behavior:'smooth',block:'center'});
|
|
94
|
+
}catch(e){if(generation===workspaceGeneration){label('HOLD');show('Build request failed; no proof of a valid HMI. Review Gateway logs.',true);}}
|
|
95
|
+
finally{if(generation===workspaceGeneration){busy=false;el('engineer-run').disabled=false;approve.classList.add('hidden');}}});
|
|
96
|
+
// Initialize the panel with the actual current form values. The connection
|
|
97
|
+
// script and HTML model data may have been populated before this deferred JS ran.
|
|
98
|
+
invalidate();
|
|
99
|
+
label(el('equipment_csv').value.trim() ? 'READY FOR AI REVIEW' : 'WAITING FOR INPUTS');
|
|
100
|
+
window.DevAgentUnifiedReady=true;
|
|
101
|
+
})();
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/* V0.7.6 isolated *fake OPC UA* test AFTER AI-reviewed Generate. Never a PLC or Ignition action. */
|
|
2
|
+
(()=>{'use strict';
|
|
3
|
+
let pid=null, busy=false;
|
|
4
|
+
const button=document.getElementById('v076-opc-run'),state=document.getElementById('v076-opc-status');
|
|
5
|
+
if(!button||!state)return;
|
|
6
|
+
document.addEventListener('devagent:project-built',event=>{
|
|
7
|
+
const value=event.detail?.project_id;
|
|
8
|
+
if(!/^[a-z0-9-]{1,100}$/.test(value||''))return;
|
|
9
|
+
pid=value;button.classList.remove('hidden');state.classList.add('hidden');
|
|
10
|
+
});
|
|
11
|
+
document.addEventListener('devagent:project-reset',()=>{
|
|
12
|
+
pid=null;busy=false;button.disabled=false;button.classList.add('hidden');state.classList.add('hidden');state.replaceChildren();
|
|
13
|
+
});
|
|
14
|
+
button.addEventListener('click',async()=>{
|
|
15
|
+
if(!pid||busy)return;
|
|
16
|
+
const confirmation=window.confirm('Run fake OPC UA on 127.0.0.1 only? This is synthetic protocol testing, NOT Ignition/PLC FAT.');
|
|
17
|
+
if(!confirmation)return;
|
|
18
|
+
busy=true;button.disabled=true;state.classList.remove('hidden');
|
|
19
|
+
state.textContent='Starting ephemeral loopback fake OPC UA and independent TCP readback…';
|
|
20
|
+
try{
|
|
21
|
+
const controller=new AbortController(),timeout=setTimeout(()=>controller.abort(),55000);
|
|
22
|
+
let response;
|
|
23
|
+
try{response=await fetch('/api/v076-opc/'+encodeURIComponent(pid)+'/run',{
|
|
24
|
+
method:'POST',headers:{'Content-Type':'application/json','X-DevAgent-Request':'isolated-opc-fat-v076'},
|
|
25
|
+
body:JSON.stringify({confirm:'ISOLATED_FAKE_OPC_LOOPBACK_ONLY'}),signal:controller.signal});}
|
|
26
|
+
finally{clearTimeout(timeout);}
|
|
27
|
+
const result=await response.json();
|
|
28
|
+
if(response.ok&&result.status==='PASS_LAB_OPC_PROTOCOL_ONLY'){
|
|
29
|
+
state.textContent='Synthetic OPC UA protocol: '+result.cases_passed+'/'+result.cases_planned+
|
|
30
|
+
' readbacks PASS. Ignition HMI FAT: NOT VERIFIED. Production: HOLD. ';
|
|
31
|
+
if(result.report_url){const link=document.createElement('a');link.href=result.report_url;
|
|
32
|
+
link.target='_blank';link.rel='noopener';link.textContent='Open lab evidence ↗';state.append(link);}
|
|
33
|
+
}else{
|
|
34
|
+
state.textContent='HOLD '+(result.reason||result.status||'UNKNOWN')+
|
|
35
|
+
'. For a real fake OPC test install optional asyncua in the app Python environment. No pass was recorded.';
|
|
36
|
+
}
|
|
37
|
+
}catch(e){state.textContent='HOLD: fake OPC UA protocol test unavailable. No FAT PASS.';}
|
|
38
|
+
finally{busy=false;button.disabled=false;}
|
|
39
|
+
});
|
|
40
|
+
})();
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/* V078 device command functional FAT: ONLY ephemeral loopback virtual PLC. */
|
|
2
|
+
(()=>{'use strict';
|
|
3
|
+
let pid=null,busy=false;
|
|
4
|
+
const button=document.getElementById('v078-command-run');
|
|
5
|
+
const state=document.getElementById('v078-command-status');
|
|
6
|
+
if(!button||!state)return;
|
|
7
|
+
document.addEventListener('devagent:project-built',event=>{
|
|
8
|
+
const value=event.detail?.project_id;
|
|
9
|
+
if(!/^[a-z0-9-]{1,100}$/.test(value||''))return;
|
|
10
|
+
pid=value;button.classList.remove('hidden');state.classList.add('hidden');
|
|
11
|
+
});
|
|
12
|
+
document.addEventListener('devagent:project-reset',()=>{
|
|
13
|
+
pid=null;busy=false;button.disabled=false;button.classList.add('hidden');state.classList.add('hidden');state.replaceChildren();
|
|
14
|
+
});
|
|
15
|
+
button.addEventListener('click',async()=>{
|
|
16
|
+
if(!pid||busy)return;
|
|
17
|
+
const asset=window.prompt('LAB ONLY: Equipment ID to test (e.g., CV101). START/STOP/RESET will target a temporary Virtual PLC on 127.0.0.1, never production.','CV101');
|
|
18
|
+
if(!asset)return;
|
|
19
|
+
if(!/^[A-Za-z][A-Za-z0-9_]{1,59}$/.test(asset)){
|
|
20
|
+
state.classList.remove('hidden');state.textContent='HOLD: invalid equipment ID.';return;
|
|
21
|
+
}
|
|
22
|
+
const reviewer=window.prompt('Lab reviewer name (recorded for test only; not proof of engineering authorization):','');
|
|
23
|
+
if(!reviewer||reviewer.trim().length<3)return;
|
|
24
|
+
if(!window.confirm('Confirm isolated Virtual PLC only? No Ignition Gateway or real PLC will be contacted.'))return;
|
|
25
|
+
busy=true;button.disabled=true;state.classList.remove('hidden');
|
|
26
|
+
state.textContent='Running START / STOP / RESET, fault and permissive tests over isolated OPC UA TCP…';
|
|
27
|
+
try{
|
|
28
|
+
const controller=new AbortController(),timer=setTimeout(()=>controller.abort(),50000);
|
|
29
|
+
let response;
|
|
30
|
+
try{response=await fetch('/api/v078-command/'+encodeURIComponent(pid)+'/run',{
|
|
31
|
+
method:'POST',headers:{'Content-Type':'application/json','X-DevAgent-Request':'isolated-device-command-v078'},
|
|
32
|
+
body:JSON.stringify({asset_id:asset,reviewer:reviewer.trim(),confirm:'ISOLATED_VIRTUAL_PLC_LOOPBACK_ONLY'}),
|
|
33
|
+
signal:controller.signal});}finally{clearTimeout(timer);}
|
|
34
|
+
const result=await response.json();
|
|
35
|
+
if(response.ok&&result.status==='PASS_ISOLATED_VIRTUAL_PLC_COMMAND_FAT_ONLY'){
|
|
36
|
+
state.textContent='Virtual PLC: '+result.cases_checked+' functional checks PASS. Ignition: NOT VERIFIED. Production: HOLD. ';
|
|
37
|
+
if(result.report_url){const link=document.createElement('a');link.href=result.report_url;
|
|
38
|
+
link.target='_blank';link.rel='noopener';link.textContent='Open command FAT evidence ↗';state.append(link);}
|
|
39
|
+
}else{state.textContent='HOLD: '+(result.reason||result.status||'COMMAND_TEST_FAILED')+
|
|
40
|
+
'. Install asyncua in the Creator Python environment if missing. No production claims.';}
|
|
41
|
+
}catch(e){state.textContent='HOLD: Virtual PLC OPC UA FAT did not complete. No PASS recorded.';}
|
|
42
|
+
finally{busy=false;button.disabled=false;}
|
|
43
|
+
});
|
|
44
|
+
})();
|