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,367 @@
|
|
|
1
|
+
// DevAgent V1D representative demonstration layout.
|
|
2
|
+
// Equipment identities and connectivity follow the V1C example; positions are NOT as-built.
|
|
3
|
+
const F1=0.95,F2=6.75,F3=12.55;
|
|
4
|
+
const pt=(x,y,z)=>[x,y,z];
|
|
5
|
+
const line=(a,b)=>[a,b];
|
|
6
|
+
const dev=(id,floor,category,path,up=[],down=[],options={})=>({id,floor,category,path,up,down,...options});
|
|
7
|
+
const p1=(x,z)=>pt(x,F1,z),p2=(x,z)=>pt(x,F2,z),p3=(x,z)=>pt(x,F3,z);
|
|
8
|
+
// Conceptual modular incline: long graded runs joined by powered curve modules.
|
|
9
|
+
// A direct 4.8-unit horizontal rise to Floor 2 would be far too steep for parcels.
|
|
10
|
+
// Rounded in plan, with elevation distributed by true horizontal travel distance.
|
|
11
|
+
const INCLINE_PLAN = [
|
|
12
|
+
[4.2,0],[8.8,0],[11.8,-1.2],[11.8,-3.5],[9.4,-4.55],
|
|
13
|
+
[2.8,-4.55],[1.1,-3.0],[1.1,2.4],[2.6,4.4],
|
|
14
|
+
[8.2,4.4],[10.1,2.5],[10.15,1.2],[10.05,0],[9,0]
|
|
15
|
+
];
|
|
16
|
+
// C1 endpoint tangents for mechanical handoffs. These are representative geometry,
|
|
17
|
+
// NOT as-built vendor-approved conveyor designs. No point moves at the interfaces.
|
|
18
|
+
export function cubicPath(a,b,c,d,steps=28){
|
|
19
|
+
if(steps<2)throw Error('invalid cubic steps');
|
|
20
|
+
const out=[];
|
|
21
|
+
for(let i=0;i<=steps;i++){
|
|
22
|
+
const t=i/steps,u=1-t;
|
|
23
|
+
out.push(a.map((_,axis)=>u*u*u*a[axis]+3*u*u*t*b[axis]+3*u*t*t*c[axis]+t*t*t*d[axis]));
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
export function smoothMergeArm(z){
|
|
28
|
+
return cubicPath(p1(-21,z),p1(-18,z),p1(-16,0),p1(-13.3,0));
|
|
29
|
+
}
|
|
30
|
+
export function smoothSorterArm(z){
|
|
31
|
+
const mid=p2(-1.2,z);
|
|
32
|
+
return [...cubicPath(p2(1.15,0),p2(.1,0),p2(.3,z),mid,20).slice(0,-1),
|
|
33
|
+
...cubicPath(mid,p2(-2.5,z),p2(-2.5,0),p2(-3.65,0),20)];
|
|
34
|
+
}
|
|
35
|
+
export function smoothDestinationArm(z){
|
|
36
|
+
return cubicPath(p3(-2.3,0),p3(-.9,0),p3(.7,z),p3(2.3,z));
|
|
37
|
+
}
|
|
38
|
+
export function smoothChute(start,end){
|
|
39
|
+
// Both tangents point along +X: no cross-belt panel or sudden 90-degree turn.
|
|
40
|
+
const dx=Math.max(.3,end[0]-start[0]);
|
|
41
|
+
const c1=pt(start[0]+dx*.28,start[1],start[2]);
|
|
42
|
+
const c2=pt(end[0]-dx*.28,end[1],end[2]);
|
|
43
|
+
return cubicPath(start,c1,c2,end,32);
|
|
44
|
+
}
|
|
45
|
+
export function endpointTangent(e,kind){
|
|
46
|
+
const a=kind==='start'?e.path[0]:e.path.at(-2);
|
|
47
|
+
const b=kind==='start'?e.path[1]:e.path.at(-1);
|
|
48
|
+
const x=b[0]-a[0],y=b[1]-a[1],z=b[2]-a[2];
|
|
49
|
+
const l=Math.hypot(x,y,z);return l?[x/l,y/l,z/l]:[0,0,0];
|
|
50
|
+
}
|
|
51
|
+
export function tangentAlignment(a,b){
|
|
52
|
+
const v=endpointTangent(a,'end'),w=endpointTangent(b,'start');
|
|
53
|
+
return v.reduce((sum,n,i)=>sum+n*w[i],0);
|
|
54
|
+
}
|
|
55
|
+
// Gradual belt elevation transitions: flatten pitch at both physical
|
|
56
|
+
// handoffs; avoid instantaneous horizontal-to-incline changes.
|
|
57
|
+
export function easeGradeAtEnds(t,shoulder=.1){
|
|
58
|
+
if(!(t>=0&&t<=1)||!(shoulder>0&&shoulder<.5))throw Error('invalid eased grade');
|
|
59
|
+
const total=1-shoulder;
|
|
60
|
+
if(t<shoulder)return (t*t/(2*shoulder))/total;
|
|
61
|
+
if(t>1-shoulder)return (total-(1-t)*(1-t)/(2*shoulder))/total;
|
|
62
|
+
return (t-shoulder/2)/total;
|
|
63
|
+
}
|
|
64
|
+
export function makeGradedIncline(plan,low=F1,high=F2){
|
|
65
|
+
if(!Array.isArray(plan)||plan.length<2||!(high>low))throw Error('Invalid incline profile');
|
|
66
|
+
let path=plan.map(p=>[...p]);
|
|
67
|
+
for(let iter=0;iter<3;iter++){
|
|
68
|
+
const out=[path[0]];
|
|
69
|
+
for(let i=0;i<path.length-1;i++){
|
|
70
|
+
const a=path[i],b=path[i+1];
|
|
71
|
+
out.push([.75*a[0]+.25*b[0],.75*a[1]+.25*b[1]],
|
|
72
|
+
[.25*a[0]+.75*b[0],.25*a[1]+.75*b[1]]);
|
|
73
|
+
}
|
|
74
|
+
path=[...out,path[path.length-1]];
|
|
75
|
+
}
|
|
76
|
+
let horizontal=0;
|
|
77
|
+
const distance=[0];
|
|
78
|
+
for(let i=1;i<path.length;i++){
|
|
79
|
+
const d=Math.hypot(path[i][0]-path[i-1][0],path[i][1]-path[i-1][1]);
|
|
80
|
+
horizontal+=d;distance.push(horizontal);
|
|
81
|
+
}
|
|
82
|
+
if(horizontal<=0)throw Error('Incline horizontal distance must be positive');
|
|
83
|
+
return path.map((p,i)=>pt(p[0],low+(high-low)*easeGradeAtEnds(distance[i]/horizontal),p[1]));
|
|
84
|
+
}
|
|
85
|
+
export function inclineStats(path){
|
|
86
|
+
let run=0,maxAngle=0,minRise=Infinity;
|
|
87
|
+
for(let i=1;i<path.length;i++){
|
|
88
|
+
const prev=path[i-1],cur=path[i];
|
|
89
|
+
const h=Math.hypot(cur[0]-prev[0],cur[2]-prev[2]);
|
|
90
|
+
const rise=cur[1]-prev[1];
|
|
91
|
+
if(h<1e-9)continue;
|
|
92
|
+
run+=h;maxAngle=Math.max(maxAngle,Math.atan2(Math.abs(rise),h)*180/Math.PI);
|
|
93
|
+
minRise=Math.min(minRise,rise);
|
|
94
|
+
}
|
|
95
|
+
const rise=path.at(-1)[1]-path[0][1];
|
|
96
|
+
return {horizontalRun:run,rise,averageAngle:Math.atan2(rise,run)*180/Math.PI,
|
|
97
|
+
maxSegmentAngle:maxAngle,minSegmentRise:minRise};
|
|
98
|
+
}
|
|
99
|
+
const incline=makeGradedIncline(INCLINE_PLAN);
|
|
100
|
+
// Public engineering metadata imported by the 3D equipment inspector.
|
|
101
|
+
// Derived from the same sampled polyline the scene uses, not a hardcoded value.
|
|
102
|
+
export const INCLINE_ENGINEERING_DEMO = Object.freeze(inclineStats(incline));
|
|
103
|
+
// Two-and-a-half-turn powered ascent: inlet tangent -X, outlet tangent +X.
|
|
104
|
+
// A smooth centerline drift makes both physical ports connect at x=-9.2,z=0,
|
|
105
|
+
// without reversing travel direction, stopping parcels, or a tangent cusp.
|
|
106
|
+
const spiral=[];
|
|
107
|
+
for(let i=0;i<=240;i++){
|
|
108
|
+
const t=i/240,theta=Math.PI/2+5*Math.PI*t;
|
|
109
|
+
const drift=(3*t*t-2*t*t*t)*3.8;
|
|
110
|
+
spiral.push(pt(-9.2+1.9*Math.cos(theta),F2+(F3-F2)*easeGradeAtEnds(t),
|
|
111
|
+
-1.9+drift+1.9*Math.sin(theta)));
|
|
112
|
+
}
|
|
113
|
+
// Representative outbound: a destination chute feeds one powered spiral decline
|
|
114
|
+
// to a ground-level shipping door. Four turns with 2.4 scene-unit radius keep
|
|
115
|
+
// grade near 10.5 deg (concept only, NOT a vendor-certified installation).
|
|
116
|
+
export function makeDeclineSpiral(z, high=F3-0.5, low=F1, radius=2.4, turns=4){
|
|
117
|
+
if(!(high>low)||!(radius>0)||!(turns>=1))throw Error('invalid spiral');
|
|
118
|
+
// Both ends have a +X tangent, matching chute and shipping dock belt direction.
|
|
119
|
+
const n=turns*96,arr=[];
|
|
120
|
+
for(let i=0;i<=n;i++){
|
|
121
|
+
const t=i/n,theta=-Math.PI/2+turns*2*Math.PI*t;
|
|
122
|
+
arr.push(pt(17+radius*Math.cos(theta),high+(low-high)*easeGradeAtEnds(t),
|
|
123
|
+
z+radius+radius*Math.sin(theta)));
|
|
124
|
+
}
|
|
125
|
+
return arr;
|
|
126
|
+
}
|
|
127
|
+
// R6 concept recirculation: an additional discharge from the EXISTING floor-3
|
|
128
|
+
// destination sorter lands on a floor-2 powered spiral and takeaway belt, then
|
|
129
|
+
// joins the EXISTING SORT201 induction upstream of MERGE201. No original centerlines or IDs are displaced.
|
|
130
|
+
// Designed for a single optional re-sort pass, not an endless loop.
|
|
131
|
+
const RECIRC_F3_CHUTE=cubicPath(p3(-2.3,0),p3(-.9,0),pt(2.8,F3-1.0,-8),pt(4,F3-1.0,-8),40);
|
|
132
|
+
export function recircDecline(upper=F3-1.0,lower=F2,radius=2.15,turns=3){
|
|
133
|
+
if(!(upper>lower&&radius>0&&Number.isInteger(turns)&&turns>=2))throw Error('invalid recirc decline');
|
|
134
|
+
const points=[];
|
|
135
|
+
for(let i=0;i<=turns*96;i++){
|
|
136
|
+
const t=i/(turns*96),angle=-Math.PI/2+turns*2*Math.PI*t;
|
|
137
|
+
points.push(pt(4+radius*Math.cos(angle),upper+(lower-upper)*easeGradeAtEnds(t),
|
|
138
|
+
-8+radius+radius*Math.sin(angle)));
|
|
139
|
+
}
|
|
140
|
+
return points;
|
|
141
|
+
}
|
|
142
|
+
const RECIRC_DOWN=recircDecline();
|
|
143
|
+
const RECIRC_TAKEAWAY=cubicPath(p2(4,-8),p2(8,-8),p2(3.8,0),p2(2.55,0),48);
|
|
144
|
+
export const R6_ADDED_IDS=Object.freeze(['R6_CHUTE_F2','R6_DOWN_F2','R6_TAKEAWAY_F2']);
|
|
145
|
+
export const BASE_V1C_IDS=Object.freeze([
|
|
146
|
+
'MERGE_F1','INCL01','CV201','CV202','CV203','SORT201','CV204','CV205',
|
|
147
|
+
'CV206','MERGE201','CV207','CV208','SPIRAL01','CV301','CV302','SORT301',
|
|
148
|
+
'CV303','CV304','CV305','CV306','CV307','CV308'
|
|
149
|
+
]);
|
|
150
|
+
// R9 concept: compact FOUR-OUTLET sliding-shoe sorter on Floor 3.
|
|
151
|
+
// Each chute feeds its OWN powered, independently modeled 7-turn spiral to Floor 2.
|
|
152
|
+
// Chute connections and spiral bottom ports are contiguous; only after descent do
|
|
153
|
+
// four distinct takeaway belts merge onto a floor-2 collector and secondary sorter.
|
|
154
|
+
// Scene units are illustrative: NOT a vendor or physical installation design.
|
|
155
|
+
const R9_Z_SORT=5.1, R9_Z_SPIRAL=6.6, R9_Z_COLLECT=4.5;
|
|
156
|
+
const R9_Z_HIGH=F3-.30, R9_START_X=-15.8,R9_SPACING=2.65;
|
|
157
|
+
const R9_RADIUS=.80, R9_SPIRAL_TURNS=7;
|
|
158
|
+
const R9_STATIONS=Array.from({length:4},(_,i)=>`R9_SORT_${String(i+1).padStart(2,'0')}`);
|
|
159
|
+
const R9_CHUTES=Array.from({length:4},(_,i)=>`R9_CHUTE_${String(i+1).padStart(2,'0')}`);
|
|
160
|
+
const R9_SPIRALS=Array.from({length:4},(_,i)=>`R9_SPIRAL_${String(i+1).padStart(2,'0')}`);
|
|
161
|
+
const R9_TAKEAWAYS=Array.from({length:4},(_,i)=>`R9_TAKEAWAY_${String(i+1).padStart(2,'0')}`);
|
|
162
|
+
const R9_JOINS=Array.from({length:4},(_,i)=>`R9_JOIN_${String(i+1).padStart(2,'0')}`);
|
|
163
|
+
const R9_COLLECT=Array.from({length:3},(_,i)=>`R9_COLLECT_${String(i+1).padStart(2,'0')}`);
|
|
164
|
+
const R9_AT=i=>R9_START_X+(i+1)*R9_SPACING;
|
|
165
|
+
const R9_X=i=>R9_AT(i)+.50;
|
|
166
|
+
const R9_COL_END=i=>R9_X(i)+.25;
|
|
167
|
+
const R9_END=R9_COL_END(3),R9_SECOND_END=R9_END+2.10;
|
|
168
|
+
const R9_SECOND_MERGE_X=R9_SECOND_END+2.60;
|
|
169
|
+
function r9Chute(i){
|
|
170
|
+
const x=R9_AT(i),end=pt(R9_X(i),R9_Z_HIGH,R9_Z_SPIRAL);
|
|
171
|
+
return cubicPath(p3(x,R9_Z_SORT),p3(x+.33,R9_Z_SORT),
|
|
172
|
+
pt(end[0]-.25,R9_Z_HIGH,end[2]),end,48);
|
|
173
|
+
}
|
|
174
|
+
function r9Spiral(i){
|
|
175
|
+
const n=R9_SPIRAL_TURNS*96;
|
|
176
|
+
return Array.from({length:n+1},(_,j)=>{
|
|
177
|
+
const t=j/n,theta=-Math.PI/2+R9_SPIRAL_TURNS*2*Math.PI*t;
|
|
178
|
+
return pt(R9_X(i)+R9_RADIUS*Math.cos(theta),
|
|
179
|
+
R9_Z_HIGH+(F2-R9_Z_HIGH)*easeGradeAtEnds(t),
|
|
180
|
+
R9_Z_SPIRAL+R9_RADIUS+R9_RADIUS*Math.sin(theta));
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
function r9Takeaway(i){
|
|
184
|
+
const x=R9_X(i);
|
|
185
|
+
return cubicPath(p2(x,R9_Z_SPIRAL),p2(x+.3,R9_Z_SPIRAL),
|
|
186
|
+
p2(x+.3,R9_Z_COLLECT),p2(x,R9_Z_COLLECT),48);
|
|
187
|
+
}
|
|
188
|
+
function r9SecondaryExit(offset){
|
|
189
|
+
const a=p2(R9_SECOND_END,R9_Z_COLLECT),d=p2(R9_SECOND_MERGE_X,R9_Z_COLLECT);
|
|
190
|
+
const m=p2((R9_SECOND_END+R9_SECOND_MERGE_X)*.5,R9_Z_COLLECT+offset);
|
|
191
|
+
return [...cubicPath(a,p2(R9_SECOND_END+.45,R9_Z_COLLECT),
|
|
192
|
+
p2(m[0]-.45,m[2]),m,24).slice(0,-1),
|
|
193
|
+
...cubicPath(m,p2(m[0]+.45,m[2]),
|
|
194
|
+
p2(R9_SECOND_MERGE_X-.45,R9_Z_COLLECT),d,24)];
|
|
195
|
+
}
|
|
196
|
+
function r9Return(){
|
|
197
|
+
// Curved, in-footprint east-edge return avoids crossing any R9 spiral
|
|
198
|
+
// centerline. Physically screened merging is still pending vendor review.
|
|
199
|
+
const a=p2(R9_SECOND_MERGE_X,R9_Z_COLLECT), east=p2(12,5.0);
|
|
200
|
+
const south=p2(12,-8), end=p2(4,-8);
|
|
201
|
+
return [...cubicPath(a,p2(3.8,R9_Z_COLLECT),p2(7.5,5.0),east,45).slice(0,-1),
|
|
202
|
+
...cubicPath(east,p2(15,5.0),p2(15,-8),south,75).slice(0,-1),
|
|
203
|
+
...line(south,end)];
|
|
204
|
+
}
|
|
205
|
+
export const R9_ADDED_IDS=Object.freeze([
|
|
206
|
+
'R9_INFEED',...R9_STATIONS,...R9_CHUTES,...R9_SPIRALS,...R9_TAKEAWAYS,
|
|
207
|
+
...R9_JOINS,...R9_COLLECT,'R9_SECOND_SORTER',
|
|
208
|
+
'R9_SECOND_PASS','R9_SECOND_REWORK','R9_RETURN_F2'
|
|
209
|
+
]);
|
|
210
|
+
export const R9_STATION_IDS=Object.freeze([...R9_STATIONS]);
|
|
211
|
+
export const R9_CHUTE_IDS=Object.freeze([...R9_CHUTES]);
|
|
212
|
+
export const R9_SPIRAL_IDS=Object.freeze([...R9_SPIRALS]);
|
|
213
|
+
export const R9_TAKEAWAY_IDS=Object.freeze([...R9_TAKEAWAYS]);
|
|
214
|
+
export const R9_COLLECT_IDS=Object.freeze([...R9_COLLECT]);
|
|
215
|
+
const R9_EQUIPMENT=[
|
|
216
|
+
dev('R9_INFEED',3,'Compact sorter induction',cubicPath(
|
|
217
|
+
p3(-2.3,0),p3(-.9,0),p3(-17.1,R9_Z_SORT),p3(R9_START_X,R9_Z_SORT),80),
|
|
218
|
+
['SORT301'],[R9_STATIONS[0]],{demoOnly:true}),
|
|
219
|
+
...R9_STATIONS.map((id,i)=>dev(id,3,'Compact sliding-shoe divert station',
|
|
220
|
+
line(p3(R9_START_X+i*R9_SPACING,R9_Z_SORT),p3(R9_AT(i),R9_Z_SORT)),
|
|
221
|
+
[i?R9_STATIONS[i-1]:'R9_INFEED'],[...(i<3?[R9_STATIONS[i+1]]:[]),R9_CHUTES[i]],
|
|
222
|
+
{demoOnly:true,sorterStation:i+1})),
|
|
223
|
+
...R9_CHUTES.map((id,i)=>dev(id,3,'Powered lateral discharge chute',r9Chute(i),
|
|
224
|
+
[R9_STATIONS[i]],[R9_SPIRALS[i]],{demoOnly:true,graded:true,chute:String(i+1).padStart(2,'0')})),
|
|
225
|
+
...R9_SPIRALS.map((id,i)=>dev(id,3,'Independent powered 7-turn spiral decline',r9Spiral(i),
|
|
226
|
+
[R9_CHUTES[i]],[R9_TAKEAWAYS[i]],
|
|
227
|
+
{demoOnly:true,connector:true,toFloor:2,graded:true,spiralStation:i+1})),
|
|
228
|
+
...R9_TAKEAWAYS.map((id,i)=>dev(id,2,'Individual Floor 02 powered takeaway',r9Takeaway(i),
|
|
229
|
+
[R9_SPIRALS[i]],[R9_JOINS[i]],{demoOnly:true})),
|
|
230
|
+
...R9_JOINS.map((id,i)=>dev(id,2,'Floor 02 merge admission',
|
|
231
|
+
line(p2(R9_X(i),R9_Z_COLLECT),p2(R9_COL_END(i),R9_Z_COLLECT)),
|
|
232
|
+
[R9_TAKEAWAYS[i],...(i?[R9_COLLECT[i-1]]:[])],
|
|
233
|
+
[i<3?R9_COLLECT[i]:'R9_SECOND_SORTER'],{demoOnly:true,mergeConcept:true})),
|
|
234
|
+
...R9_COLLECT.map((id,i)=>dev(id,2,'Powered Floor 02 collection belt',
|
|
235
|
+
line(p2(R9_COL_END(i),R9_Z_COLLECT),p2(R9_X(i+1),R9_Z_COLLECT)),
|
|
236
|
+
[R9_JOINS[i]],[R9_JOINS[i+1]],{demoOnly:true})),
|
|
237
|
+
dev('R9_SECOND_SORTER',2,'Compact secondary parcel sorter',
|
|
238
|
+
line(p2(R9_END,R9_Z_COLLECT),p2(R9_SECOND_END,R9_Z_COLLECT)),
|
|
239
|
+
[R9_JOINS[3]],['R9_SECOND_PASS','R9_SECOND_REWORK'],{demoOnly:true}),
|
|
240
|
+
dev('R9_SECOND_PASS',2,'Secondary pass routing',r9SecondaryExit(-.35),
|
|
241
|
+
['R9_SECOND_SORTER'],['R9_RETURN_F2'],{demoOnly:true}),
|
|
242
|
+
dev('R9_SECOND_REWORK',2,'Secondary rework routing',r9SecondaryExit(.35),
|
|
243
|
+
['R9_SECOND_SORTER'],['R9_RETURN_F2'],{demoOnly:true}),
|
|
244
|
+
dev('R9_RETURN_F2',2,'In-floor edge return to original induction',r9Return(),
|
|
245
|
+
['R9_SECOND_PASS','R9_SECOND_REWORK'],['R6_TAKEAWAY_F2'],{demoOnly:true,mergeConcept:true})
|
|
246
|
+
];
|
|
247
|
+
export const EQUIPMENT=[
|
|
248
|
+
dev('DOCK_IN_A',1,'Receiving dock',smoothMergeArm(-7),[],['MERGE_F1'],{demoOnly:true,dock:'INBOUND 01'}),
|
|
249
|
+
dev('DOCK_IN_B',1,'Receiving dock',smoothMergeArm(7),[],['MERGE_F1'],{demoOnly:true,dock:'INBOUND 02'}),
|
|
250
|
+
dev('MERGE_F1',1,'Merge',line(p1(-13.3,0),p1(4.2,0)),['DOCK_IN_A','DOCK_IN_B'],['INCL01']),
|
|
251
|
+
dev('INCL01',1,'Incline',incline,['MERGE_F1'],['CV201'],{connector:true,toFloor:2,graded:true}),
|
|
252
|
+
dev('CV201',2,'Conveyor',line(p2(9.0,0),p2(6.5,0)),['INCL01'],['CV202']),
|
|
253
|
+
dev('CV202',2,'Conveyor',line(p2(6.5,0),p2(4.65,0)),['CV201'],['CV203']),
|
|
254
|
+
dev('CV203',2,'Conveyor',line(p2(4.65,0),p2(2.55,0)),['CV202'],['SORT201']),
|
|
255
|
+
dev('SORT201',2,'Sorter',line(p2(2.55,0),p2(1.15,0)),['CV203','R6_TAKEAWAY_F2'],['CV204','CV205','CV206']),
|
|
256
|
+
dev('CV204',2,'Divert',smoothSorterArm(-2.5),['SORT201'],['MERGE201']),
|
|
257
|
+
dev('CV205',2,'Conveyor',line(p2(1.15,0),p2(-3.65,0)),['SORT201'],['MERGE201']),
|
|
258
|
+
dev('CV206',2,'Divert',smoothSorterArm(2.5),['SORT201'],['MERGE201']),
|
|
259
|
+
dev('MERGE201',2,'Merge',line(p2(-3.65,0),p2(-4.6,0)),['CV204','CV205','CV206'],['CV207']),
|
|
260
|
+
dev('CV207',2,'Conveyor',line(p2(-4.6,0),p2(-6.8,0)),['MERGE201'],['CV208']),
|
|
261
|
+
dev('CV208',2,'Conveyor',line(p2(-6.8,0),p2(-9.2,0)),['CV207'],['SPIRAL01']),
|
|
262
|
+
dev('SPIRAL01',2,'Powered spiral ascent',spiral,['CV208'],['CV301'],{connector:true,toFloor:3,graded:true}),
|
|
263
|
+
dev('CV301',3,'Conveyor',line(p3(-9.2,0),p3(-6.4,0)),['SPIRAL01'],['CV302']),
|
|
264
|
+
dev('CV302',3,'Conveyor',line(p3(-6.4,0),p3(-4.0,0)),['CV301'],['SORT301']),
|
|
265
|
+
dev('SORT301',3,'Destination sorter',line(p3(-4.0,0),p3(-2.3,0)),['CV302'],['CV303','CV304','R6_CHUTE_F2','R9_INFEED']),
|
|
266
|
+
dev('CV303',3,'Sort divert',smoothDestinationArm(-2.45),['SORT301'],['CV305']),
|
|
267
|
+
dev('CV304',3,'Sort divert',smoothDestinationArm(2.45),['SORT301'],['CV306']),
|
|
268
|
+
dev('CV305',3,'Conveyor',line(p3(2.3,-2.45),p3(6.15,-2.45)),['CV303'],['CV307']),
|
|
269
|
+
dev('CV306',3,'Conveyor',line(p3(2.3,2.45),p3(6.15,2.45)),['CV304'],['CV308']),
|
|
270
|
+
dev('CV307',3,'Conveyor',line(p3(6.15,-2.45),p3(10.35,-2.45)),['CV305'],['CHUTE_A']),
|
|
271
|
+
dev('CV308',3,'Conveyor',line(p3(6.15,2.45),p3(10.35,2.45)),['CV306'],['DIVERT_D3']),
|
|
272
|
+
dev('DIVERT_D3',3,'Destination diverter',line(p3(10.35,2.45),p3(12,2.45)),['CV308'],['CHUTE_B','CHUTE_C'],{demoOnly:true}),
|
|
273
|
+
// Added concept chute + powered cross-floor decline + F2 takeaway. No PLC actuation.
|
|
274
|
+
dev('R6_CHUTE_F2',3,'Recirculation discharge chute',RECIRC_F3_CHUTE,['SORT301'],['R6_DOWN_F2'],{demoOnly:true,graded:true,chute:'F2'}),
|
|
275
|
+
dev('R6_DOWN_F2',3,'Powered spiral decline to floor 02',RECIRC_DOWN,['R6_CHUTE_F2'],['R6_TAKEAWAY_F2'],{demoOnly:true,connector:true,toFloor:2,graded:true}),
|
|
276
|
+
dev('R6_TAKEAWAY_F2',2,'Floor 02 takeaway to sorter induction',RECIRC_TAKEAWAY,['R6_DOWN_F2','R9_RETURN_F2'],['SORT201'],{demoOnly:true}),
|
|
277
|
+
dev('CHUTE_A',3,'Guarded chute',smoothChute(p3(10.35,-2.45),pt(17,F3-.5,-7)),['CV307'],['DOWN_A'],{demoOnly:true,graded:true,chute:'01'}),
|
|
278
|
+
dev('CHUTE_B',3,'Guarded chute',smoothChute(p3(12,2.45),pt(17,F3-.5,0)),['DIVERT_D3'],['DOWN_B'],{demoOnly:true,graded:true,chute:'02'}),
|
|
279
|
+
dev('CHUTE_C',3,'Guarded chute',smoothChute(p3(12,2.45),pt(17,F3-.5,7)),['DIVERT_D3'],['DOWN_C'],{demoOnly:true,graded:true,chute:'03'}),
|
|
280
|
+
dev('DOWN_A',3,'Powered spiral decline',makeDeclineSpiral(-7),['CHUTE_A'],['DOCK_OUT_A'],{demoOnly:true,connector:true,toFloor:1,graded:true}),
|
|
281
|
+
dev('DOWN_B',3,'Powered spiral decline',makeDeclineSpiral(0),['CHUTE_B'],['DOCK_OUT_B'],{demoOnly:true,connector:true,toFloor:1,graded:true}),
|
|
282
|
+
dev('DOWN_C',3,'Powered spiral decline',makeDeclineSpiral(7),['CHUTE_C'],['DOCK_OUT_C'],{demoOnly:true,connector:true,toFloor:1,graded:true}),
|
|
283
|
+
dev('DOCK_OUT_A',1,'Shipping dock',line(p1(17,-7),p1(24.0,-7)),['DOWN_A'],[],{demoOnly:true,dock:'OUTBOUND 01'}),
|
|
284
|
+
dev('DOCK_OUT_B',1,'Shipping dock',line(p1(17,0),p1(24.0,0)),['DOWN_B'],[],{demoOnly:true,dock:'OUTBOUND 02'}),
|
|
285
|
+
dev('DOCK_OUT_C',1,'Shipping dock',line(p1(17,7),p1(24.0,7)),['DOWN_C'],[],{demoOnly:true,dock:'OUTBOUND 03'}),
|
|
286
|
+
...R9_EQUIPMENT,
|
|
287
|
+
];
|
|
288
|
+
export const EQUIPMENT_BY_ID=Object.fromEntries(EQUIPMENT.map(e=>[e.id,e]));
|
|
289
|
+
export const FLOORS={1:{name:'INBOUND + SHIPPING DOCKS',elevation:F1,tint:0x203b4e},2:{name:'INDUCTION + SORTATION',elevation:F2,tint:0x124963},3:{name:'DESTINATION SORTER + CHUTES',elevation:F3,tint:0x15565b}};
|
|
290
|
+
export const DESTINATIONS=['A','B','C'];
|
|
291
|
+
export const ROUTES=Array.from({length:6},(_,i)=>[
|
|
292
|
+
i%2?'DOCK_IN_B':'DOCK_IN_A','MERGE_F1','INCL01',
|
|
293
|
+
'CV201','CV202','CV203','SORT201',['CV204','CV205','CV206'][i%3],
|
|
294
|
+
'MERGE201','CV207','CV208','SPIRAL01','CV301','CV302','SORT301',
|
|
295
|
+
i%3===0?'CV303':'CV304',i%3===0?'CV305':'CV306',i%3===0?'CV307':'CV308',
|
|
296
|
+
...(i%3===0?[]:['DIVERT_D3']),
|
|
297
|
+
'CHUTE_'+DESTINATIONS[i%3],'DOWN_'+DESTINATIONS[i%3],'DOCK_OUT_'+DESTINATIONS[i%3]
|
|
298
|
+
]);
|
|
299
|
+
// Exactly one optional re-sort: Floor 03 -> Floor 02 -> original spiral
|
|
300
|
+
// ascent -> Floor 03 -> an existing destination chute. Every route terminates.
|
|
301
|
+
const R6_RECIRC_ROUTE=[...ROUTES[0].slice(0,ROUTES[0].indexOf('SORT301')+1),
|
|
302
|
+
'R6_CHUTE_F2','R6_DOWN_F2','R6_TAKEAWAY_F2',
|
|
303
|
+
'SORT201','CV204','MERGE201','CV207','CV208','SPIRAL01','CV301','CV302','SORT301',
|
|
304
|
+
...ROUTES[0].slice(ROUTES[0].indexOf('SORT301')+1)];
|
|
305
|
+
ROUTES.push(R6_RECIRC_ROUTE);
|
|
306
|
+
// Four independently routed chute + spiral descents, followed by a
|
|
307
|
+
// metered Floor 02 collector and secondary sorter. Original delivery routes preserved.
|
|
308
|
+
export const R9_ROUTES=Array.from({length:4},(_,i)=>{
|
|
309
|
+
const original=ROUTES[i%6];
|
|
310
|
+
const prefix=original.slice(0,original.indexOf('SORT301')+1);
|
|
311
|
+
const suffix=original.slice(original.indexOf('SORT301')+1);
|
|
312
|
+
return [...prefix,'R9_INFEED',...R9_STATIONS.slice(0,i+1),R9_CHUTES[i],
|
|
313
|
+
R9_SPIRALS[i],R9_TAKEAWAYS[i],R9_JOINS[i],
|
|
314
|
+
...R9_COLLECT.slice(i).flatMap((id,j)=>[id,R9_JOINS[i+j+1]]),
|
|
315
|
+
'R9_SECOND_SORTER',i%2?'R9_SECOND_REWORK':'R9_SECOND_PASS',
|
|
316
|
+
'R9_RETURN_F2','R6_TAKEAWAY_F2',
|
|
317
|
+
'SORT201',['CV204','CV205','CV206'][i%3],
|
|
318
|
+
'MERGE201','CV207','CV208','SPIRAL01','CV301','CV302','SORT301',...suffix];
|
|
319
|
+
});
|
|
320
|
+
ROUTES.push(...R9_ROUTES);
|
|
321
|
+
|
|
322
|
+
export function gradeStats(path){
|
|
323
|
+
let horizontal=0,minRise=Infinity,maxAngle=0;
|
|
324
|
+
for(let i=1;i<path.length;i++){
|
|
325
|
+
const a=path[i-1],b=path[i],h=Math.hypot(b[0]-a[0],b[2]-a[2]),rise=b[1]-a[1];
|
|
326
|
+
if(h<1e-7)continue;
|
|
327
|
+
horizontal+=h;maxAngle=Math.max(maxAngle,Math.atan2(Math.abs(rise),h)*180/Math.PI);
|
|
328
|
+
minRise=Math.min(minRise,rise);
|
|
329
|
+
}
|
|
330
|
+
return {horizontalRun:horizontal,rise:path.at(-1)[1]-path[0][1],maxSegmentAngle:maxAngle,minSegmentRise:minRise};
|
|
331
|
+
}
|
|
332
|
+
export const HUB_DESIGN=Object.freeze({
|
|
333
|
+
inboundDocks:2,outboundDocks:3,sortDestinations:3,
|
|
334
|
+
illustrativeScale:'Scene units — NOT surveyed meters',
|
|
335
|
+
incline:gradeStats(incline),
|
|
336
|
+
declines:DESTINATIONS.map(id=>gradeStats(EQUIPMENT_BY_ID['DOWN_'+id].path)),
|
|
337
|
+
recirculation:gradeStats(RECIRC_DOWN),
|
|
338
|
+
geometryStatus:'Concept only: equipment vendor, clearance, parcel retention, guarding, egress and throughput unqualified'
|
|
339
|
+
});
|
|
340
|
+
export function validateModel(){
|
|
341
|
+
const failures=[],seen=new Set();
|
|
342
|
+
for(const e of EQUIPMENT){
|
|
343
|
+
if(seen.has(e.id))failures.push('duplicate '+e.id);seen.add(e.id);
|
|
344
|
+
if(e.path.length<2)failures.push('missing path '+e.id);
|
|
345
|
+
for(const n of [...e.up,...e.down])if(!EQUIPMENT_BY_ID[n])failures.push('missing neighbor '+e.id+':'+n);
|
|
346
|
+
for(const n of e.down){
|
|
347
|
+
const next=EQUIPMENT_BY_ID[n];if(!next)continue;
|
|
348
|
+
if(!next.up.includes(e.id))failures.push('asymmetric '+e.id+' to '+n);
|
|
349
|
+
const a=e.path.at(-1),b=next.path[0];
|
|
350
|
+
if(Math.hypot(...a.map((x,i)=>x-b[i]))>.001)failures.push('disconnected '+e.id+' to '+n);
|
|
351
|
+
}
|
|
352
|
+
if(e.connector){
|
|
353
|
+
const grade=gradeStats(e.path);
|
|
354
|
+
if(grade.maxSegmentAngle>15.5)failures.push('too steep '+e.id+':'+grade.maxSegmentAngle.toFixed(2));
|
|
355
|
+
if(e.id==='INCL01'&&grade.minSegmentRise<-.00001)failures.push('incline reverses');
|
|
356
|
+
if(e.id.startsWith('DOWN_')&&grade.minSegmentRise>0)failures.push('decline wrong direction');
|
|
357
|
+
if(e.id==='R6_DOWN_F2'&&(grade.rise>=0||grade.minSegmentRise>0))failures.push('recirc decline must descend');
|
|
358
|
+
if(e.id.startsWith('R9_SPIRAL_')&&(grade.rise>=0||grade.minSegmentRise>0))failures.push('R9 spiral must descend');
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
for(const route of ROUTES){
|
|
362
|
+
if(!route[0].startsWith('DOCK_IN_')||!route.at(-1).startsWith('DOCK_OUT_'))failures.push('route endpoints');
|
|
363
|
+
for(let i=1;i<route.length;i++)if(!EQUIPMENT_BY_ID[route[i-1]].down.includes(route[i]))failures.push('invalid route '+route[i-1]+' to '+route[i]);
|
|
364
|
+
}
|
|
365
|
+
if(BASE_V1C_IDS.length!==22||BASE_V1C_IDS.some(id=>!EQUIPMENT_BY_ID[id]||EQUIPMENT_BY_ID[id].demoOnly))failures.push('V1C identity drift');
|
|
366
|
+
return failures;
|
|
367
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Fail-closed equipment signal reader for V1C's potentially different API contracts.
|
|
2
|
+
// Do not infer motor state from arbitrary fields or invent parcel positions.
|
|
3
|
+
import {EQUIPMENT_BY_ID} from './model.mjs';
|
|
4
|
+
const plain=x=>x!==null && typeof x==='object' && !Array.isArray(x);
|
|
5
|
+
const bool=(v)=>v===true||v===1||v==='true'||v==='TRUE'||v==='1'?true:v===false||v===0||v==='false'||v==='FALSE'||v==='0'?false:null;
|
|
6
|
+
const value=(obj,...keys)=>{for(const key of keys){if(Object.hasOwn(obj,key))return obj[key];}return undefined};
|
|
7
|
+
const stateStr=(v)=>typeof v==='string'?v.trim().toUpperCase():'';
|
|
8
|
+
export function normalizeDevice(id,row){
|
|
9
|
+
if(!EQUIPMENT_BY_ID[id]||EQUIPMENT_BY_ID[id].demoOnly||!plain(row))return null;
|
|
10
|
+
const rawRun=value(row,'run_feedback','runFeedback','running','is_running','motor_running','Running');
|
|
11
|
+
const rawFault=value(row,'fault','faulted','has_fault','alarm','jammed','Fault');
|
|
12
|
+
let running=bool(rawRun),fault=bool(rawFault),blocked=bool(value(row,'blocked','pe_blocked','jam'));
|
|
13
|
+
const status=stateStr(value(row,'status','state','equipment_status','Status'));
|
|
14
|
+
// A string status may be used ONLY when its meaning is explicit.
|
|
15
|
+
if(['RUNNING','STOPPED','OFF','ON'].includes(status)&&running===null)running=['RUNNING','ON'].includes(status);
|
|
16
|
+
if(['FAULT','FAULTED','JAMMED','ERROR'].includes(status))fault=true;
|
|
17
|
+
if(['BLOCKED','JAM'].includes(status))blocked=true;
|
|
18
|
+
const speedRaw=value(row,'speed_mps','speedMps','motor_speed_mps','motorSpeed','speed');
|
|
19
|
+
const speed=typeof speedRaw==='number'&&Number.isFinite(speedRaw)&&speedRaw>=0&&speedRaw<=30?speedRaw:null;
|
|
20
|
+
const peIn=bool(value(row,'pe_in','peIn','photoeye_in','sensor_in'));
|
|
21
|
+
const peOut=bool(value(row,'pe_out','peOut','photoeye_out','sensor_out'));
|
|
22
|
+
if(running===null&&fault===null&&blocked===null&&speed===null&&peIn===null&&peOut===null)return null;
|
|
23
|
+
return {id,running,fault,blocked,speed,peIn,peOut,quality:'V1C READ-ONLY'};
|
|
24
|
+
}
|
|
25
|
+
export function extractEquipment(payload){
|
|
26
|
+
const results={};let traversed=0;
|
|
27
|
+
function scan(node,depth){
|
|
28
|
+
if(depth>7||++traversed>2000||node===null||typeof node!=='object')return;
|
|
29
|
+
if(Array.isArray(node)){for(const child of node.slice(0,800))scan(child,depth+1);return;}
|
|
30
|
+
const id=value(node,'id','equipment_id','equipmentId','device_id','deviceId','name','asset_id','assetId');
|
|
31
|
+
if(typeof id==='string'&&EQUIPMENT_BY_ID[id.toUpperCase()]){
|
|
32
|
+
const v=normalizeDevice(id.toUpperCase(),node);
|
|
33
|
+
if(v)results[v.id]=v;
|
|
34
|
+
}
|
|
35
|
+
for(const [key,val] of Object.entries(node).slice(0,900)){
|
|
36
|
+
if(EQUIPMENT_BY_ID[key.toUpperCase()]){
|
|
37
|
+
const v=normalizeDevice(key.toUpperCase(),val);
|
|
38
|
+
if(v)results[v.id]=v;
|
|
39
|
+
}
|
|
40
|
+
if(plain(val)||Array.isArray(val))scan(val,depth+1);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
scan(payload,0);return results;
|
|
44
|
+
}
|
|
45
|
+
export function summarizeState(mapped){
|
|
46
|
+
const arr=Object.values(mapped),known=arr.filter(x=>x.running!==null||x.fault!==null||x.blocked!==null);
|
|
47
|
+
return {matched:arr.length,known:known.length,online:arr.filter(x=>x.running===true&&x.fault!==true).length,faults:arr.filter(x=>x.fault===true).length};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Parcel positions are displayed only when V1C provides both an equipment ID
|
|
51
|
+
// and a normalized 0..1 progress on that equipment. Counts alone do not imply
|
|
52
|
+
// any parcel can be positioned in space.
|
|
53
|
+
export function extractParcelState(payload){
|
|
54
|
+
const containers=[payload,plain(payload)?payload.facility:null,plain(payload)?payload.state:null];
|
|
55
|
+
let count=null,positions=[];
|
|
56
|
+
for(const c of containers){
|
|
57
|
+
if(!plain(c))continue;
|
|
58
|
+
const v=value(c,'parcels_in_transit','parcelsInTransit','parcel_count','parcelCount');
|
|
59
|
+
if(typeof v==='number'&&Number.isSafeInteger(v)&&v>=0&&v<1000000)count=v;
|
|
60
|
+
let arr=value(c,'parcels','parcel_positions','parcelPositions');
|
|
61
|
+
if(plain(arr))arr=value(arr,'in_transit','inTransit','items','positions');
|
|
62
|
+
if(!Array.isArray(arr))continue;
|
|
63
|
+
if(count===null)count=arr.length;
|
|
64
|
+
positions=[];
|
|
65
|
+
for(const item of arr.slice(0,150)){
|
|
66
|
+
if(!plain(item))continue;
|
|
67
|
+
const id=value(item,'id','parcel_id','parcelId');
|
|
68
|
+
const eq=value(item,'equipmentId','equipment_id','conveyorId','conveyor_id');
|
|
69
|
+
const frac=value(item,'progress','progress_fraction','progressFraction','position_ratio');
|
|
70
|
+
if((typeof id==='string'||Number.isSafeInteger(id))&&typeof eq==='string'&&EQUIPMENT_BY_ID[eq.toUpperCase()]&&!EQUIPMENT_BY_ID[eq.toUpperCase()].demoOnly&&
|
|
71
|
+
typeof frac==='number'&&Number.isFinite(frac)&&frac>=0&&frac<=1){
|
|
72
|
+
positions.push({id:String(id),equipmentId:eq.toUpperCase(),progress:frac});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
return {count,positions};
|
|
78
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
:root{color-scheme:dark;--bg:#07111d;--panel:#0f2230;--panel2:#102637;--border:#254454;--text:#e4eef3;--muted:#7e9bab;--cyan:#46d0c2;--blue:#67b8e1;--gold:#e3b35d;--red:#e96f7a;--base:#0b1b29;font-family:Inter,'Segoe UI',system-ui,-apple-system,Arial,sans-serif}*{box-sizing:border-box}html,body{margin:0;min-height:100%;background:var(--bg);color:var(--text)}button,input{font:inherit}button{cursor:pointer}button:focus-visible,input:focus-visible{outline:2px solid #7be9e1;outline-offset:3px}button:disabled{cursor:wait;opacity:.6}.app-shell{min-height:100vh;display:flex}.rail{width:67px;flex:none;background:#061320;border-right:1px solid #203949;display:flex;flex-direction:column;align-items:center;padding:19px 9px 23px;gap:17px}.brand{font-weight:900;color:#f1fafc;font-size:24px;letter-spacing:-3px;margin-bottom:9px}.brand span{color:var(--cyan);font-size:22px}.rail-sep{width:22px;height:1px;background:#325162;margin:1px 0 13px}.rail-btn{width:43px;height:44px;border:1px solid transparent;border-radius:10px;background:transparent;color:#7498aa;font-size:25px}.rail-btn.active{background:#123743;color:#5ed9c5;border-color:#226070}.rail-btn:hover{color:white;background:#142a39}.rail-spacer{flex:1}.rail-footer{text-align:center;font-size:12px;line-height:20px;font-weight:800;color:#80a1b0;letter-spacing:1px}.rail-footer small{font-size:8px;color:#61869a}.workarea{flex:1;min-width:0;max-width:2200px;margin:0 auto;padding:23px 24px 14px}.topbar{margin:0 0 20px}.eyebrow,.panel-index,.section-mark{color:#6bc6e5;font-size:10px;font-weight:800;letter-spacing:1.8px}.slash{color:#426276;padding:0 8px}.topbar-bottom{display:flex;justify-content:space-between;align-items:center;gap:20px}.topbar h1{font-size:27px;font-weight:650;letter-spacing:-.6px;margin:7px 0 4px}.build-tag{font-size:9px;letter-spacing:1px;color:#94cddd;border:1px solid #386078;padding:5px 7px;border-radius:4px;vertical-align:middle;margin-left:7px}.lede{color:var(--muted);font-size:12px;margin:0}.top-actions{display:flex;gap:12px;align-items:center}.time-chip{display:flex;gap:9px;align-items:center;font-size:10px;letter-spacing:.7px;color:#c6e6eb;background:#0e2939;border:1px solid #295167;padding:11px 13px;border-radius:7px}.pulse,.pulsedot{width:7px;height:7px;background:#4de1c5;border-radius:50%;box-shadow:0 0 0 4px #35b89f1f;display:inline-block}.outline-button,.accent-button{white-space:nowrap;font-size:10px;font-weight:750;letter-spacing:.3px;padding:11px 14px;border-radius:6px;border:1px solid #37617a;color:#d6edf5;background:#173447}.outline-button:hover{background:#24536c}.accent-button{background:#177d84;border-color:#20a1a0;color:white}.accent-button:hover{background:#20979c}.metrics{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:10px;margin-bottom:14px}.metric{background:linear-gradient(140deg,#102736,#0a1d2b);border:1px solid #28495b;border-radius:8px;min-height:98px;padding:15px 17px}.metric>span{font-size:9px;color:#86c5e4;font-weight:750;letter-spacing:1.3px}.metric-value{margin-top:7px;display:flex;align-items:baseline;gap:6px}.metric-value b{font-size:26px;font-weight:750;letter-spacing:-1.1px}.metric-value small{font-size:10px;color:#7d9dab}.metric em{font-style:normal;font-size:10px;color:#7b9cad;display:block;margin-top:5px}.source-value{display:flex;align-items:center;gap:9px;margin-top:13px}.source-value b{font-size:18px;letter-spacing:.3px}.source-value i{height:8px;width:8px;border-radius:50%;background:var(--gold);box-shadow:0 0 11px #f5bd5791}.content-grid{display:grid;grid-template-columns:minmax(0,1fr) 302px;grid-template-rows:auto auto;align-items:stretch;gap:10px}.viewer-card,.inspector,.bottom-card{background:#0e2231;border:1px solid #29495c;border-radius:8px;overflow:hidden}.panel-head{padding:15px 16px 13px;display:flex;justify-content:space-between;gap:12px;align-items:center}.panel-head h2,.inspector h2{margin:5px 0 4px;font-size:17px;font-weight:650}.panel-head p,.inspector-head p{font-size:11px;color:#84a2b5;margin:0}.panel-index span{color:#4e6975}.panel-utilities{display:flex;gap:7px}.panel-utilities button{padding:10px 11px}.view-toolbar{border-top:1px solid #213c4f;border-bottom:1px solid #213c4f;padding:9px 12px;display:flex;gap:5px;align-items:center;flex-wrap:wrap}.toolbar-title{color:#6c95a9;font-size:9px;letter-spacing:1px;font-weight:750;margin-right:10px;white-space:nowrap}.view-mode{border:1px solid transparent;color:#9bb7c8;background:transparent;font-size:10px;padding:9px 10px;border-radius:5px;white-space:nowrap}.view-mode.active{background:#19546b;border-color:#27758c;color:#f3ffff}.view-mode:not(.active):hover{background:#1b3546;color:white}.toolbar-divider{height:17px;width:1px;background:#355061;margin:0 5px}.toolbar-grow{flex:1}.icon-button{font-size:12px;font-weight:800;background:#15394b;color:#b2dbec;border:1px solid #335b70;padding:7px 8px;border-radius:5px}.stage{height:clamp(425px,50vw,666px);position:relative;background:radial-gradient(circle at 49% 40%,#173447 0%,#0b1b2a 59%,#071522 100%);overflow:hidden}.stage-grid{position:absolute;inset:0;pointer-events:none;background-image:linear-gradient(#49728b09 1px,transparent 1px),linear-gradient(90deg,#49728b09 1px,transparent 1px);background-size:26px 26px}.stage canvas{display:block;width:100%;height:100%;touch-action:none}.stage #canvas-mount{position:absolute;inset:0;z-index:1}.stage-hud,.floor-key,.stage-legend,.camera-hint{position:absolute;z-index:4;pointer-events:none}.top-left{top:17px;left:17px}.hud-kicker{font-size:9px;letter-spacing:1.4px;color:#75b7d4;font-weight:800}.hud-title{font-size:14px;font-weight:750;letter-spacing:.4px;margin-top:6px}.hud-note{font-size:10px;color:#749bb1;margin-top:4px}.top-right{top:18px;right:16px;text-align:right}.live-indicator{display:flex;align-items:center;gap:8px;font-size:9px;letter-spacing:1px;color:#9cd8df;justify-content:end}.fps{font-size:9px;color:#537b92;letter-spacing:1px;margin-top:10px}.floor-key{top:100px;left:17px;width:190px;background:#091b28d9;border:1px solid #25495a;border-radius:7px;padding:8px;pointer-events:auto;backdrop-filter:blur(5px)}.floor-key-heading{font-size:9px;color:#76aec6;letter-spacing:1px;margin:4px 4px 7px;font-weight:700}.floor-key button{width:100%;display:flex;align-items:center;gap:9px;text-align:left;background:#112b3b8c;border:1px solid #254557;margin-bottom:4px;border-radius:5px;color:#d1e6ed;padding:8px;min-height:41px;font-size:11px;font-weight:750}.floor-key button:hover{background:#205066}.floor-key button.active{border-color:#65b7ca;background:#20445b}.floor-key button small{display:block;color:#7c9eac;font-size:8px;font-weight:600;margin-top:3px}.floor-key button b{font-size:10px;color:#9fcbd1;margin-left:auto}.floor-icon{width:16px;height:16px;border:1px solid #64b9c3;transform:skewY(-22deg);background:#196a72;flex:none}.floor-two{background:#2d719c;border-color:#7ab9de}.floor-one{background:#385267;border-color:#8599b2}.camera-hint{bottom:45px;left:20px;font-size:9px;letter-spacing:.6px;color:#65879b}.camera-hint strong{color:#a3c4d3;font-weight:750}.camera-hint i{font-style:normal;margin:0 8px}.stage-legend{bottom:13px;left:16px;background:#102a3cc9;border:1px solid #345164;border-radius:4px;padding:8px 10px;display:flex;gap:14px;font-size:9px;color:#a8c3d1}.swatch{width:7px;height:7px;background:#2bbeb1;border-radius:1px;display:inline-block;margin-right:5px}.swatch.warning{background:#e4af58}.swatch.error{background:#f2707d}.swatch.muted{background:#6e8090}.swatch.parcel{background:#ecd18a}.status-strip{height:43px;padding:0 16px;display:flex;align-items:center;justify-content:space-between;gap:12px;border-top:1px solid #2a4656;color:#8aaab8;font-size:10px}.status-square{width:7px;height:7px;background:var(--cyan);display:inline-block;margin-right:8px;border-radius:2px}.status-strip>span{font-size:9px;color:#668ea1;letter-spacing:.4px}.inspector{display:flex;flex-direction:column}.inspector-head{padding:16px 15px 14px;border-bottom:1px solid #29465a}.inspector-head h2{font-size:17px;margin-top:6px}.selected-asset{padding:17px 15px 15px;display:flex;flex-direction:column;flex:1}.asset-heading{display:flex;justify-content:space-between;align-items:center;gap:8px}.asset-category{color:#77bdd8;font-size:9px;font-weight:800;letter-spacing:1.1px}.asset-heading h3{font-size:22px;letter-spacing:-.4px;margin:5px 0}.asset-location{color:#87a2b4;font-size:10px}.run-badge{border:1px solid #378d7e;background:#1d6158;color:#87ead5;padding:7px 8px;border-radius:3px;font-size:9px;font-weight:800;align-self:flex-start;margin-top:6px}.run-badge.fault{background:#5b2636;border-color:#9b4c63;color:#ffc3cc}.run-badge.unknown{background:#304455;border-color:#6a7f8d;color:#c4d3e0}.asset-stats{margin-top:17px}.asset-stats>div{display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid #223d50;padding:12px 0;gap:8px}.asset-stats span{color:#89a7b9;font-size:10px}.asset-stats strong{font-size:11px;font-weight:750;color:#e7f1f6}.detail-divider{height:1px;background:#2a495a;margin:16px 0}.detail-heading{color:#75bbd5;font-size:9px;font-weight:800;letter-spacing:1.1px;margin:0 0 10px}.route-chip{border:1px solid #2c5066;background:#0b1d2a;border-radius:4px;padding:12px 9px;color:#d0ebf2;font:10px ui-monospace,monospace;margin-bottom:17px}.asset-neighbors{display:flex;gap:7px;margin-bottom:2px}.neighbor-btn{background:#17384a;border:1px solid #376076;border-radius:5px;padding:9px;color:#83b5ce;font-size:9px;flex:1;text-align:left}.neighbor-btn strong{display:block;color:#d4f4f9;font-size:12px;margin-top:6px}.neighbor-btn:disabled{opacity:.4;cursor:not-allowed}.search-row{display:flex;gap:6px}.search-row input{width:0;flex:1;background:#081c2a;border:1px solid #33566d;color:white;padding:11px 9px;border-radius:4px;font-size:11px}.search-row button{background:#20627b;border:1px solid #3a8097;color:white;border-radius:4px;padding:9px;font-size:10px;font-weight:700}.search-error{min-height:22px;color:#f2b5b9;font-size:10px;padding-top:6px}.inspector-foot{border-top:1px solid #2d4657;display:flex;gap:8px;align-items:flex-start;padding-top:12px;margin-top:auto;color:#769fb1;font-size:10px;line-height:1.5}.inspector-dot{margin-top:5px;flex:none;width:6px;height:6px;background:#78aec8;border-radius:100%}.bottom-card{grid-column:1/3;padding:13px 16px 11px;min-height:119px}.bottom-flow{display:flex;gap:10px;align-items:center;margin:13px 0 14px}.flow-step{display:flex;gap:8px;align-items:center;font-size:10px;color:#d9eef4;flex:1;white-space:nowrap}.flow-step>span{background:#173c4f;border:1px solid #315d6d;border-radius:4px;padding:7px 9px;color:#76c6d9;font-weight:700}.flow-step strong{letter-spacing:.6px;font-size:10px}.flow-step small{color:#7191a4;font-size:9px}.flow-arrow{color:#4b8295}.activity{background:#091b28;padding:9px 12px;border:1px solid #264359;border-radius:4px;font:10px ui-monospace,monospace;color:#79c8d4}.footer{padding:14px 5px 3px;color:#5d8194;font-size:9px;letter-spacing:.7px}.footer span{margin:0 9px}.footer-right{float:right}.scene-loading{position:absolute;z-index:3;inset:0;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:14px;background:#091827ee;font-size:11px;color:#b9dbe6;letter-spacing:1px}.scene-loading.hidden{display:none}.scene-loading span{color:#7091a8;font-size:10px;letter-spacing:normal}.spinner{width:33px;height:33px;border:2px solid #1c667a;border-top-color:#7be4d0;border-radius:100%;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}@media(max-width:1400px){.workarea{padding:18px 14px}.content-grid{grid-template-columns:minmax(0,1fr) 288px}.stage{height:550px}.panel-utilities button{padding:8px}.flow-step small{display:none}.camera-hint{display:none}}@media(max-width:1100px){.content-grid{grid-template-columns:1fr}.inspector{grid-row:2}.bottom-card{grid-column:1;grid-row:3}.selected-asset{min-height:0}.metrics{grid-template-columns:repeat(3,1fr)}.stage{height:580px}.floor-key{top:95px}}@media(max-width:700px){.rail{width:48px;padding:15px 4px}.rail-btn{width:37px}.workarea{padding:15px 8px}.topbar h1{font-size:19px}.topbar-bottom{align-items:flex-start}.time-chip{display:none}.metrics{grid-template-columns:repeat(2,1fr)}.metric{padding:11px;min-height:85px}.metric:last-child{grid-column:1/3}.panel-head{flex-wrap:wrap}.panel-utilities{width:100%}.panel-utilities button{flex:1}.view-toolbar{gap:2px}.view-mode{padding:7px;font-size:9px}.toolbar-title{display:none}.stage{height:505px}.floor-key{width:154px;top:107px;left:9px}.floor-key button{font-size:9px}.floor-key button small{font-size:7px}.top-left{left:10px;top:12px}.hud-title{font-size:11px}.top-right{right:10px;top:10px}.stage-legend{flex-wrap:wrap;gap:7px;bottom:7px;left:6px;right:6px}.status-strip{font-size:9px}.status-strip>span{display:none}.bottom-flow{overflow-x:auto}.flow-step{min-width:max-content}.footer-right{display:none}}
|
|
2
|
+
|
|
3
|
+
/* V1E — graded inter-floor transfer inspector; stays compact on narrow panels. */
|
|
4
|
+
.transfer-details{margin:16px 0 2px;padding:12px 11px;background:rgba(13,49,64,.56);border:1px solid rgba(74,184,188,.25);border-radius:6px}
|
|
5
|
+
.transfer-details[hidden]{display:none}
|
|
6
|
+
.transfer-details .detail-heading{margin:0 0 9px}
|
|
7
|
+
.transfer-measures{display:flex;gap:16px;justify-content:space-between}
|
|
8
|
+
.transfer-measures div{display:flex;flex-direction:column;gap:4px}
|
|
9
|
+
.transfer-measures span{color:#6c9cb0;font-size:9px;letter-spacing:1px}
|
|
10
|
+
.transfer-measures strong{font-size:14px;color:#a4edf0}
|
|
11
|
+
.transfer-details small{display:block;margin-top:10px;color:#789daf;font-size:10px;line-height:1.5}
|
|
12
|
+
|
|
13
|
+
/* V1F: route stages remain legible on mid-size monitors. */
|
|
14
|
+
@media (min-width:701px) and (max-width:1250px){
|
|
15
|
+
.bottom-flow{flex-wrap:wrap;row-gap:10px}
|
|
16
|
+
.flow-step{flex:0 1 auto}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/* V1F-R1: loading failure is an error, not an endlessly spinning progress UI. */
|
|
20
|
+
.scene-loading.engine-error .spinner { display: none; }
|
|
21
|
+
.scene-loading.engine-error b { color: #f0b56e; }
|
|
22
|
+
.scene-loading.engine-error span { max-width: 490px; line-height: 1.7; font-size: 12px; }
|
|
23
|
+
|
|
24
|
+
/* Compact operator frame: retain the full 3D viewport and inspector, hide duplicate
|
|
25
|
+
chrome inside Ignition Perspective. Both modes remain read-only. */
|
|
26
|
+
.ignition-embedded .rail,
|
|
27
|
+
.ignition-embedded .topbar,
|
|
28
|
+
.ignition-embedded .metrics,
|
|
29
|
+
.ignition-embedded .footer,
|
|
30
|
+
.ignition-embedded .bottom-card {display:none!important;}
|
|
31
|
+
.ignition-embedded .app-shell{display:block;min-height:0;}
|
|
32
|
+
.ignition-embedded .content-grid{margin-top:0;}
|
|
33
|
+
.ignition-embedded .stage{min-height:560px;}
|
|
34
|
+
@media(max-width:900px){.ignition-embedded .stage{min-height:380px;}}
|
|
35
|
+
|
|
36
|
+
.ignition-embedded .workarea{padding:8px;max-width:none;}
|
|
37
|
+
.ignition-embedded .content-grid{grid-template-columns:minmax(0,1fr) 280px;}
|
|
38
|
+
@media(max-width:960px){.ignition-embedded .content-grid{grid-template-columns:1fr;}}
|
|
39
|
+
|
|
40
|
+
/* V1H isolated operator strip; the exact same strip displays in Perspective iframe. */
|
|
41
|
+
.lab-console{margin:0 0 12px;border:1px solid #315265;border-radius:8px;background:linear-gradient(110deg,#112c3a,#0a1b27);padding:12px 15px;display:flex;gap:12px;align-items:center;flex-wrap:wrap}
|
|
42
|
+
.lab-label{flex:1 1 205px;min-width:170px}.lab-label .panel-index{font-size:9px}
|
|
43
|
+
.lab-state-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-top:8px;color:#8db1c5;font-size:10px}
|
|
44
|
+
#lab-state{padding:4px 9px;border-radius:4px;background:#213e4d;color:#c4e7eb;font-size:11px;border:1px solid #4a7184}
|
|
45
|
+
#lab-state[data-state=RUNNING]{color:#7df8ca;border-color:#2c9d7e;background:#17463a}
|
|
46
|
+
#lab-state[data-state=FAULT]{color:#ffc2c3;background:#562637;border-color:#c56e79}
|
|
47
|
+
#lab-state[data-state=STOPPED]{color:#ffd89b;border-color:#a0804b}
|
|
48
|
+
.lab-buttons{display:flex;gap:7px;flex-wrap:wrap;align-items:center}
|
|
49
|
+
.lab-btn{border:1px solid #547389;border-radius:5px;min-width:76px;min-height:40px;padding:7px 12px;color:#e4f2f5;font-size:11px;font-weight:760;letter-spacing:.2px}
|
|
50
|
+
.lab-btn:disabled{opacity:.36;cursor:not-allowed}
|
|
51
|
+
.lab-start{background:#1b715e;border-color:#389b7b}.lab-stop{background:#71313e;border-color:#bc6972}
|
|
52
|
+
.lab-reset{background:#27516a;border-color:#477f9f}.lab-ack{background:#795626;border-color:#ac8647}
|
|
53
|
+
.lab-test{background:transparent;border-color:#706044;color:#d7ad65}
|
|
54
|
+
.lab-message{flex:1 1 190px;color:#83b5bc;font-size:10px;line-height:1.4}.lab-message.lab-bad{color:#f0a7ad}
|
|
55
|
+
.ignition-embedded .lab-console{margin:0 0 8px;padding:8px 10px}
|
|
56
|
+
@media(max-width:750px){.lab-console{align-items:stretch}.lab-buttons{width:100%}.lab-btn{flex:1 1 75px}.lab-message{flex:1 1 100%}}
|
|
57
|
+
/* V1I embedding: Perspective supplies the native command strip and KPI cards.
|
|
58
|
+
The Three.js 3D viewport plus equipment inspector remain interactive. */
|
|
59
|
+
.ignition-embedded.native-operator .lab-console{display:none!important}
|
|
60
|
+
.ignition-embedded.native-operator .panel-utilities{display:none!important}
|
|
61
|
+
.ignition-embedded.native-operator .workarea{padding:0!important}
|
|
62
|
+
.ignition-embedded.native-operator .stage{height:590px!important;min-height:0!important}
|
|
63
|
+
.ignition-embedded.native-operator .content-grid{gap:8px}
|
|
64
|
+
|
|
65
|
+
/* V1J virtual sensor/scan inspector; no certified sensor I/O */
|
|
66
|
+
.sensor-io{padding:4px 0 10px;border-top:1px solid #25485b;margin-top:8px}
|
|
67
|
+
.sensor-io .asset-stats strong{font-variant-numeric:tabular-nums}
|
|
68
|
+
.sensor-summary,.sensor-event{margin-top:8px;padding:8px;border:1px solid #285065;border-radius:5px;color:#a6dbe9;background:#0a1d2b;font:600 10px ui-monospace,monospace;overflow-wrap:anywhere}
|
|
69
|
+
.sensor-event{color:#e8bf7a;background:#102634}
|
|
70
|
+
|
|
71
|
+
/* V1J R3: compact industrial PE status in the selected-asset inspector. */
|
|
72
|
+
#asset-pe-in,#asset-pe-out{display:inline-flex;align-items:center;gap:7px;}
|
|
73
|
+
#asset-pe-in::before,#asset-pe-out::before{content:"";display:inline-block;width:7px;height:5px;background:#758d9b;border:1px solid #8ba3af;border-radius:1px;flex:none;}
|
|
74
|
+
#asset-pe-in[data-signal="clear"]::before,#asset-pe-out[data-signal="clear"]::before{background:#40bdb4;border-color:#70e7d5;}
|
|
75
|
+
#asset-pe-in[data-signal="blocked"]::before,#asset-pe-out[data-signal="blocked"]::before{background:#eab45d;border-color:#ffd188;}
|
|
76
|
+
#asset-pe-in[data-signal="blocked"],#asset-pe-out[data-signal="blocked"]{color:#ffd188;}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# DevAgent V1J R9 — Compact sorter with four independent spiral declines
|
|
2
|
+
|
|
3
|
+
This is a **locally isolated preview**, reconstructed from the verified R8 concept base. The original V1J application, the Ignition Perspective project, the Studio and real PLC/OT networks are not changed.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
On Floor 3, the compact sliding-shoe sorter has four sequential divert stations. Each station discharges onto a short lateral chute that terminates directly at **its own** powered spiral, `R9_SPIRAL_01` through `R9_SPIRAL_04`. The four spirals descend to Floor 2 and each has its own powered takeaway. Their Floor 2 paths feed a common collection conveyor with four modeled join points, a short secondary sorter, and a return path to the original `R6_TAKEAWAY_F2 → SORT201 → MERGE201`.
|
|
8
|
+
|
|
9
|
+
Existing 34 original equipment centerlines, 6 normal delivery routes, V1C asset identities, style sheet, and Ignition project bytes are kept unchanged. Seven-turn spirals and scene geometry are not surveyed dimensions. A real installation requires checking pitch/parcel clearance, guarding, lane blocking, motor sizing, emergency stops, recirculation throughput, and deterministic merge arbitration. **The synthetic demo does not claim full industrial interlocks or collision avoidance.**
|
|
10
|
+
|
|
11
|
+
## Run on Ubuntu with your existing local runtime
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
cd ~/Downloads
|
|
15
|
+
unzip -n devagent-v1j-r9-compact-four-spiral-preview.zip
|
|
16
|
+
cd devagent-v1j-r9-compact-four-spiral-preview
|
|
17
|
+
sha256sum -c SHA256SUMS.txt
|
|
18
|
+
./qualify_r9.sh
|
|
19
|
+
python3 install_existing_three.py ~/Downloads/devagent-v1i-native-ignition-fake-opcua
|
|
20
|
+
python3 install_three.py --check
|
|
21
|
+
PY=~/Downloads/devagent-v1i-native-ignition-fake-opcua/.v1i-py313/bin/python
|
|
22
|
+
DEVAGENT_LAB_PYTHON="$PY" DEVAGENT_LAB_COMMANDS_CONFIRM=ISOLATED_TEST_ONLY ./run_r9_preview.sh
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Preview URL: http://127.0.0.1:8807/ ; fake OPC UA binds **127.0.0.1:4876** only. If either port is occupied, the launcher HOLDS rather than killing any process. Separate local V1J port 8803, Ignition Gateway 9088 and Studio 8770 are not modified or restarted.
|
|
26
|
+
|
|
27
|
+
Select the `4 CHUTES · 4 SPIRALS` camera view, click START, inspect `R9_CHUTE_01` through `R9_CHUTE_04`, and `R9_SPIRAL_01` through `R9_SPIRAL_04`. Browser/WebGL and mechanical clearances need on-device acceptance.
|