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,45 @@
|
|
|
1
|
+
import {collectFloors,renderFacility2D} from './facility2d_core.mjs';
|
|
2
|
+
import {showDeviceControls} from './equipment_controls.mjs';
|
|
3
|
+
import {installFacility2DSimulation} from './facility2d_simulation.mjs';
|
|
4
|
+
const model=JSON.parse(document.getElementById('project-data').textContent);
|
|
5
|
+
const controlPlan=JSON.parse(document.getElementById('control-plan').textContent);
|
|
6
|
+
const $=id=>document.getElementById(id);
|
|
7
|
+
$('site-title').textContent=model.name;
|
|
8
|
+
const warehouse=/warehouse|parcel|distribution|fulfill|sortation/i.test((model.industry||'')+' '+(model.description||''));
|
|
9
|
+
$('hmi-page-title').textContent=warehouse?'Warehouse overview':'Facility overview';
|
|
10
|
+
$('work-items-title').textContent=warehouse?'PARCELS ON SYSTEM':'ITEMS IN PROCESS';
|
|
11
|
+
$('asset-total').textContent=String(model.equipment.length);
|
|
12
|
+
$('signal-total').textContent=String(model.signals.length);
|
|
13
|
+
const floors=collectFloors(model.equipment),select=$('floor-selector');
|
|
14
|
+
for(const f of floors){const el=document.createElement('option');el.value=f;el.textContent='Floor '+f;select.append(el);}
|
|
15
|
+
const zoneSelect=$('zone-selector');let currentId='',filterFloor=floors[0]||'1',filterZone='all',zoom=1,focused=false,selectedCenter=null,sim=null;
|
|
16
|
+
select.value=filterFloor;
|
|
17
|
+
function zones(){zoneSelect.replaceChildren(new Option('All areas','all'));const visible=model.equipment.filter(a=>filterFloor==='all'||String(a.floor||1)===filterFloor);for(const z of [...new Set(visible.map(a=>a.zone||'UNASSIGNED'))])zoneSelect.append(new Option(z,z));}
|
|
18
|
+
function pick(a){currentId=a.id;$('selected-name').textContent=a.name||a.id;$('selected-id').textContent=a.id;$('selected-type').textContent=a.kind.toUpperCase()+' · FLOOR '+(a.floor||1);$('selected-zone').textContent='Zone: '+(a.zone||'UNASSIGNED');const list=$('selected-signals');list.replaceChildren();let own=model.signals.filter(s=>s.asset_id===a.id);if(!own.length){const r=document.createElement('div');r.textContent='No source signals declared · NOT MAPPED';r.className='signal-row';list.append(r);}for(const s of own){const row=document.createElement('div');row.className='signal-row';const title=document.createElement('span'),value=document.createElement('b');title.textContent=s.role.replaceAll('_',' ').toUpperCase();value.textContent=`UNKNOWN · ${s.datatype}${s.unit?' / '+s.unit:''}`;row.append(title,value);list.append(row);}$('selected-count').textContent=own.length+' declared signals';showDeviceControls(controlPlan,a,$('device-controls'));paint();sim?.render();}
|
|
19
|
+
function paint(){const results=renderFacility2D($('facility-map'),model.equipment,model.connections,{floor:filterFloor,zone:filterZone,selected:currentId,onSelect:pick,onPortal:changeFloor});$('map-count').textContent='Floor '+filterFloor+' · '+results.equipment+' equipment · '+results.links+' internal links';
|
|
20
|
+
renderTransitions();$('area-total').textContent=String([...new Set(model.equipment.map(a=>a.zone||'UNASSIGNED'))].length);$('no-source-note').textContent='UNVERIFIED MAPPINGS · NO LIVE PLC DATA';selectedCenter=results.selectedCenter;
|
|
21
|
+
const focus=focused&&selectedCenter;
|
|
22
|
+
const width=1180/zoom,height=660/zoom;
|
|
23
|
+
const cx=focus?selectedCenter.x:590,cy=focus?selectedCenter.y:330;
|
|
24
|
+
const x=Math.max(0,Math.min(1180-width,cx-width/2));
|
|
25
|
+
const y=Math.max(0,Math.min(660-height,cy-height/2));
|
|
26
|
+
$('facility-map').setAttribute('viewBox',`${x} ${y} ${width} ${height}`);
|
|
27
|
+
$('map-inner').style.transform='none';sim?.render();}
|
|
28
|
+
function changeFloor(next){if(!floors.includes(String(next)))return;filterFloor=String(next);select.value=filterFloor;filterZone='all';zoom=1;focused=false;currentId='';$('selected-id').textContent='Select equipment';$('selected-name').textContent='Click equipment on Floor '+filterFloor; $('selected-type').textContent='';$('selected-signals').replaceChildren();$('device-controls').replaceChildren();zones();paint();}
|
|
29
|
+
const tabs=$('floor-tabs');for(const f of floors){const b=document.createElement('button');b.type='button';b.textContent='Floor '+f;b.dataset.floor=f;b.addEventListener('click',()=>changeFloor(f));tabs.append(b);}
|
|
30
|
+
function renderTransitions(){for(const b of tabs.querySelectorAll('button')){const active=b.dataset.floor===filterFloor;b.classList.toggle('active',active);b.setAttribute('aria-current',active?'page':'false');}
|
|
31
|
+
const panel=$('floor-transfers');panel.replaceChildren();const byId=new Map(model.equipment.map(a=>[a.id,a]));
|
|
32
|
+
for(const c of model.connections){const a=byId.get(c.from),b=byId.get(c.to);if(!a||!b||String(a.floor)===String(b.floor))continue;const fromFloor=String(a.floor),toFloor=String(b.floor);if(fromFloor!==filterFloor&&toFloor!==filterFloor)continue;const outbound=fromFloor===filterFloor;const target=outbound?toFloor:fromFloor;
|
|
33
|
+
const btn=document.createElement('button');btn.type='button';btn.className='floor-transfer-link';btn.textContent=(outbound?'→ TO FLOOR ':'← FROM FLOOR ')+target+' · '+c.from+' → '+c.to;btn.addEventListener('click',()=>changeFloor(target));panel.append(btn);}
|
|
34
|
+
panel.hidden=!panel.childElementCount; }
|
|
35
|
+
select.addEventListener('change',()=>changeFloor(select.value));zoneSelect.addEventListener('change',()=>{filterZone=zoneSelect.value;paint();});
|
|
36
|
+
$('zoom-in').addEventListener('click',()=>{zoom=Math.min(1.8,+(zoom+.15).toFixed(2));paint();});$('zoom-out').addEventListener('click',()=>{zoom=Math.max(.6,+(zoom-.15).toFixed(2));paint();});$('zoom-fit').addEventListener('click',()=>{zoom=1;focused=false;paint();});
|
|
37
|
+
$('focus-selected').addEventListener('click',()=>{
|
|
38
|
+
const asset=model.equipment.find(a=>a.id===currentId);if(!asset)return;
|
|
39
|
+
if(String(asset.floor)!==filterFloor){changeFloor(String(asset.floor));}
|
|
40
|
+
filterZone='all';zoneSelect.value='all';zoom=2.3;focused=true;paint();
|
|
41
|
+
});
|
|
42
|
+
zones();paint();
|
|
43
|
+
// Read-only source tracking and operator writes to in-memory simulation ONLY.
|
|
44
|
+
// No connection to PLC/OPC UA or Ignition is created here.
|
|
45
|
+
sim=installFacility2DSimulation({model,selectedId:()=>currentId});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/* DevAgent 2D facility flow engine — source-qualified read-only conceptual geometry.
|
|
2
|
+
Render ONLY customer supplied asset IDs and links. Never represent live PLC values. */
|
|
3
|
+
const NS='http://www.w3.org/2000/svg';
|
|
4
|
+
const svgEl=(tag,attrs={},parent)=>{const n=document.createElementNS(NS,tag);for(const [k,v] of Object.entries(attrs))n.setAttribute(k,String(v));if(parent)parent.appendChild(n);return n;};
|
|
5
|
+
const label=(parent,txt,x,y,attributes={})=>{const n=svgEl('text',{x,y,...attributes},parent);n.textContent=String(txt??'');return n;};
|
|
6
|
+
const key=v=>String(v??'').trim();
|
|
7
|
+
const isTransport=k=>['conveyor','spiral','sorter','junction'].includes(k);
|
|
8
|
+
const clipLabel=(v,max=19)=>String(v??'').length>max?String(v).slice(0,max-1)+'…':String(v??'');
|
|
9
|
+
export function collectFloors(assets){return [...new Set((assets||[]).map(a=>String(a.floor??1)))].sort((a,b)=>Number(a)-Number(b)||a.localeCompare(b));}
|
|
10
|
+
export function renderFacility2D(svg, assets, connections, {floor='all',zone='all',selected='',onSelect=()=>{},onPortal=()=>{}}={}){
|
|
11
|
+
// Standard 2D never overlays multiple physical floors. An unspecified floor resolves to Floor 1 (or first declared floor).
|
|
12
|
+
const floorList=collectFloors(assets);if(floor==='all')floor=floorList[0]||'1';
|
|
13
|
+
svg.replaceChildren();svg.setAttribute('viewBox','0 0 1180 660');svg.setAttribute('preserveAspectRatio','xMidYMid meet');
|
|
14
|
+
const defs=svgEl('defs',{},svg);
|
|
15
|
+
const pat=svgEl('pattern',{id:'v0752grid',width:24,height:24,patternUnits:'userSpaceOnUse'},defs);
|
|
16
|
+
svgEl('path',{d:'M24 0H0V24',fill:'none',stroke:'#25485b','stroke-width':.5,opacity:.65},pat);
|
|
17
|
+
const filtered=(assets||[]).filter(a=>(floor==='all'||String(a.floor??1)===String(floor))&&(zone==='all'||key(a.zone||'OTHER')===zone));
|
|
18
|
+
const byId=new Map(filtered.map(a=>[a.id,a]));
|
|
19
|
+
const links=(connections||[]).filter(c=>byId.has(c.from)&&byId.has(c.to));
|
|
20
|
+
svgEl('rect',{width:1180,height:660,fill:'#0b1f2d'},svg);
|
|
21
|
+
svgEl('rect',{width:1180,height:660,fill:'url(#v0752grid)'},svg);
|
|
22
|
+
if(!filtered.length){label(svg,'No equipment on selected floor / zone',590,324,{'text-anchor':'middle',fill:'#90a8b8','font-size':19});label(svg,'CONCEPT ONLY · NO VERIFIED LIVE DATA',590,356,{'text-anchor':'middle',fill:'#577c8c','font-size':11,'letter-spacing':2});return {equipment:0,links:0};}
|
|
23
|
+
const multiFloor=false; // explicit single-floor 2D view; 3D owns full-facility spatial view
|
|
24
|
+
const category=a=>key(a.zone)||'UNASSIGNED';
|
|
25
|
+
const zones=[...new Set(filtered.map(category))];
|
|
26
|
+
const ordered=zones.length>9?zones.slice(0,8).concat(['OTHER AREAS']):zones;
|
|
27
|
+
const inCategory=(a,z)=>z==='OTHER AREAS'?!ordered.slice(0,8).includes(category(a)):category(a)===z;
|
|
28
|
+
const pad=62,usable=1056;
|
|
29
|
+
// Each 2D page presents only equipment and zones of one selected floor.
|
|
30
|
+
const zoneW=usable/Math.max(1,ordered.length);
|
|
31
|
+
ordered.forEach((z,i)=>{
|
|
32
|
+
svgEl('rect',{x:pad+i*zoneW+5,y:52,width:Math.max(65,zoneW-10),height:548,rx:12,
|
|
33
|
+
stroke:'#32566a','stroke-width':1,'stroke-dasharray':'5 7',fill:i%2?'#0d2331':'#0d2635'},svg);
|
|
34
|
+
label(svg,String(i+1).padStart(2,'0')+' / '+clipLabel(z.toUpperCase(),17),pad+i*zoneW+22,81,
|
|
35
|
+
{fill:'#91b3c5','font-size':12,'font-weight':750,'letter-spacing':1.4});
|
|
36
|
+
});
|
|
37
|
+
// Ordering uses only explicitly declared links and does not infer physical sensor or drive associations.
|
|
38
|
+
const order=new Map(filtered.map(a=>[a.id,0]));
|
|
39
|
+
for(let iteration=0;iteration<Math.min(filtered.length,50);iteration++){
|
|
40
|
+
let changed=false;for(const c of links){let next=Math.min(20,(order.get(c.from)||0)+1);if(next>(order.get(c.to)||0)){order.set(c.to,next);changed=true;}}
|
|
41
|
+
if(!changed)break;
|
|
42
|
+
}
|
|
43
|
+
const nodes=[];
|
|
44
|
+
ordered.forEach((z,i)=>{
|
|
45
|
+
const group=filtered.filter(a=>inCategory(a,z));
|
|
46
|
+
const transport=group.filter(a=>isTransport(a.kind)).sort((a,b)=>(order.get(a.id)||0)-(order.get(b.id)||0)||a.id.localeCompare(b.id));
|
|
47
|
+
const accessories=group.filter(a=>!isTransport(a.kind)).sort((a,b)=>a.id.localeCompare(b.id));
|
|
48
|
+
const zoneW=usable/Math.max(1,ordered.length),left=pad+i*zoneW;
|
|
49
|
+
transport.forEach((a,j)=>nodes.push({asset:a,x:left+(j+.5)*(zoneW/Math.max(1,transport.length)),y:320}));
|
|
50
|
+
accessories.forEach((a,j)=>nodes.push({asset:a,x:left+(j+.5)*(zoneW/Math.max(1,accessories.length)),y:166+(j%2)*276}));
|
|
51
|
+
});
|
|
52
|
+
const coordinates=new Map(nodes.map(o=>[o.asset.id,o]));
|
|
53
|
+
const drawLink=(c)=>{
|
|
54
|
+
const a=coordinates.get(c.from),b=coordinates.get(c.to);if(!a||!b)return;
|
|
55
|
+
const x1=a.x+35,y1=a.y,x2=b.x-35,y2=b.y;
|
|
56
|
+
const reversed=x2<x1-40;
|
|
57
|
+
const curve=Math.max(24,Math.abs(x2-x1)*.32);
|
|
58
|
+
const d=reversed?`M ${x1} ${y1} Q ${(x1+x2)/2} ${(y1+y2)/2+32}, ${x2} ${y2}`:`M ${x1} ${y1} C ${x1+curve} ${y1}, ${x2-curve} ${y2}, ${x2} ${y2}`;
|
|
59
|
+
const g=svgEl('g',{'aria-label':`${c.from} to ${c.to}: declared topology (not as-built)`},svg);
|
|
60
|
+
svgEl('path',{d,stroke:'#395361','stroke-width':26,'stroke-linecap':'round',fill:'none'},g);
|
|
61
|
+
svgEl('path',{d,stroke:'#102f3c','stroke-width':16,'stroke-linecap':'round',fill:'none'},g);
|
|
62
|
+
svgEl('path',{d,stroke:'#619ca4','stroke-width':2.5,'stroke-dasharray':'8 15',opacity:.86,fill:'none'},g);
|
|
63
|
+
};
|
|
64
|
+
links.forEach(drawLink);
|
|
65
|
+
// Explicit cross-floor links are *portals*, never physical continuous belt geometry.
|
|
66
|
+
// The linked endpoint's floor and association must originate in customer-provided topology.
|
|
67
|
+
const allAssets=new Map((assets||[]).map(a=>[a.id,a]));
|
|
68
|
+
const transfers=(connections||[]).filter(c=>{
|
|
69
|
+
const a=allAssets.get(c.from),b=allAssets.get(c.to);
|
|
70
|
+
return a&&b&&String(a.floor??1)!==String(b.floor??1) &&
|
|
71
|
+
(String(a.floor??1)===String(floor)||String(b.floor??1)===String(floor));
|
|
72
|
+
});
|
|
73
|
+
for(const [i,c] of transfers.entries()){
|
|
74
|
+
const a=coordinates.get(c.from)||coordinates.get(c.to);
|
|
75
|
+
if(!a)continue;
|
|
76
|
+
const source=allAssets.get(c.from),dest=allAssets.get(c.to);
|
|
77
|
+
const outgoing=String(source.floor??1)===String(floor);
|
|
78
|
+
const other=outgoing?String(dest.floor??1):String(source.floor??1);
|
|
79
|
+
const dir=outgoing?'TO':'FROM';
|
|
80
|
+
const x=Math.min(1040,Math.max(110,a.x+85));
|
|
81
|
+
const y=Math.min(560,Math.max(135,a.y-58-(i%3)*36));
|
|
82
|
+
const g=svgEl('g',{role:'button',tabindex:0,cursor:'pointer',
|
|
83
|
+
'aria-label':`${dir} Floor ${other} via declared ${c.from} to ${c.to}; switch floor`},svg);
|
|
84
|
+
svgEl('path',{d:`M${a.x+16} ${a.y-16} L${x} ${y+11}`,stroke:'#d9ae72','stroke-width':2,'stroke-dasharray':'4 6',fill:'none'},g);
|
|
85
|
+
svgEl('rect',{x:x-12,y:y-10,width:145,height:35,rx:6,fill:'#164052',stroke:'#d4ac6f','stroke-width':1},g);
|
|
86
|
+
label(g,`${dir} FLOOR ${other} ↗`,x+58,y+12,{'text-anchor':'middle',fill:'#f0d2a2','font-size':12,'font-weight':850});
|
|
87
|
+
const nav=()=>onPortal(other);
|
|
88
|
+
g.addEventListener('click',nav);
|
|
89
|
+
g.addEventListener('keydown',e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();nav();}});
|
|
90
|
+
}
|
|
91
|
+
// Draw every declared asset, including those without flow links; do not infer new connections.
|
|
92
|
+
for(const n of nodes){const {asset:a,x,y}=n,g=svgEl('g',{role:'button',tabindex:0,'aria-label':`Inspect ${a.id}`,cursor:'pointer'},svg);
|
|
93
|
+
const active=selected===a.id;
|
|
94
|
+
const transport=isTransport(a.kind);
|
|
95
|
+
if(transport){
|
|
96
|
+
svgEl('rect',{x:x-42,y:y-18,width:84,height:36,rx:9,fill:'#16384a',stroke:active?'#b5ffee':'#5f8698','stroke-width':active?3:1.2},g);
|
|
97
|
+
svgEl('path',{d:`M ${x-35} ${y} H ${x+35}`,stroke:'#67a9b6','stroke-width':4,'stroke-dasharray':'4 10',opacity:.88},g);
|
|
98
|
+
}else if(a.kind==='photoeye'){
|
|
99
|
+
svgEl('path',{d:`M${x} ${y+14}v30`,stroke:'#8399a6','stroke-width':5},g);
|
|
100
|
+
svgEl('circle',{cx:x,cy:y,r:13,fill:'#75cfc0',stroke:active?'#fff':'#b9eee8','stroke-width':3},g);
|
|
101
|
+
}else{
|
|
102
|
+
svgEl('rect',{x:x-25,y:y-24,width:50,height:48,rx:7,fill:a.kind==='scanner'?'#b7a376':'#517687',stroke:active?'#efffff':'#a1bbc5','stroke-width':active?3:1},g);
|
|
103
|
+
svgEl('rect',{x:x-18,y:y-13,width:36,height:9,rx:2,fill:'#15394a',opacity:.9},g);
|
|
104
|
+
}
|
|
105
|
+
const w=Math.min(118,Math.max(61,a.id.length*7+18));
|
|
106
|
+
svgEl('rect',{x:x-w/2,y:y+25,width:w,height:25,rx:5,fill:active?'#0a5260':'#0b2231',stroke:active?'#9ae7da':'#416a7c','stroke-width':1},g);
|
|
107
|
+
label(g,clipLabel(a.id,17),x,y+42,{'text-anchor':'middle',fill:'#dff4fa','font-size':11,'font-weight':800});
|
|
108
|
+
g.addEventListener('click',()=>onSelect(a));
|
|
109
|
+
g.addEventListener('keydown',ev=>{if(ev.key==='Enter'||ev.key===' '){ev.preventDefault();onSelect(a);}});
|
|
110
|
+
}
|
|
111
|
+
label(svg,'DECLARED MATERIAL FLOW · CONCEPTUAL / NOT TO SCALE',24,637,{fill:'#679cb1','font-size':10,'font-weight':800,'letter-spacing':1.4});
|
|
112
|
+
label(svg,filtered.length+' ASSETS / '+links.length+' DECLARED LINKS',1150,637,{'text-anchor':'end',fill:'#6d9bae','font-size':10,'font-weight':800,'letter-spacing':1.1});
|
|
113
|
+
return {equipment:filtered.length,links:links.length,portals:transfers.length,selectedCenter:coordinates.get(selected)?{x:coordinates.get(selected).x,y:coordinates.get(selected).y}:null};
|
|
114
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/* Read-only 2D telemetry + explicitly synthetic operator control.
|
|
2
|
+
No production OPC / PLC connection, no tag write capability.
|
|
3
|
+
Controls are dynamically unlocked ONLY for a confirmed in-memory demo session.
|
|
4
|
+
Device commands remain fail-closed without separate approved contracts.
|
|
5
|
+
*/
|
|
6
|
+
export function installFacility2DSimulation({model, selectedId}) {
|
|
7
|
+
const $=id=>document.getElementById(id);
|
|
8
|
+
const ids=['start','stop','reset','ack'];
|
|
9
|
+
const buttons=Object.fromEntries(ids.map(id=>[id,document.querySelector('.facility-command.fc-'+id)]));
|
|
10
|
+
const badge=document.querySelector('.facility-control-locked');
|
|
11
|
+
const banner=document.querySelector('.top .status');
|
|
12
|
+
const metrics=document.querySelectorAll('.metrics .metric strong');
|
|
13
|
+
const projectId=location.pathname.match(/^\/projects\/([a-z0-9-]{1,100})\/viewer\/index\.html$/)?.[1];
|
|
14
|
+
const policy=JSON.parse($('operations-policy').textContent);
|
|
15
|
+
const expected=new Map(model.signals.map(s=>[s.id,s]));
|
|
16
|
+
let frame=null,virtualPE=new Map(),busy=false,inflight=false,abort=null,lastPoll=0;
|
|
17
|
+
const set=(node,value)=>{if(node)node.textContent=String(value);};
|
|
18
|
+
function lock(message='🔒 CONTROL LOCKED'){
|
|
19
|
+
for(const button of Object.values(buttons)){if(!button)continue;button.disabled=true;button.setAttribute('aria-disabled','true');}
|
|
20
|
+
set(badge,message);badge?.classList.remove('sim-linked');
|
|
21
|
+
}
|
|
22
|
+
function valid(data){
|
|
23
|
+
if(data?.status!=='PASS_SYNTHETIC_PREVIEW_ONLY'||
|
|
24
|
+
data?.source!=='IN_MEMORY_SYNTHETIC_NOT_OPC'||data?.opc_writes!==false||
|
|
25
|
+
data?.production!=='HOLD'||data?.receipt?.origin!=='SYNTHETIC_OFFLINE_FIXTURE'||
|
|
26
|
+
data?.receipt?.mapping_digest!==policy.mapping_digest ||
|
|
27
|
+
!Array.isArray(data?.receipt?.frames) || !data.receipt.frames.length ||
|
|
28
|
+
!Array.isArray(data?.virtual_pe) ||
|
|
29
|
+
data.virtual_pe_source!=='CONCEPTUAL_SYNTHETIC_PORTS_NOT_MAPPED')return false;
|
|
30
|
+
const rows=data.receipt.frames.at(-1)?.signals;
|
|
31
|
+
if(!Array.isArray(rows)||rows.length!==expected.size)return false;
|
|
32
|
+
const seen=new Set();
|
|
33
|
+
for(const row of rows){
|
|
34
|
+
const src=expected.get(row?.signal_id);
|
|
35
|
+
if(!src||seen.has(row.signal_id)||row.asset_id!==src.asset_id||row.role!==src.role||
|
|
36
|
+
!['GOOD','BAD','UNCERTAIN','STALE','MISSING'].includes(row.quality)||typeof row.valid!=='boolean')return false;
|
|
37
|
+
seen.add(row.signal_id);
|
|
38
|
+
}
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
function assetStates(){const m=new Map();for(const row of frame?.signals||[]){
|
|
42
|
+
if(!m.has(row.asset_id))m.set(row.asset_id,[]);
|
|
43
|
+
m.get(row.asset_id).push(row);
|
|
44
|
+
}return m;}
|
|
45
|
+
function equipmentState(states){
|
|
46
|
+
if(!states?.length||states.some(s=>!s.valid))return 'UNKNOWN';
|
|
47
|
+
if(states.some(s=>s.role==='fault'&&s.active===true))return 'FAULT';
|
|
48
|
+
const run=states.find(s=>s.role==='run_feedback');
|
|
49
|
+
if(run)return run.value===true?'RUNNING':'STOPPED';
|
|
50
|
+
const pe=states.find(s=>s.role==='photoeye_blocked');
|
|
51
|
+
if(pe)return pe.active===true?'BLOCKED':'CLEAR';
|
|
52
|
+
return 'DECLARED';
|
|
53
|
+
}
|
|
54
|
+
function mapColors(){
|
|
55
|
+
const byAsset=assetStates();
|
|
56
|
+
for(const g of $('facility-map').querySelectorAll('g[aria-label^="Inspect "]')){
|
|
57
|
+
const id=g.getAttribute('aria-label').slice('Inspect '.length);
|
|
58
|
+
const part=g.querySelector('rect, circle, path');
|
|
59
|
+
if(!part)continue;
|
|
60
|
+
if(!part.hasAttribute('data-raw-stroke'))part.setAttribute('data-raw-stroke',part.getAttribute('stroke')||'');
|
|
61
|
+
const state=equipmentState(byAsset.get(id));
|
|
62
|
+
const color={RUNNING:'#41cdb1',FAULT:'#f07883',STOPPED:'#95aaba',BLOCKED:'#e3b66a',
|
|
63
|
+
CLEAR:'#41cdb1'}[state];
|
|
64
|
+
if(color && frame)part.setAttribute('stroke',color);
|
|
65
|
+
else {
|
|
66
|
+
const raw=part.getAttribute('data-raw-stroke');
|
|
67
|
+
if(raw)part.setAttribute('stroke',raw);else part.removeAttribute('stroke');
|
|
68
|
+
}
|
|
69
|
+
g.setAttribute('data-synthetic-state',frame?state:'UNAVAILABLE');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function selection(){
|
|
73
|
+
const id=selectedId();
|
|
74
|
+
const list=$('selected-signals');
|
|
75
|
+
if(!id||!list)return;
|
|
76
|
+
const states=(frame?.signals||[]).filter(s=>s.asset_id===id);
|
|
77
|
+
for(const row of list.querySelectorAll('.signal-row')){
|
|
78
|
+
const label=row.querySelector('span'),output=row.querySelector('b');
|
|
79
|
+
if(!label||!output)continue;
|
|
80
|
+
const role=label.textContent.toLowerCase().replaceAll(' ','_');
|
|
81
|
+
const entry=states.find(s=>s.role===role);
|
|
82
|
+
if(!frame||!entry||!entry.valid){set(output,'UNKNOWN');continue;}
|
|
83
|
+
const value=(role==='photoeye_blocked'||role==='fault')?(entry.active?'ACTIVE':'CLEAR'):
|
|
84
|
+
role==='run_feedback'?(entry.value===true?'RUNNING':'STOPPED'):String(entry.value);
|
|
85
|
+
set(output,value+' · SIM');
|
|
86
|
+
}
|
|
87
|
+
const count=$('selected-count');
|
|
88
|
+
if(count&&frame)count.textContent=states.length+' SIGNALS · SYNTHETIC';
|
|
89
|
+
else if(count)count.textContent=model.signals.filter(s=>s.asset_id===id).length+' declared signals';
|
|
90
|
+
}
|
|
91
|
+
function render(){
|
|
92
|
+
mapColors();selection();
|
|
93
|
+
if(!frame){
|
|
94
|
+
set(banner,'● CONCEPT PREVIEW · NO LIVE PLC');
|
|
95
|
+
set($('no-source-note'),'UNVERIFIED MAPPINGS · NO LIVE PLC DATA');
|
|
96
|
+
if(metrics[0]){set(metrics[0],'UNKNOWN');metrics[0].classList.add('unavailable');}
|
|
97
|
+
if(metrics[2])set(metrics[2],'N/A');
|
|
98
|
+
if(metrics[4])set(metrics[4],'N/A');
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
set(banner,'● SYNTHETIC DIGITAL TWIN · NOT OPC / PLC');
|
|
102
|
+
set($('no-source-note'),'SYNTHETIC DIGITAL TWIN · CUSTOMER OPC IDENTITY NOT VERIFIED');
|
|
103
|
+
const good=frame.signals.every(s=>s.valid);
|
|
104
|
+
set(metrics[0],good?'SIM · GOOD QUALITY':'SIM · DATA QUALITY ISSUE');
|
|
105
|
+
metrics[0]?.classList.toggle('unavailable',!good);
|
|
106
|
+
set(metrics[2],String(currentParcels));
|
|
107
|
+
set(metrics[4],String(frame.active_alarms?.length??'N/A'));
|
|
108
|
+
}
|
|
109
|
+
let currentParcels=0;
|
|
110
|
+
function setSource(data){
|
|
111
|
+
if(!valid(data)){
|
|
112
|
+
frame=null;virtualPE.clear();currentParcels=0;lock();render();return;
|
|
113
|
+
}
|
|
114
|
+
frame=data.receipt.frames.at(-1);
|
|
115
|
+
currentParcels=Array.isArray(data.parcels)?data.parcels.length:0;
|
|
116
|
+
virtualPE=new Map(data.virtual_pe.map(p=>[p.asset_id,p]));
|
|
117
|
+
// Never enable a control against a read-only OPC lab or production source.
|
|
118
|
+
const fault=!!data.fault_asset,paused=!!data.paused;
|
|
119
|
+
const enable={start:paused&&!fault,stop:!paused,reset:paused,ack:fault&&!data.acknowledged};
|
|
120
|
+
for(const id of ids){
|
|
121
|
+
const b=buttons[id];if(!b)continue;
|
|
122
|
+
const disabled=busy||!enable[id];b.disabled=disabled;
|
|
123
|
+
b.setAttribute('aria-disabled',String(disabled));
|
|
124
|
+
}
|
|
125
|
+
set(badge,'SIMULATOR LINKED · NO PLC');badge?.classList.add('sim-linked');
|
|
126
|
+
render();
|
|
127
|
+
}
|
|
128
|
+
async function poll(){
|
|
129
|
+
if(!projectId||inflight)return;
|
|
130
|
+
inflight=true;
|
|
131
|
+
const controller=new AbortController();abort=controller;
|
|
132
|
+
const timeout=setTimeout(()=>controller.abort(),6000);
|
|
133
|
+
try{
|
|
134
|
+
const response=await fetch('/api/live-lab/'+encodeURIComponent(projectId)+'/readings',
|
|
135
|
+
{cache:'no-store',credentials:'same-origin',signal:controller.signal});
|
|
136
|
+
const data=await response.json();
|
|
137
|
+
if(!response.ok)throw Error('READBACK_NOT_APPROVED');
|
|
138
|
+
lastPoll=Date.now();setSource(data);
|
|
139
|
+
}catch(_err){setSource(null);}
|
|
140
|
+
finally{clearTimeout(timeout);inflight=false;abort=null;}
|
|
141
|
+
}
|
|
142
|
+
for(const id of ids){
|
|
143
|
+
const button=buttons[id];
|
|
144
|
+
if(!button)continue;
|
|
145
|
+
button.addEventListener('click',async()=>{
|
|
146
|
+
if(!projectId||button.disabled||busy||!frame||Date.now()-lastPoll>3500){lock();return;}
|
|
147
|
+
if(id==='reset'&&!confirm('Reset SYNTHETIC demo fault and parcel position? NO PLC write.'))return;
|
|
148
|
+
busy=true;lock('SIM '+id.toUpperCase()+' PENDING · NO PLC');
|
|
149
|
+
try{
|
|
150
|
+
const response=await fetch('/api/preview-sim/'+encodeURIComponent(projectId)+'/operator/'+id,{
|
|
151
|
+
method:'POST',credentials:'same-origin',
|
|
152
|
+
headers:{'Content-Type':'application/json','X-DevAgent-Operator':'synthetic-session-v1'},
|
|
153
|
+
body:JSON.stringify({confirm:'SYNTHETIC_OPERATOR_ONLY'})
|
|
154
|
+
});
|
|
155
|
+
const result=await response.json();
|
|
156
|
+
if(!response.ok||result.command_authority!=='SYNTHETIC_SIMULATOR_ONLY'||
|
|
157
|
+
result.opc_writes!==false)throw Error(result.reason||'SIM_COMMAND_HOLD');
|
|
158
|
+
set(badge,'SIM '+id.toUpperCase()+' ACCEPTED · NOT PLC');
|
|
159
|
+
}catch(_err){lock('SIM COMMAND HOLD · NO PLC');}
|
|
160
|
+
finally{busy=false;await poll();}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
lock();
|
|
164
|
+
if(projectId){poll();setInterval(()=>{if(document.visibilityState==='visible')poll();},1000);
|
|
165
|
+
document.addEventListener('visibilitychange',()=>{if(document.visibilityState==='visible')poll();});}
|
|
166
|
+
return {render,sourceUnavailable:()=>setSource(null)};
|
|
167
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>DevAgent · Facility Flow Overview</title><link rel="stylesheet" href="./facility2d.css"></head><body>
|
|
2
|
+
<div class="app"><nav class="rail" aria-label="Application"><div class="brand">D<span style="color:#31bba9">·</span></div><span>▦</span><span>⌁</span><span>◈</span><span>⚙</span></nav><main class="content"><header class="top"><div><div class="kicker">DEVAGENT / INDUSTRIAL OPERATIONS</div><div class="site" id="site-title">Facility Operations</div></div><div class="status">● CONCEPT PREVIEW · NO LIVE PLC</div></header><div class="body"><div class="heading"><div><div class="breadcrumb">OPERATIONS / FACILITY VISIBILITY</div><h1 id="hmi-page-title">Facility overview</h1><div class="sub">Industrial 2D material flow overview · one floor at a time.</div></div><span class="tag">DESIGN REVIEW · READ ONLY</span></div>
|
|
3
|
+
<div class="metrics"><div class="metric"><small>SYSTEM HEALTH</small><strong class="unavailable">UNKNOWN</strong><p>Live equipment states not verified</p></div><div class="metric"><small>THROUGHPUT</small><strong>N/A</strong><p>No authorized source</p></div><div class="metric"><small id="work-items-title">ITEMS IN TRANSIT</small><strong>N/A</strong><p>Requires WCS or validated telemetry</p></div><div class="metric"><small>ASSETS MODELED</small><strong id="asset-total">—</strong><p>From customer equipment register</p></div><div class="metric"><small>ACTIVE ALARMS</small><strong>N/A</strong><p>Unknown does not mean zero</p></div></div>
|
|
4
|
+
<section class="facility-control-strip" aria-label="Facility controls (not authorized)"><div class="facility-control-label"><strong>Facility controls</strong></div><div class="facility-control-buttons" role="group" aria-label="Facility commands locked"><button type="button" class="facility-command fc-start" disabled aria-disabled="true" title="START unavailable: no approved command mapping, roles or interlocks">START</button><button type="button" class="facility-command fc-stop" disabled aria-disabled="true" title="STOP unavailable: not a safety-rated emergency stop">STOP</button><button type="button" class="facility-command fc-reset" disabled aria-disabled="true" title="RESET unavailable: approved device command contract required">RESET</button><button type="button" class="facility-command fc-ack" disabled aria-disabled="true" title="ACK unavailable: alarm acknowledgement and audit not configured">ACK</button></div><span class="facility-control-locked" role="status">🔒 CONTROL LOCKED</span></section>
|
|
5
|
+
<div class="layout"><section class="panel"><div class="panel-title"><div><div class="kicker">01 / FACILITY MAP</div><h2>Material flow & equipment</h2></div><span class="pill" id="map-count">—</span></div><div class="controlbar"><div class="group"><label for="floor-selector">FLOOR</label><select id="floor-selector" aria-label="Select floor"></select><div class="floor-tabs" id="floor-tabs" aria-label="Floor navigation"></div><label for="zone-selector">AREA</label><select id="zone-selector"><option value="all">All areas</option></select></div><div class="group"><button id="zoom-out" title="Zoom out">−</button><button id="zoom-in" title="Zoom in">+</button><button id="zoom-fit">FIT VIEW</button><button id="focus-selected" title="Zoom to selected equipment">FOCUS DEVICE</button></div></div><div class="map-canvas" role="region" aria-label="Read-only conceptual 2D equipment map"><div class="map-inner" id="map-inner"><svg id="facility-map" xmlns="http://www.w3.org/2000/svg" aria-label="Declared equipment and topology"></svg></div></div><div id="floor-transfers" class="floor-transfers" aria-label="Customer-declared inter-floor links (unverified)"></div><div class="map-note" id="no-source-note">UNVERIFIED MAPPINGS · NO LIVE PLC DATA</div><div class="map-caption"><span>LEGEND: cyan paths = declared connections · symbols = declared equipment only</span><span>NOT TO SCALE</span></div></section>
|
|
6
|
+
<aside class="panel"><div class="panel-title"><div><div class="kicker">02 / NETWORK INSIGHTS</div><h2>Equipment diagnostics</h2></div></div><div class="inspector"><div class="panel-kicker">DESIGN METADATA</div><h3 id="selected-id">Select equipment</h3><div class="muted" id="selected-name">Click equipment in the map for declared information.</div><p class="muted" id="selected-type">Live values are unavailable.</p><p class="muted" id="selected-zone">—</p><div class="divider"></div><div class="panel-kicker" id="selected-count">SIGNALS · NOT VERIFIED</div><div id="selected-signals"></div><div class="divider"></div><div class="panel-kicker">DEVICE COMMANDS</div><div id="device-controls" aria-label="Device control intents (locked)"></div><div class="divider"></div><div class="panel-kicker">LAYOUT SUMMARY</div><p class="muted"><span id="area-total">0</span> declared areas · <span id="signal-total">0</span> signal declarations</p><div class="safety">No PLC writes · No live telemetry · Signal associations and machine status require source verification and FAT.</div></div></aside></div><div class="footer">DEVAGENT ENGINEERING CANDIDATE · 2D DESIGN CONCEPT · IGNITION / PHYSICAL FAT NOT EXECUTED</div></div></main></div>
|
|
7
|
+
<script type="application/json" id="project-data">__EMBEDDED_PROJECT__</script><script type="application/json" id="operations-policy">__EMBEDDED_OPERATIONS_POLICY__</script><script type="application/json" id="control-plan">__EMBEDDED_CONTROL_PLAN__</script><script type="module" src="./facility2d_app.mjs"></script></body></html>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Offline 2.5D SVG conceptual fallback, explicitly NOT advertised as Three.js. */
|
|
2
|
+
export function startFallback(model,host,onSelect){
|
|
3
|
+
const NS='http://www.w3.org/2000/svg';let zoom=1,dx=0,dy=0,selected=null,floor='all',mode='iso';
|
|
4
|
+
let replayAlarms=[];const svg=document.createElementNS(NS,'svg');svg.setAttribute('viewBox','0 0 1200 780');svg.setAttribute('role','img');svg.setAttribute('aria-label','Conceptual isometric equipment layout');host.append(svg);
|
|
5
|
+
function node(tag,attrs={},text){const n=document.createElementNS(NS,tag);for(const[k,v]of Object.entries(attrs))n.setAttribute(k,String(v));if(text!=null)n.textContent=String(text);return n;}
|
|
6
|
+
const coords=e=>{const x=e.position.x,y=e.position.y,z=e.position.z;return mode==='top'?[580+x*15,350+z*13-y*3]:mode==='side'?[580+x*16,550-y*17]:[600+x*19+z*12,420+z*5-x*4-y*14];};
|
|
7
|
+
function render(){svg.replaceChildren();svg.append(node('rect',{x:0,y:0,width:1200,height:780,fill:'#0b2031'}));const layer=node('g',{transform:`translate(${dx} ${dy}) translate(600 390) scale(${zoom}) translate(-600 -390)`});svg.append(layer);
|
|
8
|
+
for(const c of model.connections){const a=model.equipment.find(e=>e.id===c.from),b=model.equipment.find(e=>e.id===c.to);if(!a||!b||floor!=='all'&&(String(a.floor)!==floor||String(b.floor)!==floor))continue;const[x1,y1]=coords(a),[x2,y2]=coords(b);layer.append(node('path',{d:`M${x1} ${y1} L${x2} ${y2}`,fill:'none',stroke:'#3d9bb8','stroke-width':3,'stroke-dasharray':'6 5'}));}
|
|
9
|
+
for(const e of model.equipment){if(floor!=='all'&&String(e.floor)!==floor)continue;const[x,y]=coords(e),size=Math.min(80,Math.max(30,e.dimensions.x*7)),height=Math.max(17,Math.min(58,e.dimensions.z*8));const g=node('g',{tabindex:0,role:'button','aria-label':`Inspect ${e.id}`});const active=e.id===selected;const fill=e.kind==='scanner'?'#b18c4a':e.kind==='tank'?'#468596':e.kind==='photoeye'?'#3bc7bb':e.kind==='spiral'?'#318ea6':'#407f99';g.append(node('path',{d:`M${x-size/2} ${y} l${size*.27} -${height*.36} h${size} l-${size*.27} ${height*.36} Z`,fill:active?'#6ec8db':'#437f99',stroke:'#80a5b7'}));g.append(node('rect',{x:x-size/2,y:y,width:size,height:height,rx:3,fill,stroke:active?'#ccf7ff':'#2c566c','stroke-width':active?3:1}));const label=node('text',{x:x,y:y+height/2+4,'text-anchor':'middle',fill:'#eaf9ff','font-size':Math.min(13,Math.max(10,75/e.id.length)),'font-family':'system-ui','font-weight':'bold'},e.id);g.append(label);g.addEventListener('click',()=>onSelect(e.id));g.addEventListener('keydown',ev=>{if(ev.key==='Enter'||ev.key===' '){ev.preventDefault();onSelect(e.id);}});layer.append(g);}}
|
|
10
|
+
for(const alert of replayAlarms){const asset=model.equipment.find(e=>e.id===alert.asset_id);if(!asset||(floor!=='all'&&String(asset.floor)!==floor))continue;
|
|
11
|
+
const[x,y]=coords(asset);layer.append(node('circle',{cx:x,cy:y,r:46,fill:'none',stroke:alert.severity==='CRITICAL'?'#ff6582':'#edbb5a','stroke-width':5,'stroke-dasharray':'6 4'}));}
|
|
12
|
+
let pan=false,px,py;svg.addEventListener('pointerdown',e=>{pan=true;px=e.clientX;py=e.clientY;svg.setPointerCapture(e.pointerId);});svg.addEventListener('pointermove',e=>{if(!pan||e.buttons!==1)return;dx+=(e.clientX-px)*1200/svg.clientWidth;dy+=(e.clientY-py)*780/svg.clientHeight;px=e.clientX;py=e.clientY;render();});svg.addEventListener('pointerup',()=>pan=false);svg.addEventListener('wheel',e=>{e.preventDefault();zoom=Math.max(.35,Math.min(3,zoom*(e.deltaY<0?1.1:.9)));render();},{passive:false});function focus(id){const e=model.equipment.find(x=>x.id===id);if(!e)return false;
|
|
13
|
+
if(floor!=='all'&&String(e.floor)!==floor)floor=String(e.floor);
|
|
14
|
+
zoom=2;const [x,y]=coords(e);dx=(600-x)*zoom;dy=(390-y)*zoom;selected=id;render();return true;}
|
|
15
|
+
function overview(){zoom=1;dx=dy=0;render();}
|
|
16
|
+
render();return{focus,overview,operatingStates(_signals){/* 2.5D fallback does not claim mechanical state */},alarms(entries){replayAlarms=Array.isArray(entries)?entries:[];render();},select(id){selected=id;render();},floor(v){floor=v;render();},view(v){mode=v;dx=dy=0;zoom=1;render();}};
|
|
17
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/** DevAgent Spatial Operations: deterministic, display-only routing proposals.
|
|
2
|
+
* Explicit customer graph edges ONLY. Paths are illustrative, never measured as-built belts.
|
|
3
|
+
* No machine commands, signals, vendor access, inferred graph edges, or physics claims.
|
|
4
|
+
*/
|
|
5
|
+
const HANDLING = new Set(['conveyor', 'sorter', 'junction', 'spiral']);
|
|
6
|
+
const FLAT = new Set(['conveyor', 'sorter', 'junction']);
|
|
7
|
+
const EPS=1e-8;
|
|
8
|
+
const clamp=(v,a,b)=>Math.max(a,Math.min(b,v));
|
|
9
|
+
function point(e, y){ return {x:e.position.x,y,z:e.position.z}; }
|
|
10
|
+
function port(e, toward, y, spiralOutlet=false){
|
|
11
|
+
const w=Number(e.dimensions.x), d=Number(e.dimensions.z);
|
|
12
|
+
const dx=toward.position.x-e.position.x, dz=toward.position.z-e.position.z;
|
|
13
|
+
const out=point(e,y);
|
|
14
|
+
// Port selection is based on equipment bounds, not an assertion about OEM interfaces.
|
|
15
|
+
if(e.kind==='spiral'){
|
|
16
|
+
const a=Math.atan2(dz,dx);
|
|
17
|
+
const r=Math.max(.35,Math.min(w,d)*.42);
|
|
18
|
+
out.x+=Math.cos(a)*r;out.z+=Math.sin(a)*r;
|
|
19
|
+
} else if(Math.abs(dx)/(w+EPS)>=Math.abs(dz)/(d+EPS)){
|
|
20
|
+
out.x+=Math.sign(dx||1)*w*.49;
|
|
21
|
+
if(e.kind==='sorter')out.z+=clamp(dz,-d*.33,d*.33);
|
|
22
|
+
} else {
|
|
23
|
+
out.z+=Math.sign(dz||1)*d*.49;
|
|
24
|
+
if(e.kind==='sorter')out.x+=clamp(dx,-w*.33,w*.33);
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
function controlPath(a,b){
|
|
29
|
+
const dx=b.x-a.x,dz=b.z-a.z;
|
|
30
|
+
const length=Math.hypot(dx,dz),side=clamp(length*.27,.45,3.3);
|
|
31
|
+
if(length<.1)return [a,b];
|
|
32
|
+
const ux=dx/length,uz=dz/length;
|
|
33
|
+
return [a,{x:a.x+ux*side,y:a.y+(b.y-a.y)*.25,z:a.z+uz*side},
|
|
34
|
+
{x:b.x-ux*side,y:a.y+(b.y-a.y)*.75,z:b.z-uz*side},b];
|
|
35
|
+
}
|
|
36
|
+
export function planSpatialRoutes(model){
|
|
37
|
+
const byId=new Map(model.equipment.map(e=>[e.id,e]));
|
|
38
|
+
const levels=[...new Set(model.equipment.map(e=>e.floor))].sort((a,b)=>a-b);
|
|
39
|
+
const result=[];
|
|
40
|
+
for(const edge of model.connections){
|
|
41
|
+
const a=byId.get(edge.from),b=byId.get(edge.to);
|
|
42
|
+
if(!a||!b)continue; // caller's deterministic contract separately rejects dangling IDs
|
|
43
|
+
const cross=a.floor!==b.floor;
|
|
44
|
+
const spiral=a.kind==='spiral'||b.kind==='spiral';
|
|
45
|
+
let kind='logical_unverified';
|
|
46
|
+
if(!cross&&HANDLING.has(a.kind)&&HANDLING.has(b.kind))kind='conceptual_conveyor_transition';
|
|
47
|
+
// A vertical material path may be drawn only when an explicit spiral participates.
|
|
48
|
+
if(cross&&spiral&&HANDLING.has(a.kind)&&HANDLING.has(b.kind))kind='conceptual_spiral_transfer';
|
|
49
|
+
const ya=a.position.y+1.1,yb=b.position.y+1.1;
|
|
50
|
+
const p=port(a,b,cross&&a.kind==='spiral'?yb:ya);
|
|
51
|
+
const q=port(b,a,cross&&b.kind==='spiral'?ya:yb);
|
|
52
|
+
// Cross-level spiral endpoints coincide with the served level; the spiral's helix
|
|
53
|
+
// visually represents vertical travel. The bridge itself is horizontal.
|
|
54
|
+
const pts=controlPath(p,q);
|
|
55
|
+
result.push({from:a.id,to:b.id,from_floor:a.floor,to_floor:b.floor,
|
|
56
|
+
kind,geometry_status:'CONCEPTUAL_CONNECTION_NEEDS_APPROVAL',points:pts});
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
export function commonFacilityBounds(model,pad=3.5){
|
|
61
|
+
if(!model.equipment.length)return null;
|
|
62
|
+
const xs=model.equipment.flatMap(e=>[e.position.x-e.dimensions.x/2,e.position.x+e.dimensions.x/2]);
|
|
63
|
+
const zs=model.equipment.flatMap(e=>[e.position.z-e.dimensions.z/2,e.position.z+e.dimensions.z/2]);
|
|
64
|
+
const minX=Math.min(...xs)-pad,maxX=Math.max(...xs)+pad;
|
|
65
|
+
const minZ=Math.min(...zs)-pad,maxZ=Math.max(...zs)+pad;
|
|
66
|
+
return {minX,maxX,minZ,maxZ,width:maxX-minX,depth:maxZ-minZ,
|
|
67
|
+
centerX:(minX+maxX)/2,centerZ:(minZ+maxZ)/2,
|
|
68
|
+
levels:[...new Set(model.equipment.map(e=>e.floor))].sort((a,b)=>a-b)};
|
|
69
|
+
}
|
|
70
|
+
export function nextSpiralRise(model,id){
|
|
71
|
+
const spiral=model.equipment.find(e=>e.id===id&&e.kind==='spiral');if(!spiral)return null;
|
|
72
|
+
const others=model.connections.filter(c=>c.from===id||c.to===id)
|
|
73
|
+
.map(c=>model.equipment.find(e=>e.id===(c.from===id?c.to:c.from))).filter(Boolean)
|
|
74
|
+
.filter(e=>e.floor!==spiral.floor);
|
|
75
|
+
if(!others.length)return null;
|
|
76
|
+
return Math.max(...others.map(e=>Math.abs(e.position.y-spiral.position.y)));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* V0.5.4 preview assembly contract. Every visual port is owned by an existing
|
|
81
|
+
* customer asset, and every bridge is owned by one declared material-flow edge.
|
|
82
|
+
* This does not modify source coordinates or make OEM/mechanical claims.
|
|
83
|
+
*/
|
|
84
|
+
export function planVisualAssembly(model){
|
|
85
|
+
const byId=new Map(model.equipment.map(e=>[e.id,e]));
|
|
86
|
+
const parents=new Map(model.equipment.map(e=>[e.id,[]]));
|
|
87
|
+
const children=new Map(model.equipment.map(e=>[e.id,[]]));
|
|
88
|
+
for(const c of model.connections){
|
|
89
|
+
if(!byId.has(c.from)||!byId.has(c.to))throw Error('ASSEMBLY_DANGLING_CONNECTION');
|
|
90
|
+
parents.get(c.to).push(byId.get(c.from));children.get(c.from).push(byId.get(c.to));
|
|
91
|
+
}
|
|
92
|
+
const kindFlat=new Set(['conveyor','sorter','junction']);
|
|
93
|
+
const normalize=(x,z,fallback={x:1,z:0})=>{const n=Math.hypot(x,z);return n>1e-6?{x:x/n,z:z/n}:fallback;};
|
|
94
|
+
const vec=(a,b)=>normalize(b.position.x-a.position.x,b.position.z-a.position.z);
|
|
95
|
+
const P=(x,y,z)=>({x,y,z});
|
|
96
|
+
const add=(p,d,m)=>P(p.x+d.x*m,p.y,p.z+d.z*m);
|
|
97
|
+
const stations=new Map();
|
|
98
|
+
for(const e of model.equipment){
|
|
99
|
+
if(!kindFlat.has(e.kind)&&e.kind!=='spiral')continue;
|
|
100
|
+
const p=e.position,d=e.dimensions,baseY=p.y+1.1;
|
|
101
|
+
if(e.kind==='spiral'){
|
|
102
|
+
const input=parents.get(e.id).find(s=>s.floor===e.floor);
|
|
103
|
+
const output=children.get(e.id).find(s=>s.floor!==e.floor);
|
|
104
|
+
const inward=input?vec(input,e):{x:1,z:0};
|
|
105
|
+
const outward=output?vec(e,output):inward;
|
|
106
|
+
// Tangent-facing ports, not arbitrary points on the spiral radius.
|
|
107
|
+
const startAngle=Math.atan2(inward.z,inward.x)-Math.PI/2;
|
|
108
|
+
const goalAngle=Math.atan2(outward.z,outward.x)-Math.PI/2;
|
|
109
|
+
const sweep=(goalAngle-startAngle+Math.PI*10)%(Math.PI*2)+Math.PI*4;
|
|
110
|
+
const radius=Math.max(.72,Math.min(d.x,d.z)*.42);
|
|
111
|
+
const exitY=output?output.position.y+1.1:baseY+Math.max(2,d.y);
|
|
112
|
+
const sample=t=>{
|
|
113
|
+
const theta=startAngle+t*sweep;
|
|
114
|
+
return P(p.x+radius*Math.cos(theta),baseY+t*(exitY-baseY),p.z+radius*Math.sin(theta));
|
|
115
|
+
};
|
|
116
|
+
const at=t=>normalize(-Math.sin(startAngle+t*sweep),Math.cos(startAngle+t*sweep));
|
|
117
|
+
const helix=Array.from({length:193},(_,i)=>sample(i/192));
|
|
118
|
+
stations.set(e.id,{id:e.id,kind:e.kind,geometry_status:'CONCEPTUAL_ASSEMBLY_REVIEW_REQUIRED',
|
|
119
|
+
entry:helix[0],exit:helix.at(-1),entryTangent:at(0),exitTangent:at(1),helix,
|
|
120
|
+
length:0,radius,baseY,exitY});
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const incoming=parents.get(e.id).filter(x=>x.floor===e.floor&&x.kind!=='spiral');
|
|
124
|
+
const outgoing=children.get(e.id).filter(x=>x.floor===e.floor&&x.kind!=='spiral');
|
|
125
|
+
const incomingAll=parents.get(e.id),outgoingAll=children.get(e.id);
|
|
126
|
+
const source=incoming[0]||incomingAll[0];
|
|
127
|
+
const targets=outgoing.length?outgoing:outgoingAll;
|
|
128
|
+
// Orient the bed to the ACTUAL material-flow direction, including reversals.
|
|
129
|
+
let direction;
|
|
130
|
+
if(source&&targets.length){const i=vec(source,e),o=vec(e,targets[0]);direction=normalize(i.x+o.x,i.z+o.z,o);}
|
|
131
|
+
else if(targets.length)direction=vec(e,targets[0]);
|
|
132
|
+
else if(source)direction=vec(source,e);
|
|
133
|
+
else direction={x:1,z:0};
|
|
134
|
+
const length=Math.max(.65,d.x*.93),center=P(p.x,baseY,p.z);
|
|
135
|
+
const entry=add(center,direction,-length/2),exit=add(center,direction,length/2);
|
|
136
|
+
const outlets={};
|
|
137
|
+
for(const t of outgoingAll){
|
|
138
|
+
let endpoint=exit,tangent=direction;
|
|
139
|
+
if(e.kind==='sorter'&&outgoingAll.length>1){
|
|
140
|
+
const towards=vec(e,t),lateral=direction.x*towards.z-direction.z*towards.x;
|
|
141
|
+
// A genuine branch chute must leave the sorter side, never float from its center.
|
|
142
|
+
if(Math.abs(lateral)>.12){
|
|
143
|
+
const side=Math.sign(lateral);
|
|
144
|
+
const perp={x:-direction.z*side,z:direction.x*side};
|
|
145
|
+
endpoint=add(add(center,direction,length*.10),perp,Math.max(.48,d.z*.46));
|
|
146
|
+
tangent=normalize(direction.x*.45+perp.x*.85,direction.z*.45+perp.z*.85);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
outlets[t.id]={point:endpoint,tangent};
|
|
150
|
+
}
|
|
151
|
+
stations.set(e.id,{id:e.id,kind:e.kind,geometry_status:'CONCEPTUAL_ASSEMBLY_REVIEW_REQUIRED',
|
|
152
|
+
direction,length,entry,exit,entryTangent:direction,exitTangent:direction,outlets,
|
|
153
|
+
center,baseY,branches:Object.entries(outlets).filter(([,v])=>v.point!==exit).map(([id,v])=>({to:id,from:center,toPoint:v.point}))});
|
|
154
|
+
}
|
|
155
|
+
const routes=[];
|
|
156
|
+
for(const c of model.connections){
|
|
157
|
+
const a=byId.get(c.from),b=byId.get(c.to),sa=stations.get(c.from),sb=stations.get(c.to);
|
|
158
|
+
const cross=a.floor!==b.floor;
|
|
159
|
+
const spiral=a.kind==='spiral'||b.kind==='spiral';
|
|
160
|
+
const physical=!!sa&&!!sb&&(!cross||spiral);
|
|
161
|
+
let kind=physical?(cross?'conceptual_spiral_transfer':'conceptual_conveyor_transition'):'logical_unverified';
|
|
162
|
+
if(!physical){routes.push({from:c.from,to:c.to,kind,geometry_status:'LOGICAL_CONNECTION_UNVERIFIED',
|
|
163
|
+
points:[P(a.position.x,a.position.y+1.1,a.position.z),P(b.position.x,b.position.y+1.1,b.position.z)],from_floor:a.floor,to_floor:b.floor});continue;}
|
|
164
|
+
const out=sa.outlets?.[b.id]||{point:sa.exit,tangent:sa.exitTangent};
|
|
165
|
+
const p=out.point,q=sb.entry;
|
|
166
|
+
const diff=Math.hypot(q.x-p.x,q.y-p.y,q.z-p.z),lead=Math.min(2.2,Math.max(.15,diff*.23));
|
|
167
|
+
const pts=[p,add(p,out.tangent,lead),add(q,sb.entryTangent,-lead),q];
|
|
168
|
+
// Adjacent belts share EXACT generated port coordinates; no hand-wavy
|
|
169
|
+
// center-to-center lines or disconnected endpoint approximations.
|
|
170
|
+
routes.push({from:c.from,to:c.to,kind,geometry_status:'CONCEPTUAL_CONNECTION_NEEDS_APPROVAL',
|
|
171
|
+
points:pts,from_floor:a.floor,to_floor:b.floor,from_port:p,to_port:q,
|
|
172
|
+
endpoint_error:0,route_length:diff});
|
|
173
|
+
}
|
|
174
|
+
return {stations:Object.fromEntries(stations),routes,source_edges:model.connections.map(c=>c.from+'>'+c.to),
|
|
175
|
+
disclaimer:'PREVIEW_ONLY_NO_PHYSICAL_CLEARANCE_OR_PORT_CERTIFICATION'};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Attach a sensor gantry to the nearest conceptual material path ONLY in the
|
|
179
|
+
* preview. The customer asset position is preserved in source and compiled data.
|
|
180
|
+
* Inspector exposes the visual displacement; no as-built assertion is made.
|
|
181
|
+
*/
|
|
182
|
+
export function planSensorMounts(model,assembly){
|
|
183
|
+
const out={};
|
|
184
|
+
for(const e of model.equipment){
|
|
185
|
+
if(!['scanner','photoeye'].includes(e.kind))continue;
|
|
186
|
+
let candidate=null;
|
|
187
|
+
for(const belt of model.equipment){
|
|
188
|
+
if(!['conveyor','sorter','junction'].includes(belt.kind)||belt.floor!==e.floor)continue;
|
|
189
|
+
const station=assembly.stations[belt.id];if(!station)continue;
|
|
190
|
+
const a=station.entry,b=station.exit,dx=b.x-a.x,dz=b.z-a.z;
|
|
191
|
+
const len2=dx*dx+dz*dz;if(len2<1e-6)continue;
|
|
192
|
+
const t=clamp(((e.position.x-a.x)*dx+(e.position.z-a.z)*dz)/len2,.05,.95);
|
|
193
|
+
const x=a.x+t*dx,z=a.z+t*dz,dist=Math.hypot(x-e.position.x,z-e.position.z);
|
|
194
|
+
if(!candidate||dist<candidate.distance){candidate={equipment:belt.id,position:{x,y:e.position.y,z},direction:station.direction,
|
|
195
|
+
distance:dist,source_position:{...e.position},status:'PREVIEW_SNAP_NEEDS_CUSTOMER_APPROVAL'};}
|
|
196
|
+
}
|
|
197
|
+
if(candidate&&candidate.distance<=3.8)out[e.id]=candidate;
|
|
198
|
+
}
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Smooth, exact-endpoint conceptual sorter diverter geometry shared by Creator
|
|
203
|
+
* and generated Operator preview. It models a visual transition, NOT an OEM
|
|
204
|
+
* diverter's timing, actuators, permissives, or physical belt configuration.
|
|
205
|
+
*/
|
|
206
|
+
export function sorterBranchPoints(station,targetId){
|
|
207
|
+
const branch=station?.outlets?.[targetId];
|
|
208
|
+
if(station?.kind!=='sorter'||!branch||!station.center||!station.direction)return null;
|
|
209
|
+
const p=station.center,q=branch.point;
|
|
210
|
+
const distance=Math.hypot(q.x-p.x,q.y-p.y,q.z-p.z);
|
|
211
|
+
if(distance<.01)return null;
|
|
212
|
+
const normal=(x,z)=>{const n=Math.hypot(x,z);return n>1e-9?{x:x/n,z:z/n}:{x:1,z:0};};
|
|
213
|
+
const inDir=normal(station.direction.x,station.direction.z);
|
|
214
|
+
const outDir=normal(branch.tangent.x,branch.tangent.z);
|
|
215
|
+
const lead=Math.min(.58,distance*.26);
|
|
216
|
+
return [p,{x:p.x+inDir.x*lead,y:p.y,z:p.z+inDir.z*lead},
|
|
217
|
+
{x:q.x-outDir.x*lead,y:q.y,z:q.z-outDir.z*lead},q];
|
|
218
|
+
}
|