opencroc 1.4.0 → 1.4.2

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.
@@ -127,7 +127,7 @@
127
127
  const S = {
128
128
  project:null, graph:{nodes:[],edges:[]}, agents:[], ws:null,
129
129
  pan:{x:0,y:0}, zoom:1, dragging:false, dragStart:{x:0,y:0},
130
- nodePos:new Map(), hoveredNode:null, running:false
130
+ nodePos:new Map(), hoveredNode:null, running:false, _userPanned:false
131
131
  };
132
132
 
133
133
  async function fetchProject(){
@@ -198,19 +198,47 @@ function updateStats(){
198
198
  function layoutGraph(){
199
199
  const nodes=S.graph.nodes; if(!nodes.length)return;
200
200
  const c=document.getElementById('graph-canvas');
201
- const w=c.clientWidth||800,h=c.clientHeight||500,cx=w/2,cy=h/2;
201
+ const w=c.clientWidth||800,h=c.clientHeight||500;
202
+ // Group non-module nodes by module
202
203
  const mods=new Map();
203
- for(const n of nodes){const m=n.module||n.id;if(!mods.has(m))mods.set(m,[]);mods.get(m).push(n);}
204
- const keys=[...mods.keys()];
204
+ S.modMeta=new Map(); // module {cx,cy,radius,color}
205
+ for(const n of nodes){
206
+ if(n.type==='module') continue;
207
+ const m=n.module||'other';
208
+ if(!mods.has(m)) mods.set(m,[]);
209
+ mods.get(m).push(n);
210
+ }
211
+ const keys=[...mods.keys()].sort((a,b)=>(mods.get(b).length-mods.get(a).length));
212
+ const nMods=keys.length||1;
213
+ // Module color palette (10 distinct colors, cycle)
214
+ const palette=['#4ecca3','#e94560','#f39c12','#3498db','#9b59b6','#1abc9c','#e67e22','#2ecc71','#e84393','#00cec9'];
215
+ // Adaptive ring radius
216
+ const totalMembers=nodes.filter(n=>n.type!=='module').length;
217
+ const baseRadius=Math.max(300, Math.sqrt(totalMembers)*30);
205
218
  for(let i=0;i<keys.length;i++){
206
- const mn=mods.get(keys[i]);
207
- const a=(i/keys.length)*Math.PI*2-Math.PI/2, r=Math.min(w,h)*.3;
208
- const mcx=cx+Math.cos(a)*r, mcy=cy+Math.sin(a)*r;
219
+ const mn=mods.get(keys[i]); if(!mn)continue;
220
+ const modAngle=(i/nMods)*Math.PI*2-Math.PI/2;
221
+ const mcx=w/2+Math.cos(modAngle)*baseRadius;
222
+ const mcy=h/2+Math.sin(modAngle)*baseRadius;
223
+ const subRadius=Math.max(50, Math.sqrt(mn.length)*22);
224
+ const col=palette[i%palette.length];
225
+ // Store module meta for cluster rendering
226
+ const modNodeId='module:'+keys[i];
227
+ S.nodePos.set(modNodeId,{x:mcx,y:mcy});
228
+ S.modMeta.set(keys[i],{cx:mcx,cy:mcy,radius:subRadius+30,color:col,count:mn.length});
209
229
  for(let j=0;j<mn.length;j++){
210
- const na=(j/mn.length)*Math.PI*2, nr=35+mn.length*8;
211
- S.nodePos.set(mn[j].id,{x:mcx+Math.cos(na)*nr,y:mcy+Math.sin(na)*nr});
230
+ const na=(j/mn.length)*Math.PI*2;
231
+ S.nodePos.set(mn[j].id,{x:mcx+Math.cos(na)*subRadius,y:mcy+Math.sin(na)*subRadius});
212
232
  }
213
233
  }
234
+ // Auto-center
235
+ if(nodes.length>0&&!S._userPanned){
236
+ let minX=Infinity,maxX=-Infinity,minY=Infinity,maxY=-Infinity;
237
+ for(const[,p] of S.nodePos){minX=Math.min(minX,p.x);maxX=Math.max(maxX,p.x);minY=Math.min(minY,p.y);maxY=Math.max(maxY,p.y);}
238
+ const gw=maxX-minX+300,gh=maxY-minY+300,gcx=minX+(maxX-minX)/2,gcy=minY+(maxY-minY)/2;
239
+ S.zoom=Math.min(1, Math.min(w/gw, h/gh));
240
+ S.pan.x=w/2-gcx*S.zoom;S.pan.y=h/2-gcy*S.zoom;
241
+ }
214
242
  }
215
243
 
216
244
  function renderCanvas(){
@@ -220,44 +248,83 @@ function renderCanvas(){
220
248
  ctx.scale(dpr,dpr);
221
249
  const w=canvas.clientWidth,h=canvas.clientHeight;
222
250
  ctx.clearRect(0,0,w,h); ctx.save(); ctx.translate(S.pan.x,S.pan.y); ctx.scale(S.zoom,S.zoom);
223
- // Grid
251
+ // Subtle grid
224
252
  ctx.strokeStyle='#151530';ctx.lineWidth=.5;
225
- for(let x=0;x<w*2;x+=32){ctx.beginPath();ctx.moveTo(x,-h);ctx.lineTo(x,h*2);ctx.stroke();}
226
- for(let y=0;y<h*2;y+=32){ctx.beginPath();ctx.moveTo(-w,y);ctx.lineTo(w*2,y);ctx.stroke();}
253
+ const gridStep=40;
254
+ for(let x=-2000;x<4000;x+=gridStep){ctx.beginPath();ctx.moveTo(x,-2000);ctx.lineTo(x,4000);ctx.stroke();}
255
+ for(let y=-2000;y<4000;y+=gridStep){ctx.beginPath();ctx.moveTo(-2000,y);ctx.lineTo(4000,y);ctx.stroke();}
256
+
227
257
  const edges=S.graph.edges||[],nodes=S.graph.nodes||[];
228
- // Edges
258
+ const largeGraph=nodes.length>80;
259
+
260
+ // Draw module cluster backgrounds
261
+ if(S.modMeta){
262
+ for(const[name,m] of S.modMeta){
263
+ ctx.beginPath(); ctx.arc(m.cx,m.cy,m.radius,0,Math.PI*2);
264
+ ctx.fillStyle=m.color+'10'; ctx.fill();
265
+ ctx.strokeStyle=m.color+'30'; ctx.lineWidth=2; ctx.setLineDash([6,4]); ctx.stroke(); ctx.setLineDash([]);
266
+ // Module label at top of cluster
267
+ ctx.font='bold 13px "Courier New"'; ctx.fillStyle=m.color+'cc'; ctx.textAlign='center'; ctx.textBaseline='bottom';
268
+ ctx.fillText(name+' ('+m.count+')',m.cx,m.cy-m.radius-6);
269
+ }
270
+ }
271
+
272
+ // Draw "uses" edges with curved lines + gradient
229
273
  for(const e of edges){
274
+ if(e.relation==='contains') continue; // never draw contains
230
275
  const s=S.nodePos.get(e.source),t=S.nodePos.get(e.target);if(!s||!t)continue;
231
- ctx.strokeStyle='rgba(78,204,163,.25)';ctx.lineWidth=1.5;
232
- ctx.beginPath();ctx.moveTo(s.x,s.y);ctx.lineTo(t.x,t.y);ctx.stroke();
233
- const a=Math.atan2(t.y-s.y,t.x-s.x),al=7,ax=t.x-Math.cos(a)*18,ay=t.y-Math.sin(a)*18;
234
- ctx.fillStyle='rgba(78,204,163,.4)';ctx.beginPath();ctx.moveTo(ax,ay);
235
- ctx.lineTo(ax-al*Math.cos(a-.4),ay-al*Math.sin(a-.4));
236
- ctx.lineTo(ax-al*Math.cos(a+.4),ay-al*Math.sin(a+.4));ctx.closePath();ctx.fill();
276
+ const dx=t.x-s.x,dy=t.y-s.y,dist=Math.sqrt(dx*dx+dy*dy);
277
+ // Curved edge via quadratic bezier, perpendicular offset
278
+ const mx=(s.x+t.x)/2+dy*0.15, my=(s.y+t.y)/2-dx*0.15;
279
+ const grad=ctx.createLinearGradient(s.x,s.y,t.x,t.y);
280
+ grad.addColorStop(0,'rgba(233,69,96,.5)'); // controller (red)
281
+ grad.addColorStop(1,'rgba(78,204,163,.5)'); // model (green)
282
+ ctx.strokeStyle=grad;ctx.lineWidth=2;
283
+ ctx.beginPath();ctx.moveTo(s.x,s.y);ctx.quadraticCurveTo(mx,my,t.x,t.y);ctx.stroke();
284
+ // Arrowhead
285
+ const t2=0.95,at2x=(1-t2)*(1-t2)*s.x+2*(1-t2)*t2*mx+t2*t2*t.x,at2y=(1-t2)*(1-t2)*s.y+2*(1-t2)*t2*my+t2*t2*t.y;
286
+ const a=Math.atan2(t.y-at2y,t.x-at2x),al=8;
287
+ ctx.fillStyle='rgba(78,204,163,.6)';ctx.beginPath();ctx.moveTo(t.x,t.y);
288
+ ctx.lineTo(t.x-al*Math.cos(a-.4),t.y-al*Math.sin(a-.4));
289
+ ctx.lineTo(t.x-al*Math.cos(a+.4),t.y-al*Math.sin(a+.4));ctx.closePath();ctx.fill();
237
290
  }
291
+
292
+ // Draw nodes
238
293
  const tc={model:'#4ecca3',controller:'#e94560',api:'#f39c12',dto:'#3498db',module:'#9b59b6'};
239
294
  const sc={idle:'#444',testing:'#f39c12',passed:'#4ecca3',failed:'#e94560'};
240
- const icons={model:'📦',controller:'🎮',api:'🔌',dto:'📋',module:'📁'};
241
295
  for(const n of nodes){
296
+ if(n.type==='module') continue; // drawn as cluster bg instead
242
297
  const p=S.nodePos.get(n.id);if(!p)continue;
243
- const sz=n.type==='module'?22:14,c=tc[n.type]||'#888',ol=sc[n.status]||'#444',hov=S.hoveredNode===n.id;
244
- if(n.status==='testing'){ctx.shadowColor=sc.testing;ctx.shadowBlur=12;}
245
- else if(n.status==='passed'){ctx.shadowColor=sc.passed;ctx.shadowBlur=8;}
246
- else if(n.status==='failed'){ctx.shadowColor=sc.failed;ctx.shadowBlur=10;}
247
- ctx.fillStyle='rgba(0,0,0,.3)';ctx.fillRect(p.x-sz+2,p.y-sz+2,sz*2,sz*2);
248
- ctx.fillStyle=c;ctx.fillRect(p.x-sz,p.y-sz,sz*2,sz*2);
249
- ctx.fillStyle='rgba(255,255,255,.15)';ctx.fillRect(p.x-sz,p.y-sz,sz*2,3);ctx.fillRect(p.x-sz,p.y-sz,3,sz*2);
250
- ctx.shadowBlur=0;ctx.strokeStyle=hov?'#fff':ol;ctx.lineWidth=hov?3:2;ctx.strokeRect(p.x-sz,p.y-sz,sz*2,sz*2);
251
- ctx.font=sz+'px serif';ctx.textAlign='center';ctx.textBaseline='middle';ctx.fillText(icons[n.type]||'⬜',p.x,p.y);
252
- ctx.font='10px "Courier New"';ctx.fillStyle=hov?'#fff':'#bbb';ctx.textAlign='center';ctx.textBaseline='top';
253
- ctx.fillText((n.label||n.id.split(':').pop()).substring(0,20),p.x,p.y+sz+4);
298
+ const sz=10,c=tc[n.type]||'#888',ol=sc[n.status]||'#444',hov=S.hoveredNode===n.id;
299
+ // Glow for active status
300
+ if(n.status==='testing'){ctx.shadowColor=sc.testing;ctx.shadowBlur=14;}
301
+ else if(n.status==='passed'){ctx.shadowColor=sc.passed;ctx.shadowBlur=10;}
302
+ else if(n.status==='failed'){ctx.shadowColor=sc.failed;ctx.shadowBlur=12;}
303
+ // Node circle instead of rect for cleaner look
304
+ ctx.beginPath();ctx.arc(p.x,p.y,sz,0,Math.PI*2);
305
+ ctx.fillStyle=c;ctx.fill();
306
+ ctx.shadowBlur=0;
307
+ ctx.strokeStyle=hov?'#fff':ol;ctx.lineWidth=hov?3:1.5;ctx.stroke();
308
+ // Icon
309
+ ctx.font=(sz)+'px serif';ctx.textAlign='center';ctx.textBaseline='middle';
310
+ ctx.fillText(n.type==='model'?'📦':'🎮',p.x,p.y);
311
+ // Label — show at higher zoom or on hover
312
+ if(S.zoom>0.4||hov){
313
+ ctx.font='9px "Courier New"';ctx.fillStyle=hov?'#fff':'#aaa';ctx.textAlign='center';ctx.textBaseline='top';
314
+ ctx.fillText((n.label||n.id.split(':').pop()).substring(0,18),p.x,p.y+sz+3);
315
+ }
254
316
  }
255
317
  ctx.restore();
318
+
319
+ // HUD legend
320
+ ctx.font='10px "Courier New"';ctx.textAlign='left';
321
+ const leg=[['📦 Model','#4ecca3'],['🎮 Controller','#e94560'],['─── uses','rgba(200,100,130,.8)']];
322
+ for(let i=0;i<leg.length;i++){ctx.fillStyle=leg[i][1];ctx.fillText(leg[i][0],10,h-40+i*14);}
256
323
  }
257
324
 
258
325
  function setupCanvas(){
259
326
  const c=document.getElementById('graph-canvas');
260
- c.addEventListener('mousedown',e=>{S.dragging=true;S.dragStart={x:e.clientX-S.pan.x,y:e.clientY-S.pan.y};c.style.cursor='grabbing';});
327
+ c.addEventListener('mousedown',e=>{S.dragging=true;S._userPanned=true;S.dragStart={x:e.clientX-S.pan.x,y:e.clientY-S.pan.y};c.style.cursor='grabbing';});
261
328
  c.addEventListener('mousemove',e=>{
262
329
  if(S.dragging){S.pan.x=e.clientX-S.dragStart.x;S.pan.y=e.clientY-S.dragStart.y;renderCanvas();}
263
330
  const rect=c.getBoundingClientRect(),mx=(e.clientX-rect.left-S.pan.x)/S.zoom,my=(e.clientY-rect.top-S.pan.y)/S.zoom;
@@ -288,7 +355,34 @@ function updateAll(){
288
355
  function renderModList(){
289
356
  const el=document.getElementById('mod-list'),mods=S.graph.nodes.filter(n=>n.type==='module');
290
357
  if(!mods.length){el.innerHTML='<div style="padding:8px;color:#555;font-size:10px">No modules found</div>';return;}
291
- el.innerHTML=mods.map(m=>'<div class="mod-item"><div class="dot '+m.status+'"></div>'+esc(m.label||m.id.replace('module:',''))+'</div>').join('');
358
+ // Sort by member count descending
359
+ const sorted=mods.sort((a,b)=>{
360
+ const ca=S.modMeta&&S.modMeta.get(a.label)?S.modMeta.get(a.label).count:0;
361
+ const cb=S.modMeta&&S.modMeta.get(b.label)?S.modMeta.get(b.label).count:0;
362
+ return cb-ca;
363
+ });
364
+ el.innerHTML=sorted.map(m=>{
365
+ const meta=S.modMeta&&S.modMeta.get(m.label);
366
+ const cnt=meta?meta.count:'';
367
+ const col=meta?meta.color:'#888';
368
+ return '<div class="mod-item" data-mod="'+esc(m.label)+'" style="border-left:3px solid '+col+';padding-left:6px">'
369
+ +'<div class="dot '+m.status+'"></div>'
370
+ +esc(m.label)+' <span style="color:#555;font-size:9px">('+cnt+')</span></div>';
371
+ }).join('');
372
+ // Click to navigate
373
+ el.querySelectorAll('.mod-item').forEach(item=>{
374
+ item.addEventListener('click',()=>{
375
+ const name=item.getAttribute('data-mod');
376
+ const meta=S.modMeta&&S.modMeta.get(name);
377
+ if(meta){
378
+ const c=document.getElementById('graph-canvas');
379
+ S.zoom=0.8;S._userPanned=true;
380
+ S.pan.x=c.clientWidth/2-meta.cx*S.zoom;
381
+ S.pan.y=c.clientHeight/2-meta.cy*S.zoom;
382
+ renderCanvas();
383
+ }
384
+ });
385
+ });
292
386
  }
293
387
  function renderAgentSB(){
294
388
  document.getElementById('agent-sidebar').innerHTML=S.agents.map(a=>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencroc",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
4
4
  "description": "AI-native E2E testing framework — source-aware test generation, intelligent validation, and self-healing",
5
5
  "keywords": [
6
6
  "e2e",