infinicode 2.8.97 → 2.8.99
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.
- package/dist/kernel/federation/dashboard-html.d.ts +1 -1
- package/dist/kernel/federation/dashboard-html.js +673 -82
- package/dist/robopark/python-env.d.ts +1 -0
- package/dist/robopark/python-env.js +3 -0
- package/dist/robopark/robot-runtime.js +1 -1
- package/dist/robopark/vision-agent-launcher.js +19 -3
- package/docs/ROBOPARK_PRODUCTION_MEMORY.md +21 -0
- package/package.json +1 -1
- package/packages/robopark/scheduler/main.py +298 -22
- package/packages/robopark/scheduler/robot_supervisor.py +83 -5
- package/packages/robopark/vision/motor_server.py +204 -88
- package/packages/robopark/vision/requirements_vision_agent.txt +1 -0
|
@@ -362,18 +362,25 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
362
362
|
.pk-kv .l{font-size:0.56rem;text-transform:uppercase;letter-spacing:0.05em;color:var(--muted);}
|
|
363
363
|
.pk-ctl{display:flex;gap:0.45rem;flex-wrap:wrap;}
|
|
364
364
|
.pk-note{font-family:var(--mono);font-size:0.58rem;color:var(--gold);opacity:0.85;margin-top:0.4rem;}
|
|
365
|
-
.pk-pipeline{display:grid;gap:0.
|
|
366
|
-
.pk-pipe-step{display:grid;grid-template-columns:
|
|
367
|
-
.pk-pipe-step
|
|
368
|
-
.pk-pipe-step.
|
|
369
|
-
.pk-pipe-step.
|
|
370
|
-
.pk-pipe-step.
|
|
371
|
-
.pk-pipe-step
|
|
372
|
-
.pk-pipe-step
|
|
373
|
-
.pk-
|
|
365
|
+
.pk-pipeline{display:grid;gap:0.42rem;margin-top:0.7rem;position:relative;}
|
|
366
|
+
.pk-pipe-step{--phase:var(--gold);display:grid;grid-template-columns:2rem minmax(7.2rem,.72fr) minmax(9rem,1.35fr);gap:0.55rem;align-items:center;padding:0.55rem 0.62rem;border-radius:11px;border:1px solid rgba(255,255,255,.06);background:linear-gradient(110deg,color-mix(in srgb,var(--phase) 7%,transparent),rgba(255,255,255,.018));font-family:var(--mono);font-size:0.58rem;color:var(--muted);transition:transform .18s,border-color .18s,background .18s;position:relative;overflow:hidden;}
|
|
367
|
+
.pk-pipe-step.phase-hardware{--phase:#63d5c6}.pk-pipe-step.phase-trigger{--phase:#efb94c}.pk-pipe-step.phase-session{--phase:#66a8ff}.pk-pipe-step.phase-media{--phase:#68d48f}.pk-pipe-step.phase-intelligence{--phase:#e38bff}.pk-pipe-step.phase-playback{--phase:#ff9b62}.pk-pipe-step.phase-close{--phase:#a8b0bd}
|
|
368
|
+
.pk-pipe-step .step-index{display:grid;place-items:center;width:1.65rem;height:1.65rem;border-radius:8px;border:1px solid color-mix(in srgb,var(--phase) 35%,transparent);color:var(--phase);background:color-mix(in srgb,var(--phase) 10%,transparent);font-weight:700;}
|
|
369
|
+
.pk-pipe-step .stage{color:var(--text);text-transform:uppercase;letter-spacing:.055em;font-weight:650;display:block;}.pk-pipe-step .phase{display:block;color:var(--phase);font-size:.49rem;letter-spacing:.08em;margin-top:.14rem;}
|
|
370
|
+
.pk-pipe-step .detail{text-align:right;overflow-wrap:anywhere;line-height:1.35;}.pk-pipe-step .time{display:block;color:color-mix(in srgb,var(--muted) 75%,transparent);font-size:.5rem;margin-top:.12rem;}
|
|
371
|
+
.pk-pipe-step.ok{border-color:color-mix(in srgb,var(--ok) 22%,transparent)}
|
|
372
|
+
.pk-pipe-step.failed,.pk-pipe-step.blocked{border-color:rgba(255,90,90,.48);background:linear-gradient(110deg,rgba(255,70,70,.14),rgba(255,255,255,.02));}
|
|
373
|
+
.pk-pipe-step.current{transform:translateX(4px);border-color:var(--phase);background:linear-gradient(105deg,color-mix(in srgb,var(--phase) 19%,transparent),rgba(255,255,255,.035));box-shadow:0 0 0 1px color-mix(in srgb,var(--phase) 18%,transparent),0 8px 28px rgba(0,0,0,.22);}
|
|
374
|
+
.pk-pipe-step.current:after{content:'ACTIVE';position:absolute;right:-1.7rem;top:.45rem;transform:rotate(38deg);padding:.12rem 2rem;background:var(--phase);color:#080808;font-size:.42rem;font-weight:800;letter-spacing:.08em;}
|
|
375
|
+
.pk-pipe-step.current .step-index{box-shadow:0 0 14px color-mix(in srgb,var(--phase) 55%,transparent);animation:pulse 1.15s infinite;}
|
|
376
|
+
.pk-pipeline-summary{padding:0.7rem 0.75rem;border:1px solid var(--glass-border);border-radius:11px;background:linear-gradient(120deg,rgba(212,170,72,.08),rgba(255,255,255,.018));font-family:var(--mono);font-size:0.62rem;color:var(--muted);line-height:1.45;}
|
|
374
377
|
.pk-pipeline-summary.ready{color:var(--ok);border-color:rgba(70,220,150,.35);}
|
|
375
378
|
.pk-pipeline-summary.running{color:var(--warn);border-color:rgba(255,196,70,.35);}
|
|
376
379
|
.pk-pipeline-summary.blocked{color:var(--bad);border-color:rgba(255,90,90,.4);}
|
|
380
|
+
.pk-pipe-history{margin-top:.75rem;border-top:1px solid var(--glass-border);padding-top:.7rem}.pk-pipe-history-head{display:flex;align-items:center;justify-content:space-between;gap:.6rem;margin-bottom:.5rem}.pk-pipe-history-head b{font-family:var(--mono);font-size:.65rem;color:var(--ink);text-transform:uppercase;letter-spacing:.06em}.pk-pipe-history-list{display:grid;gap:.45rem}.pk-run-card{border:1px solid var(--glass-border);border-radius:10px;background:var(--panel-2);padding:.55rem .62rem;cursor:pointer}.pk-run-card:hover{border-color:var(--gold)}.pk-run-top{display:grid;grid-template-columns:1fr auto;gap:.5rem;align-items:start}.pk-run-title{font-family:var(--mono);font-size:.61rem;color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pk-run-meta{font-family:var(--mono);font-size:.53rem;color:var(--muted);margin-top:.17rem}.pk-run-state{font-family:var(--mono);font-size:.5rem;text-transform:uppercase;border:1px solid var(--glass-border);border-radius:999px;padding:.18rem .4rem}.pk-run-state.ok{color:var(--ok);border-color:color-mix(in srgb,var(--ok) 45%,transparent)}.pk-run-state.bad{color:var(--bad);border-color:color-mix(in srgb,var(--bad) 45%,transparent)}.pk-run-state.running{color:var(--warn);border-color:color-mix(in srgb,var(--warn) 45%,transparent)}.pk-run-stages{display:flex;gap:3px;margin-top:.45rem}.pk-run-stages i{height:4px;flex:1;border-radius:3px;background:rgba(255,255,255,.07)}.pk-run-stages i.ok{background:var(--ok)}.pk-run-stages i.failed,.pk-run-stages i.blocked{background:var(--bad)}.pk-history-pager{display:flex;align-items:center;gap:.35rem}.pk-history-pager span{font-family:var(--mono);font-size:.54rem;color:var(--muted)}
|
|
381
|
+
.pk-mic-suite{margin-top:.6rem;display:grid;gap:.55rem}.pk-mic-health{display:grid;grid-template-columns:repeat(3,1fr);gap:.4rem}.pk-mic-check{padding:.48rem;border:1px solid var(--glass-border);border-radius:9px;background:var(--panel-2)}.pk-mic-check .k{font-family:var(--mono);font-size:.49rem;color:var(--muted);text-transform:uppercase}.pk-mic-check .v{font-family:var(--mono);font-size:.62rem;color:var(--ink);margin-top:.16rem;overflow-wrap:anywhere}.pk-mic-check.ok{border-color:color-mix(in srgb,var(--ok) 30%,transparent)}.pk-mic-check.bad{border-color:color-mix(in srgb,var(--bad) 45%,transparent)}.pk-mic-result{border:1px solid var(--glass-border);border-radius:10px;padding:.58rem;background:var(--panel-2);font-family:var(--mono);font-size:.58rem}.pk-mic-result.ok{border-color:var(--ok);background:rgba(60,180,110,.08)}.pk-mic-result.bad{border-color:var(--bad);background:rgba(220,80,80,.08)}.pk-mic-readings{display:grid;grid-template-columns:repeat(4,1fr);gap:.35rem;margin-top:.45rem}.pk-mic-reading{padding:.35rem;background:rgba(0,0,0,.16);border-radius:7px}.pk-mic-reading span{display:block;color:var(--muted);font-size:.48rem;text-transform:uppercase}.pk-mic-reading b{display:block;color:var(--ink);font-size:.67rem;margin-top:.1rem}
|
|
382
|
+
.pk-motor-registry{display:grid;gap:.4rem}.pk-motor-row{display:grid;grid-template-columns:1fr 4.5rem 5.5rem auto;gap:.35rem;align-items:end;padding:.45rem;border:1px solid var(--glass-border);border-radius:9px;background:var(--panel-2)}.pk-motor-canvas{display:flex;align-items:stretch;gap:.55rem;overflow-x:auto;padding:.65rem .2rem;min-height:130px}.pk-motor-node{min-width:165px;padding:.55rem;border:1px solid rgba(255,205,93,.35);border-radius:12px;background:linear-gradient(145deg,rgba(201,150,46,.13),rgba(255,255,255,.025));position:relative}.pk-motor-node:not(:last-child):after{content:'';position:absolute;right:-.56rem;top:50%;width:.56rem;border-top:2px solid var(--gold)}.pk-motor-node .node-num{font-family:var(--mono);font-size:.48rem;color:var(--gold);text-transform:uppercase;margin-bottom:.35rem}.pk-motor-node .rp-field{margin-bottom:.3rem}.pk-motor-empty{display:grid;place-items:center;width:100%;min-height:100px;border:1px dashed var(--glass-border);border-radius:10px;color:var(--muted);font-family:var(--mono);font-size:.58rem}.pk-motor-status{font-family:var(--mono);font-size:.58rem;padding:.5rem;border-radius:8px;background:var(--panel-2);border:1px solid var(--glass-border)}.pk-motor-status.ok{color:var(--ok);border-color:var(--ok)}.pk-motor-status.bad{color:var(--bad);border-color:var(--bad)}
|
|
383
|
+
@media(max-width:720px){.pk-pipe-step{grid-template-columns:1.8rem 1fr}.pk-pipe-step .detail{grid-column:2;text-align:left}.pk-mic-health{grid-template-columns:1fr}.pk-mic-readings{grid-template-columns:1fr 1fr}.pk-motor-row{grid-template-columns:1fr 4.5rem}}
|
|
377
384
|
.pk-prodtoggle{display:flex;align-items:center;gap:0.5rem;margin-top:0.6rem;padding:0.45rem 0.6rem;border-radius:8px;border:1px solid var(--border-strong);background:var(--panel-2);cursor:pointer;}
|
|
378
385
|
.pk-prodtoggle input{width:15px;height:15px;accent-color:var(--gold-bright,var(--gold));cursor:pointer;}
|
|
379
386
|
.pk-prodtoggle span{font-family:var(--mono);font-size:0.66rem;color:var(--ink);}
|
|
@@ -4070,8 +4077,27 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
4070
4077
|
];
|
|
4071
4078
|
// Pond / curved water feature at the lower-left of the plaza.
|
|
4072
4079
|
var POND={cx:0.18*15, cy:0.84*8, rx:2.4, ry:1.1};
|
|
4073
|
-
// Street traffic along the top road
|
|
4074
|
-
|
|
4080
|
+
// Street traffic along the top road. Each car has x, y (world), color,
|
|
4081
|
+
// a per-car speed (world units / second), a direction (+1 = right, -1 =
|
|
4082
|
+
// left). y is one of two lane bands (≈ -0.65 for outbound, ≈ -1.18 for
|
|
4083
|
+
// inbound) so cars don't overlap. Speeds are deliberately varied and
|
|
4084
|
+
// fast enough to read as movement at 60fps — the road is short, so
|
|
4085
|
+
// a 4-7s pass across the screen feels natural.
|
|
4086
|
+
var ROAD_Y0=-1.5, ROAD_Y1=-0.35;
|
|
4087
|
+
var CARS=[
|
|
4088
|
+
{x:1.5, y:-0.65, color:'#f2c14e', speed:3.2, dir:+1},
|
|
4089
|
+
{x:4.0, y:-1.18, color:'#7fb4ff', speed:3.6, dir:-1},
|
|
4090
|
+
{x:6.5, y:-0.65, color:'#e0a23c', speed:2.8, dir:+1},
|
|
4091
|
+
{x:9.0, y:-1.18, color:'#ff7e6e', speed:4.0, dir:-1},
|
|
4092
|
+
{x:11.5, y:-0.65, color:'#d8d8e0', speed:3.0, dir:+1},
|
|
4093
|
+
{x:0.5, y:-1.18, color:'#5b8bd6', speed:3.4, dir:-1},
|
|
4094
|
+
{x:13.0, y:-0.65, color:'#c87ed4', speed:2.6, dir:+1},
|
|
4095
|
+
{x:7.0, y:-1.18, color:'#f2c14e', speed:3.8, dir:-1},
|
|
4096
|
+
];
|
|
4097
|
+
// Per-car phase offsets so headlights / taillights / wheels don't all
|
|
4098
|
+
// blink in lockstep — gives a natural "different cars, different
|
|
4099
|
+
// speeds" feel.
|
|
4100
|
+
var _carPhase = CARS.map(function(_, i){ return i * 0.7; });
|
|
4075
4101
|
// ── Walking people — looping sprites that stroll along fixed plaza paths.
|
|
4076
4102
|
// Each walker is a small canvas-drawn figure (head + body + two
|
|
4077
4103
|
// swinging legs + swinging arms) with a vertical bob and a slight
|
|
@@ -4463,39 +4489,31 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
4463
4489
|
if(fill){ X.fillStyle=fill; X.fill(); }
|
|
4464
4490
|
if(stroke){ X.strokeStyle=stroke; X.lineWidth=lw||1; X.stroke(); }
|
|
4465
4491
|
}
|
|
4466
|
-
function drawGround(){
|
|
4492
|
+
function drawGround(now){
|
|
4493
|
+
// now is optional — only the road cars and water effects need a
|
|
4494
|
+
// wall-clock. We pass performance.now() in from frame() so every
|
|
4495
|
+
// animation reads the same time.
|
|
4496
|
+
if(typeof now !== 'number') now = performance.now();
|
|
4497
|
+
var _now = now;
|
|
4467
4498
|
var z=cam.z;
|
|
4468
|
-
// ── grass base —
|
|
4469
|
-
//
|
|
4470
|
-
//
|
|
4471
|
-
//
|
|
4499
|
+
// ── grass base — single uniform mid-green, no radial brightening ──
|
|
4500
|
+
// Park sits on a floating island now, so we want a flat, calm
|
|
4501
|
+
// grass tone across the whole field. The previous radial gradient
|
|
4502
|
+
// pushed a lighter green toward the center which read as a "spot"
|
|
4503
|
+
// — replaced with a single soft fill so the island feels like a
|
|
4504
|
+
// single piece of turf.
|
|
4472
4505
|
var parkCx=PARK_W/2, parkCy=PARK_D/2;
|
|
4473
4506
|
var gCent=iso(parkCx,parkCy);
|
|
4474
4507
|
var gRad=X.createRadialGradient(gCent.x,gCent.y,12*z,gCent.x,gCent.y,Math.max(W,H)*0.62);
|
|
4475
|
-
gRad.addColorStop(0,'rgba(
|
|
4476
|
-
gRad.addColorStop(0.
|
|
4477
|
-
gRad.addColorStop(1,'rgba(
|
|
4508
|
+
gRad.addColorStop(0,'rgba(94,150,84,0.95)');
|
|
4509
|
+
gRad.addColorStop(0.5,'rgba(86,140,76,0.95)');
|
|
4510
|
+
gRad.addColorStop(1,'rgba(78,128,68,0.95)');
|
|
4478
4511
|
// grass field — tight margin around the plaza + lot, NOT covering the road or the beach
|
|
4479
4512
|
var GF_X0=0.0, GF_X1=PARK_W, GF_Y0=-0.32, GF_Y1=PARK_D-0.55;
|
|
4480
4513
|
poly([[GF_X0,GF_Y0],[GF_X1,GF_Y0],[GF_X1,GF_Y1],[GF_X0,GF_Y1]], gRad, 'rgba(140,200,150,0.10)',1);
|
|
4481
|
-
//
|
|
4482
|
-
//
|
|
4483
|
-
|
|
4484
|
-
var stripeColors=['rgba(108,162,90,0.45)','rgba(86,140,72,0.35)'];
|
|
4485
|
-
var stripeCount=5;
|
|
4486
|
-
var stripeW=(GF_X1-GF_X0+GF_Y1-GF_Y0+2)/stripeCount;
|
|
4487
|
-
for(var si=0;si<stripeCount;si++){
|
|
4488
|
-
var off=si*stripeW - ((GF_X1-GF_X0)+(GF_Y1-GF_Y0))*0.5;
|
|
4489
|
-
// Build a parallelogram in world coords that runs from left-bottom to right-top
|
|
4490
|
-
// at a slight angle (skewed +X, -Y direction) — looks like a stadium mowed lawn.
|
|
4491
|
-
// Tightly fitted to the grass field bounds.
|
|
4492
|
-
var p1=[GF_X0 + off, GF_Y1];
|
|
4493
|
-
var p2=[GF_X0 + off + 1.0, GF_Y0];
|
|
4494
|
-
var p3=[GF_X0 + off + 1.0 + (GF_X1-GF_X0), GF_Y0];
|
|
4495
|
-
var p4=[GF_X0 + off + (GF_X1-GF_X0), GF_Y1];
|
|
4496
|
-
poly([p1,p2,p3,p4], stripeColors[si%2], null, 0);
|
|
4497
|
-
}
|
|
4498
|
-
X.restore();
|
|
4514
|
+
// (mowed-stripe effect removed — was the lighter green alternating
|
|
4515
|
+
// bands on the sides of the park. Keeping the lawn as a single
|
|
4516
|
+
// uniform tone so the park reads as a clean floating island.)
|
|
4499
4517
|
// ── outer hedge / foliage border just inside the grass field ──
|
|
4500
4518
|
// Slightly ragged inner edge: extra points along each side to break the straight line.
|
|
4501
4519
|
// The hedge wraps the grass field, which now hugs the plaza footprint (no side overhang).
|
|
@@ -4566,16 +4584,197 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
4566
4584
|
X.closePath();
|
|
4567
4585
|
X.strokeStyle='rgba(96,148,86,0.7)'; X.lineWidth=0.8*z; X.stroke();
|
|
4568
4586
|
X.restore();
|
|
4569
|
-
// grass noise — scattered
|
|
4587
|
+
// grass noise — scattered darker tufts for texture (no lighter
|
|
4588
|
+
// tufts, so the field stays a single uniform mid-green)
|
|
4570
4589
|
X.save();
|
|
4571
|
-
for(var ni=0;ni<
|
|
4590
|
+
for(var ni=0;ni<40;ni++){
|
|
4572
4591
|
var gnx=GF_X0 + ((ni*37)%100)/100*(GF_X1-GF_X0);
|
|
4573
4592
|
var gny=GF_Y0 + ((ni*53)%100)/100*(GF_Y1-GF_Y0);
|
|
4574
4593
|
var gp=iso(gnx,gny);
|
|
4575
|
-
X.fillStyle=
|
|
4594
|
+
X.fillStyle='rgba(56,112,62,0.22)';
|
|
4576
4595
|
X.beginPath(); X.arc(gp.x+((ni*7)%13-6)*z,gp.y+((ni*11)%9-4)*z,1.6*z,0,7); X.fill();
|
|
4577
4596
|
}
|
|
4578
4597
|
X.restore();
|
|
4598
|
+
// ── GOLD BORDER + FLOATING ISLAND DEPTH ──
|
|
4599
|
+
// The whole park now reads as a single floating slab of turf in
|
|
4600
|
+
// the sky. We render (in world space, around the grass field):
|
|
4601
|
+
// 1. a soft drop-shadow ellipse on the sky below the island
|
|
4602
|
+
// 2. a tapered rocky underside (darker, hanging from the edge)
|
|
4603
|
+
// 3. a thick gold rim tracing the grass perimeter
|
|
4604
|
+
// 4. a thin warm-glow outer ring (sells the gold as "lit by
|
|
4605
|
+
// the warm sun")
|
|
4606
|
+
// The underside and drop shadow give the park real depth so it
|
|
4607
|
+
// doesn't look like a flat sticker on the sky.
|
|
4608
|
+
// Build the grass perimeter polygon once (offset slightly outward
|
|
4609
|
+
// so the gold rim sits just outside the turf edge).
|
|
4610
|
+
var GRASS_RIM = 0.18; // world units of gold rim thickness
|
|
4611
|
+
function grassPerim(offset){
|
|
4612
|
+
// Use the same world corners as the grass field, offset outward.
|
|
4613
|
+
var o = offset;
|
|
4614
|
+
return [
|
|
4615
|
+
[GF_X0-o, GF_Y0-o],
|
|
4616
|
+
[GF_X1+o, GF_Y0-o],
|
|
4617
|
+
[GF_X1+o, GF_Y1+o],
|
|
4618
|
+
[GF_X0-o, GF_Y1+o]
|
|
4619
|
+
];
|
|
4620
|
+
}
|
|
4621
|
+
// 1. drop shadow on the sky — a wide soft ellipse below the
|
|
4622
|
+
// island's bottom edge (in screen space, large + soft).
|
|
4623
|
+
var botL = iso(GF_X0, GF_Y1);
|
|
4624
|
+
var botR = iso(GF_X1, GF_Y1);
|
|
4625
|
+
var botM = { x:(botL.x+botR.x)/2, y:(botL.y+botR.y)/2 };
|
|
4626
|
+
var islandWidth = Math.abs(botR.x - botL.x);
|
|
4627
|
+
var shadowCx = botM.x;
|
|
4628
|
+
var shadowCy = botM.y + 28*z;
|
|
4629
|
+
var shadowRX = islandWidth * 0.55;
|
|
4630
|
+
var shadowRY = 22 * z;
|
|
4631
|
+
var dropShadow = X.createRadialGradient(shadowCx, shadowCy, 0, shadowCx, shadowCy, shadowRX);
|
|
4632
|
+
dropShadow.addColorStop(0, 'rgba(20, 24, 36, 0.55)');
|
|
4633
|
+
dropShadow.addColorStop(0.5, 'rgba(20, 24, 36, 0.25)');
|
|
4634
|
+
dropShadow.addColorStop(1, 'rgba(20, 24, 36, 0)');
|
|
4635
|
+
X.fillStyle = dropShadow;
|
|
4636
|
+
X.beginPath();
|
|
4637
|
+
X.ellipse(shadowCx, shadowCy, shadowRX, shadowRY, 0, 0, Math.PI*2);
|
|
4638
|
+
X.fill();
|
|
4639
|
+
// 2. rocky underside — draw the grass field polygon again, but
|
|
4640
|
+
// offset down by ~14*z and slightly darker, then close it into
|
|
4641
|
+
// a wedge. We do this with two filled paths:
|
|
4642
|
+
// - the bottom face: same perimeter polygon shifted down
|
|
4643
|
+
// - the side rim: a band that traces from the top of the
|
|
4644
|
+
// bottom face up to the top of the island (so the side
|
|
4645
|
+
// of the slab reads as dirt/rock)
|
|
4646
|
+
var UNDERSIDE_LIFT = 16 * z;
|
|
4647
|
+
var topPerim = grassPerim(0);
|
|
4648
|
+
var botPerim = topPerim.map(function(p){ return [p[0], p[1] + UNDERSIDE_LIFT / (TH*z) * (1/22)]; });
|
|
4649
|
+
// Convert botPerim into screen-space points (slightly down from top).
|
|
4650
|
+
var topScreen = topPerim.map(function(p){ return iso(p[0], p[1]); });
|
|
4651
|
+
var botScreen = topPerim.map(function(p){ return iso(p[0], p[1] + 1.0); });
|
|
4652
|
+
// right side face
|
|
4653
|
+
X.save();
|
|
4654
|
+
X.fillStyle = 'rgba(38, 50, 36, 0.92)';
|
|
4655
|
+
X.beginPath();
|
|
4656
|
+
X.moveTo(topScreen[1].x, topScreen[1].y);
|
|
4657
|
+
X.lineTo(topScreen[2].x, topScreen[2].y);
|
|
4658
|
+
X.lineTo(botScreen[2].x, botScreen[2].y + UNDERSIDE_LIFT);
|
|
4659
|
+
X.lineTo(botScreen[1].x, botScreen[1].y + UNDERSIDE_LIFT);
|
|
4660
|
+
X.closePath();
|
|
4661
|
+
X.fill();
|
|
4662
|
+
// bottom-right edge highlight (slight bevel)
|
|
4663
|
+
X.strokeStyle = 'rgba(80, 96, 60, 0.55)';
|
|
4664
|
+
X.lineWidth = 1.0*z;
|
|
4665
|
+
X.beginPath();
|
|
4666
|
+
X.moveTo(topScreen[2].x, topScreen[2].y);
|
|
4667
|
+
X.lineTo(botScreen[2].x, botScreen[2].y + UNDERSIDE_LIFT);
|
|
4668
|
+
X.stroke();
|
|
4669
|
+
// left side face
|
|
4670
|
+
X.fillStyle = 'rgba(28, 38, 26, 0.95)';
|
|
4671
|
+
X.beginPath();
|
|
4672
|
+
X.moveTo(topScreen[0].x, topScreen[0].y);
|
|
4673
|
+
X.lineTo(topScreen[1].x, topScreen[1].y);
|
|
4674
|
+
X.lineTo(botScreen[1].x, botScreen[1].y + UNDERSIDE_LIFT);
|
|
4675
|
+
X.lineTo(botScreen[0].x, botScreen[0].y + UNDERSIDE_LIFT);
|
|
4676
|
+
X.closePath();
|
|
4677
|
+
X.fill();
|
|
4678
|
+
// bottom-left edge highlight
|
|
4679
|
+
X.strokeStyle = 'rgba(70, 84, 52, 0.45)';
|
|
4680
|
+
X.beginPath();
|
|
4681
|
+
X.moveTo(topScreen[0].x, topScreen[0].y);
|
|
4682
|
+
X.lineTo(botScreen[0].x, botScreen[0].y + UNDERSIDE_LIFT);
|
|
4683
|
+
X.stroke();
|
|
4684
|
+
// bottom face — slightly tapered into a rough point so the slab
|
|
4685
|
+
// doesn't look like a flat box. We taper the bottom by ~25%.
|
|
4686
|
+
var tapered = topScreen.map(function(s, i){
|
|
4687
|
+
if(i === 0) return { x: s.x + 40*z, y: s.y + UNDERSIDE_LIFT*0.95 };
|
|
4688
|
+
if(i === 1) return { x: s.x - 40*z, y: s.y + UNDERSIDE_LIFT*0.95 };
|
|
4689
|
+
if(i === 2) return { x: s.x - 60*z, y: s.y + UNDERSIDE_LIFT*0.85 };
|
|
4690
|
+
if(i === 3) return { x: s.x + 60*z, y: s.y + UNDERSIDE_LIFT*0.85 };
|
|
4691
|
+
return { x: s.x, y: s.y + UNDERSIDE_LIFT };
|
|
4692
|
+
});
|
|
4693
|
+
X.fillStyle = 'rgba(20, 28, 20, 0.92)';
|
|
4694
|
+
X.beginPath();
|
|
4695
|
+
X.moveTo(topScreen[0].x, topScreen[0].y);
|
|
4696
|
+
X.lineTo(topScreen[1].x, topScreen[1].y);
|
|
4697
|
+
X.lineTo(tapered[1].x, tapered[1].y);
|
|
4698
|
+
X.lineTo(tapered[2].x, tapered[2].y);
|
|
4699
|
+
X.lineTo(tapered[3].x, tapered[3].y);
|
|
4700
|
+
X.lineTo(tapered[0].x, tapered[0].y);
|
|
4701
|
+
X.closePath();
|
|
4702
|
+
X.fill();
|
|
4703
|
+
// a few rocky stalactite bumps hanging from the underside so it
|
|
4704
|
+
// doesn't look like a perfect box either
|
|
4705
|
+
var bumpSpots = [
|
|
4706
|
+
{ px: 0.18, len: 0.65 },
|
|
4707
|
+
{ px: 0.34, len: 0.85 },
|
|
4708
|
+
{ px: 0.52, len: 0.55 },
|
|
4709
|
+
{ px: 0.68, len: 0.95 },
|
|
4710
|
+
{ px: 0.86, len: 0.70 }
|
|
4711
|
+
];
|
|
4712
|
+
for(var bpi=0; bpi<bumpSpots.length; bpi++){
|
|
4713
|
+
var bx = GF_X0 + bumpSpots[bpi].px * (GF_X1-GF_X0);
|
|
4714
|
+
var by = GF_Y1;
|
|
4715
|
+
var bp = iso(bx, by);
|
|
4716
|
+
var bpLen = bumpSpots[bpi].len * UNDERSIDE_LIFT;
|
|
4717
|
+
X.fillStyle = 'rgba(24, 32, 22, 0.88)';
|
|
4718
|
+
X.beginPath();
|
|
4719
|
+
X.moveTo(bp.x - 14*z, bp.y + 2*z);
|
|
4720
|
+
// 2-sided stalactite that tapers to a point
|
|
4721
|
+
X.quadraticCurveTo(bp.x, bp.y + bpLen*0.5, bp.x, bp.y + bpLen);
|
|
4722
|
+
X.quadraticCurveTo(bp.x, bp.y + bpLen*0.5, bp.x + 14*z, bp.y + 2*z);
|
|
4723
|
+
X.closePath();
|
|
4724
|
+
X.fill();
|
|
4725
|
+
}
|
|
4726
|
+
X.restore();
|
|
4727
|
+
// 3. GOLD RIM — thick gold band tracing the grass perimeter.
|
|
4728
|
+
// Drawn AFTER the underside so the top edge of the gold
|
|
4729
|
+
// sits flush with the top of the grass. Two strokes: a wide
|
|
4730
|
+
// gold core + a thin lighter inner highlight on the top edge.
|
|
4731
|
+
var rimOuter = grassPerim(GRASS_RIM);
|
|
4732
|
+
var rimInner = grassPerim(0);
|
|
4733
|
+
var rimOuterScreen = rimOuter.map(function(p){ return iso(p[0], p[1]); });
|
|
4734
|
+
var rimInnerScreen = rimInner.map(function(p){ return iso(p[0], p[1]); });
|
|
4735
|
+
X.save();
|
|
4736
|
+
// outer glow
|
|
4737
|
+
var glowGrad = X.createLinearGradient(0, rimOuterScreen[0].y - 30, 0, rimOuterScreen[3].y + 30);
|
|
4738
|
+
X.fillStyle = 'rgba(255, 200, 110, 0.0)';
|
|
4739
|
+
// fill the rim band as a quad
|
|
4740
|
+
X.beginPath();
|
|
4741
|
+
X.moveTo(rimOuterScreen[0].x, rimOuterScreen[0].y);
|
|
4742
|
+
X.lineTo(rimOuterScreen[1].x, rimOuterScreen[1].y);
|
|
4743
|
+
X.lineTo(rimOuterScreen[2].x, rimOuterScreen[2].y);
|
|
4744
|
+
X.lineTo(rimOuterScreen[3].x, rimOuterScreen[3].y);
|
|
4745
|
+
X.closePath();
|
|
4746
|
+
X.fill();
|
|
4747
|
+
// gold band via two strokes: thick base + thin highlight
|
|
4748
|
+
X.strokeStyle = 'rgba(255, 196, 92, 0.95)'; // base gold
|
|
4749
|
+
X.lineWidth = 5.0*z;
|
|
4750
|
+
X.beginPath();
|
|
4751
|
+
X.moveTo(rimInnerScreen[0].x, rimInnerScreen[0].y);
|
|
4752
|
+
X.lineTo(rimInnerScreen[1].x, rimInnerScreen[1].y);
|
|
4753
|
+
X.lineTo(rimInnerScreen[2].x, rimInnerScreen[2].y);
|
|
4754
|
+
X.lineTo(rimInnerScreen[3].x, rimInnerScreen[3].y);
|
|
4755
|
+
X.closePath();
|
|
4756
|
+
X.stroke();
|
|
4757
|
+
// inner highlight (thinner, brighter)
|
|
4758
|
+
X.strokeStyle = 'rgba(255, 232, 158, 0.95)';
|
|
4759
|
+
X.lineWidth = 1.4*z;
|
|
4760
|
+
X.beginPath();
|
|
4761
|
+
X.moveTo(rimInnerScreen[0].x, rimInnerScreen[0].y);
|
|
4762
|
+
X.lineTo(rimInnerScreen[1].x, rimInnerScreen[1].y);
|
|
4763
|
+
X.lineTo(rimInnerScreen[2].x, rimInnerScreen[2].y);
|
|
4764
|
+
X.lineTo(rimInnerScreen[3].x, rimInnerScreen[3].y);
|
|
4765
|
+
X.closePath();
|
|
4766
|
+
X.stroke();
|
|
4767
|
+
// outer deep-gold edge (slightly darker outer stroke for definition)
|
|
4768
|
+
X.strokeStyle = 'rgba(190, 134, 48, 0.85)';
|
|
4769
|
+
X.lineWidth = 1.2*z;
|
|
4770
|
+
X.beginPath();
|
|
4771
|
+
X.moveTo(rimOuterScreen[0].x, rimOuterScreen[0].y);
|
|
4772
|
+
X.lineTo(rimOuterScreen[1].x, rimOuterScreen[1].y);
|
|
4773
|
+
X.lineTo(rimOuterScreen[2].x, rimOuterScreen[2].y);
|
|
4774
|
+
X.lineTo(rimOuterScreen[3].x, rimOuterScreen[3].y);
|
|
4775
|
+
X.closePath();
|
|
4776
|
+
X.stroke();
|
|
4777
|
+
X.restore();
|
|
4579
4778
|
// ── flower spots — deterministic small dots in 3 colors, scattered in the lawn only ──
|
|
4580
4779
|
X.save();
|
|
4581
4780
|
var flowerColors=['rgba(232,124,164,0.85)','rgba(248,220,96,0.88)','rgba(248,248,240,0.85)'];
|
|
@@ -4690,7 +4889,12 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
4690
4889
|
poly([[0,-1.5],[PARK_W,-1.5],[PARK_W,-0.35],[0,-0.35]], 'rgba(26,26,32,0.92)', 'rgba(255,255,255,0.08)',1);
|
|
4691
4890
|
X.strokeStyle='rgba(232,210,120,0.55)'; X.setLineDash([9*z,9*z]);
|
|
4692
4891
|
var r1=iso(0.2,-0.92), r2=iso(PARK_W-0.2,-0.92); X.beginPath(); X.moveTo(r1.x,r1.y); X.lineTo(r2.x,r2.y); X.stroke(); X.setLineDash([]);
|
|
4693
|
-
|
|
4892
|
+
// animated road cars — positions updated each frame by tickCars()
|
|
4893
|
+
// draw order: outbound lane first (front), then inbound (back) for natural overlap
|
|
4894
|
+
for(var cai=0; cai<CARS.length; cai++){
|
|
4895
|
+
var car = CARS[cai];
|
|
4896
|
+
drawCar(car.x, car.y, car.color, car.dir, _carPhase[cai]||0, _now);
|
|
4897
|
+
}
|
|
4694
4898
|
// white event tent in the left grass strip (west of the plaza)
|
|
4695
4899
|
drawTent(-0.15, 0.5);
|
|
4696
4900
|
// ── lake — wider sandy shore with soft radial fade to grass ──
|
|
@@ -4743,7 +4947,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
4743
4947
|
X.restore();
|
|
4744
4948
|
// ── animated ripple/highlight — thin white elliptical arcs that drift across the lake ──
|
|
4745
4949
|
var pondCent=iso(POND.cx, POND.cy);
|
|
4746
|
-
var ripplePhase=Math.sin(
|
|
4950
|
+
var ripplePhase=Math.sin(_now/1500);
|
|
4747
4951
|
X.save();
|
|
4748
4952
|
X.strokeStyle='rgba(232,248,252,0.7)'; X.lineWidth=1.0*z; X.lineCap='round';
|
|
4749
4953
|
for(var rk=0;rk<4;rk++){
|
|
@@ -4754,7 +4958,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
4754
4958
|
var rCy2=POND.cy - 0.10 + rk*0.12;
|
|
4755
4959
|
var rA2=iso(rCx2-rxr2, rCy2);
|
|
4756
4960
|
var rB2=iso(rCx2+rxr2, rCy2);
|
|
4757
|
-
X.globalAlpha=0.35 + 0.4*Math.abs(Math.sin(
|
|
4961
|
+
X.globalAlpha=0.35 + 0.4*Math.abs(Math.sin(_now/900 + rk));
|
|
4758
4962
|
X.beginPath();
|
|
4759
4963
|
X.moveTo(rA2.x, rA2.y);
|
|
4760
4964
|
X.quadraticCurveTo(pondCent.x, rA2.y - 3*z, rB2.x, rB2.y);
|
|
@@ -4801,7 +5005,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
4801
5005
|
// ── tiny fountain — 3 concentric rings of decreasing opacity around lake centroid ──
|
|
4802
5006
|
X.save();
|
|
4803
5007
|
for(var fk=0;fk<3;fk++){
|
|
4804
|
-
var fr=((fk+1)*0.18) + 0.06*Math.abs(Math.sin(
|
|
5008
|
+
var fr=((fk+1)*0.18) + 0.06*Math.abs(Math.sin(_now/600 + fk));
|
|
4805
5009
|
var falpha=0.55 - fk*0.18;
|
|
4806
5010
|
var fcol='rgba(220,240,248,'+falpha.toFixed(3)+')';
|
|
4807
5011
|
var fringPts=[];
|
|
@@ -4937,54 +5141,271 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
4937
5141
|
X.fillStyle='rgba(232,72,72,0.95)';
|
|
4938
5142
|
X.beginPath(); X.moveTo(rdR.x,rdR.y-4*z); X.lineTo(rdR.x+2.5*z,rdR.y-3*z); X.lineTo(rdR.x,rdR.y-2*z); X.closePath(); X.fill();
|
|
4939
5143
|
}
|
|
4940
|
-
function drawCar(wx,wy,col){
|
|
5144
|
+
function drawCar(wx,wy,col,dir,phase,now){
|
|
4941
5145
|
var p=iso(wx,wy), z=cam.z;
|
|
4942
|
-
//
|
|
5146
|
+
// Optional animation params: dir (+1 right / -1 left), phase (per-car
|
|
5147
|
+
// offset in seconds), now (performance.now in ms). Falls back to the
|
|
5148
|
+
// static parked-car look when these are undefined.
|
|
5149
|
+
var moving = (dir===1 || dir===-1) && typeof now === 'number';
|
|
5150
|
+
// gentle vertical bob so a moving car looks like it has suspension
|
|
5151
|
+
var bob = moving ? Math.sin((now/180 + (phase||0)*1000)) * 0.5*z : 0;
|
|
5152
|
+
// soft drop shadow — slight motion blur for moving cars
|
|
4943
5153
|
X.fillStyle='rgba(0,0,0,0.32)';
|
|
4944
5154
|
X.beginPath(); X.ellipse(p.x,p.y+4*z,13*z,5.5*z,0,0,7); X.fill();
|
|
5155
|
+
if(moving){
|
|
5156
|
+
// ground shadow streak behind the car (opposite to direction of travel)
|
|
5157
|
+
var sx = p.x - dir*8*z;
|
|
5158
|
+
X.fillStyle='rgba(0,0,0,0.10)';
|
|
5159
|
+
X.beginPath(); X.ellipse(sx, p.y+4.5*z, 9*z, 4*z, 0, 0, 7); X.fill();
|
|
5160
|
+
// soft headlight beam projected on the road in front of the car
|
|
5161
|
+
var beamFrontX = p.x + dir*16*z;
|
|
5162
|
+
var beam = X.createRadialGradient(beamFrontX, p.y+1*z, 1*z, beamFrontX, p.y+1*z, 14*z);
|
|
5163
|
+
beam.addColorStop(0, 'rgba(255,242,200,0.32)');
|
|
5164
|
+
beam.addColorStop(0.5, 'rgba(255,242,200,0.10)');
|
|
5165
|
+
beam.addColorStop(1, 'rgba(255,242,200,0)');
|
|
5166
|
+
X.save();
|
|
5167
|
+
X.fillStyle = beam;
|
|
5168
|
+
X.beginPath(); X.ellipse(beamFrontX, p.y+1*z, 14*z, 6*z, 0, 0, Math.PI*2); X.fill();
|
|
5169
|
+
X.restore();
|
|
5170
|
+
}
|
|
4945
5171
|
// chassis (lower body)
|
|
4946
5172
|
X.fillStyle=col;
|
|
4947
|
-
X.beginPath(); X.roundRect(p.x-13*z,p.y-5*z,26*z,11*z,3*z); X.fill();
|
|
5173
|
+
X.beginPath(); X.roundRect(p.x-13*z,p.y-5*z+bob,26*z,11*z,3*z); X.fill();
|
|
4948
5174
|
// chassis bottom shade
|
|
4949
5175
|
X.fillStyle='rgba(0,0,0,0.22)';
|
|
4950
|
-
X.beginPath(); X.roundRect(p.x-13*z,p.y+2*z,26*z,4*z,2*z); X.fill();
|
|
5176
|
+
X.beginPath(); X.roundRect(p.x-13*z,p.y+2*z+bob,26*z,4*z,2*z); X.fill();
|
|
4951
5177
|
// roof (darker top)
|
|
4952
5178
|
X.fillStyle='rgba(0,0,0,0.55)';
|
|
4953
|
-
X.beginPath(); X.roundRect(p.x-8*z,p.y-10*z,16*z,6*z,2.5*z); X.fill();
|
|
5179
|
+
X.beginPath(); X.roundRect(p.x-8*z,p.y-10*z+bob,16*z,6*z,2.5*z); X.fill();
|
|
4954
5180
|
// roof tint with car color
|
|
4955
5181
|
X.fillStyle=col;
|
|
4956
5182
|
X.globalAlpha=0.6;
|
|
4957
|
-
X.beginPath(); X.roundRect(p.x-8*z,p.y-10*z,16*z,6*z,2.5*z); X.fill();
|
|
5183
|
+
X.beginPath(); X.roundRect(p.x-8*z,p.y-10*z+bob,16*z,6*z,2.5*z); X.fill();
|
|
4958
5184
|
X.globalAlpha=1;
|
|
4959
5185
|
// windshield highlight
|
|
4960
5186
|
X.fillStyle='rgba(180,220,255,0.65)';
|
|
4961
|
-
X.beginPath(); X.roundRect(p.x-6*z,p.y-9*z,12*z,3*z,1.5*z); X.fill();
|
|
5187
|
+
X.beginPath(); X.roundRect(p.x-6*z,p.y-9*z+bob,12*z,3*z,1.5*z); X.fill();
|
|
4962
5188
|
// windshield shine
|
|
4963
5189
|
X.fillStyle='rgba(255,255,255,0.45)';
|
|
4964
5190
|
X.beginPath();
|
|
4965
|
-
X.moveTo(p.x-5*z,p.y-9*z); X.lineTo(p.x-2*z,p.y-9*z); X.lineTo(p.x-4*z,p.y-7*z); X.closePath(); X.fill();
|
|
4966
|
-
// wheels
|
|
5191
|
+
X.moveTo(p.x-5*z,p.y-9*z+bob); X.lineTo(p.x-2*z,p.y-9*z+bob); X.lineTo(p.x-4*z,p.y-7*z+bob); X.closePath(); X.fill();
|
|
5192
|
+
// wheels — animated spokes when the car is moving (rotation = distance traveled)
|
|
4967
5193
|
X.fillStyle='#0c0c10';
|
|
4968
|
-
X.beginPath(); X.arc(p.x-8*z,p.y+5*z,2.6*z,0,7); X.fill();
|
|
4969
|
-
X.beginPath(); X.arc(p.x+8*z,p.y+5*z,2.6*z,0,7); X.fill();
|
|
4970
|
-
X.beginPath(); X.arc(p.x-8*z,p.y+6*z,2.6*z,0,7); X.fill();
|
|
4971
|
-
X.beginPath(); X.arc(p.x+8*z,p.y+6*z,2.6*z,0,7); X.fill();
|
|
4972
|
-
// wheel rims
|
|
5194
|
+
X.beginPath(); X.arc(p.x-8*z,p.y+5*z+bob,2.6*z,0,7); X.fill();
|
|
5195
|
+
X.beginPath(); X.arc(p.x+8*z,p.y+5*z+bob,2.6*z,0,7); X.fill();
|
|
5196
|
+
X.beginPath(); X.arc(p.x-8*z,p.y+6*z+bob,2.6*z,0,7); X.fill();
|
|
5197
|
+
X.beginPath(); X.arc(p.x+8*z,p.y+6*z+bob,2.6*z,0,7); X.fill();
|
|
5198
|
+
// wheel rims with a moving spoke mark so wheels visibly spin
|
|
4973
5199
|
X.fillStyle='rgba(200,200,210,0.75)';
|
|
4974
|
-
X.beginPath(); X.arc(p.x-8*z,p.y+5.5*z,1*z,0,7); X.fill();
|
|
4975
|
-
X.beginPath(); X.arc(p.x+8*z,p.y+5.5*z,1*z,0,7); X.fill();
|
|
4976
|
-
//
|
|
5200
|
+
X.beginPath(); X.arc(p.x-8*z,p.y+5.5*z+bob,1*z,0,7); X.fill();
|
|
5201
|
+
X.beginPath(); X.arc(p.x+8*z,p.y+5.5*z+bob,1*z,0,7); X.fill();
|
|
5202
|
+
// wheel spoke (rotates with speed) — gives the impression of motion
|
|
5203
|
+
if(moving){
|
|
5204
|
+
var spokeAng = (now/40 + (phase||0)*1000) % (Math.PI*2);
|
|
5205
|
+
var spokeDx = Math.cos(spokeAng) * 1.6*z;
|
|
5206
|
+
var spokeDy = Math.sin(spokeAng) * 1.6*z;
|
|
5207
|
+
X.strokeStyle='rgba(220,220,230,0.85)'; X.lineWidth=0.5*z;
|
|
5208
|
+
X.beginPath(); X.moveTo(p.x-8*z - spokeDx, p.y+5.5*z+bob - spokeDy); X.lineTo(p.x-8*z + spokeDx, p.y+5.5*z+bob + spokeDy); X.stroke();
|
|
5209
|
+
X.beginPath(); X.moveTo(p.x+8*z - spokeDx, p.y+5.5*z+bob - spokeDy); X.lineTo(p.x+8*z + spokeDx, p.y+5.5*z+bob + spokeDy); X.stroke();
|
|
5210
|
+
}
|
|
5211
|
+
// headlight (front — the side the car is driving toward)
|
|
5212
|
+
var frontX = p.x + (dir||1)*12*z;
|
|
4977
5213
|
X.fillStyle='rgba(255,240,180,0.95)';
|
|
4978
|
-
X.beginPath(); X.arc(
|
|
5214
|
+
X.beginPath(); X.arc(frontX, p.y-1*z+bob, 1.6*z, 0, 7); X.fill();
|
|
4979
5215
|
X.fillStyle='rgba(255,240,180,0.4)';
|
|
4980
|
-
X.beginPath(); X.arc(
|
|
4981
|
-
//
|
|
5216
|
+
X.beginPath(); X.arc(frontX, p.y-1*z+bob, 3*z, 0, 7); X.fill();
|
|
5217
|
+
// taillight (rear — opposite side, slightly redder)
|
|
5218
|
+
var rearX = p.x - (dir||1)*12*z;
|
|
4982
5219
|
X.fillStyle='rgba(255,80,80,0.85)';
|
|
4983
|
-
X.beginPath(); X.arc(
|
|
5220
|
+
X.beginPath(); X.arc(rearX, p.y-1*z+bob, 1.2*z, 0, 7); X.fill();
|
|
5221
|
+
if(moving){
|
|
5222
|
+
// a small red glow on the road behind the car
|
|
5223
|
+
X.fillStyle='rgba(255,80,80,0.25)';
|
|
5224
|
+
X.beginPath(); X.arc(rearX - dir*3*z, p.y+1*z+bob, 2.6*z, 0, 7); X.fill();
|
|
5225
|
+
}
|
|
4984
5226
|
// body highlight
|
|
4985
5227
|
X.fillStyle='rgba(255,255,255,0.18)';
|
|
4986
|
-
X.beginPath(); X.roundRect(p.x-12*z,p.y-4*z,24*z,1.5*z,1*z); X.fill();
|
|
5228
|
+
X.beginPath(); X.roundRect(p.x-12*z,p.y-4*z+bob,24*z,1.5*z,1*z); X.fill();
|
|
4987
5229
|
}
|
|
5230
|
+
|
|
5231
|
+
// ── Atmosphere: sky, sun, distant mountains, clouds, haze ──
|
|
5232
|
+
// Painted on top of the cleared canvas and below the rest of the map
|
|
5233
|
+
// so the map looks like it sits in an actual world instead of on a
|
|
5234
|
+
// blank panel. Cheap to render (a handful of full-canvas fills + a
|
|
5235
|
+
// few silhouette paths + a small cloud set) and turns the whole
|
|
5236
|
+
// scene from "miniature on a black velvet" into "park at golden hour".
|
|
5237
|
+
//
|
|
5238
|
+
// Everything is drawn in screen space, not world space, because the
|
|
5239
|
+
// sky doesn't follow the iso projection — it's a backdrop, not a
|
|
5240
|
+
// surface. Distant mountains live just above the road horizon line,
|
|
5241
|
+
// the sun is fixed near the upper-right (a warm focal point), and a
|
|
5242
|
+
// handful of soft clouds drift slowly across the sky.
|
|
5243
|
+
function drawAtmosphere(now){
|
|
5244
|
+
if(!W || !H) return;
|
|
5245
|
+
var z = cam.z;
|
|
5246
|
+
|
|
5247
|
+
// ── sky gradient — warm late-afternoon palette (top to bottom) ──
|
|
5248
|
+
// Top: deep cobalt → mid: soft sky → low: warm gold haze. Anchored
|
|
5249
|
+
// to a virtual horizon line ~38% from the top of the stage so the
|
|
5250
|
+
// map always sits on the same horizon regardless of zoom.
|
|
5251
|
+
var horizon = H * 0.42;
|
|
5252
|
+
var sky = X.createLinearGradient(0, 0, 0, H);
|
|
5253
|
+
sky.addColorStop(0, 'rgba(46, 78, 132, 0.95)'); // deep sky
|
|
5254
|
+
sky.addColorStop(0.30, 'rgba(108, 152, 196, 0.85)'); // mid sky
|
|
5255
|
+
sky.addColorStop(0.46, 'rgba(214, 184, 138, 0.78)'); // warm horizon
|
|
5256
|
+
sky.addColorStop(0.55, 'rgba(244, 222, 168, 0.62)'); // soft golden glow
|
|
5257
|
+
sky.addColorStop(0.75, 'rgba(248, 232, 200, 0.20)'); // fades into the ground area
|
|
5258
|
+
sky.addColorStop(1, 'rgba(248, 232, 200, 0)');
|
|
5259
|
+
X.fillStyle = sky;
|
|
5260
|
+
X.fillRect(0, 0, W, H);
|
|
5261
|
+
|
|
5262
|
+
// ── sun — a soft warm disc with a wide glow halo ──
|
|
5263
|
+
// Anchored at ~78% across, 22% down so it reads as the off-screen
|
|
5264
|
+
// light source warming the whole scene. The glow pulses very
|
|
5265
|
+
// gently so it doesn't feel like a static sticker.
|
|
5266
|
+
var sunX = W * 0.78;
|
|
5267
|
+
var sunY = H * 0.22;
|
|
5268
|
+
var sunR = 28 * (0.95 + 0.05 * Math.sin(now/2400));
|
|
5269
|
+
// outer warm glow
|
|
5270
|
+
var glow = X.createRadialGradient(sunX, sunY, sunR*0.6, sunX, sunY, sunR*7);
|
|
5271
|
+
glow.addColorStop(0, 'rgba(255, 222, 140, 0.55)');
|
|
5272
|
+
glow.addColorStop(0.25,'rgba(255, 210, 130, 0.30)');
|
|
5273
|
+
glow.addColorStop(0.6, 'rgba(255, 200, 120, 0.08)');
|
|
5274
|
+
glow.addColorStop(1, 'rgba(255, 200, 120, 0)');
|
|
5275
|
+
X.fillStyle = glow;
|
|
5276
|
+
X.fillRect(0, 0, W, H);
|
|
5277
|
+
// sun disc
|
|
5278
|
+
var disc = X.createRadialGradient(sunX, sunY, sunR*0.2, sunX, sunY, sunR);
|
|
5279
|
+
disc.addColorStop(0, 'rgba(255, 250, 230, 1)');
|
|
5280
|
+
disc.addColorStop(0.6, 'rgba(255, 232, 170, 0.95)');
|
|
5281
|
+
disc.addColorStop(1, 'rgba(255, 210, 130, 0)');
|
|
5282
|
+
X.fillStyle = disc;
|
|
5283
|
+
X.beginPath(); X.arc(sunX, sunY, sunR, 0, Math.PI*2); X.fill();
|
|
5284
|
+
|
|
5285
|
+
// ── distant mountain silhouette (3 receding layers) ──
|
|
5286
|
+
// Each layer uses a slightly lighter color so the eye reads depth.
|
|
5287
|
+
// Anchored just below the horizon so the iso map sits in front of
|
|
5288
|
+
// them. Heights/shapes are deterministic so the silhouette doesn't
|
|
5289
|
+
// shimmer between frames.
|
|
5290
|
+
function mountainLayer(baseY, amp, segments, color, seedOff){
|
|
5291
|
+
X.fillStyle = color;
|
|
5292
|
+
X.beginPath();
|
|
5293
|
+
X.moveTo(0, H);
|
|
5294
|
+
// sample the silhouette using a sum of low-frequency sines
|
|
5295
|
+
for(var mx=0; mx<=W; mx+=8){
|
|
5296
|
+
var t = mx / W;
|
|
5297
|
+
var h = (
|
|
5298
|
+
Math.sin(t*6.3 + seedOff) * 0.5 +
|
|
5299
|
+
Math.sin(t*12.5 + seedOff*2.3) * 0.28 +
|
|
5300
|
+
Math.sin(t*22.1 + seedOff*0.7) * 0.14
|
|
5301
|
+
) * amp;
|
|
5302
|
+
X.lineTo(mx, baseY - h);
|
|
5303
|
+
}
|
|
5304
|
+
X.lineTo(W, H);
|
|
5305
|
+
X.closePath();
|
|
5306
|
+
X.fill();
|
|
5307
|
+
}
|
|
5308
|
+
// farther layer (lighter haze, lower)
|
|
5309
|
+
mountainLayer(H*0.46, 28, 18, 'rgba(120, 132, 158, 0.45)', 1.7);
|
|
5310
|
+
// middle layer
|
|
5311
|
+
mountainLayer(H*0.48, 36, 16, 'rgba(86, 100, 124, 0.55)', 3.1);
|
|
5312
|
+
// closer ridge (darkest, in front of the plaza)
|
|
5313
|
+
mountainLayer(H*0.50, 22, 14, 'rgba(54, 68, 86, 0.60)', 0.4);
|
|
5314
|
+
|
|
5315
|
+
// ── soft cloud band — 5 elongated ellipses drifting right ──
|
|
5316
|
+
// Drawn as flat white ellipses with low alpha so they look like
|
|
5317
|
+
// wisps, not puffs. Each cloud has its own period so they don't
|
|
5318
|
+
// march in lockstep.
|
|
5319
|
+
var cloudColors = [
|
|
5320
|
+
'rgba(255,255,255,0.55)',
|
|
5321
|
+
'rgba(255,250,240,0.45)',
|
|
5322
|
+
'rgba(248,242,228,0.40)',
|
|
5323
|
+
'rgba(255,250,235,0.35)',
|
|
5324
|
+
'rgba(252,246,232,0.50)',
|
|
5325
|
+
];
|
|
5326
|
+
for(var ci=0; ci<cloudColors.length; ci++){
|
|
5327
|
+
var period = 90 + ci*22; // seconds for a full pass
|
|
5328
|
+
var drift = ((now/1000) / period) % 1; // 0..1
|
|
5329
|
+
var cx = (drift * (W + 240)) - 120; // wrap from -120 to W+120
|
|
5330
|
+
var cy = H*0.10 + ci*16;
|
|
5331
|
+
var rx = 60 + ci*8;
|
|
5332
|
+
var ry = 10 + (ci%3)*3;
|
|
5333
|
+
X.fillStyle = cloudColors[ci];
|
|
5334
|
+
// fluffy edge: 3 overlapping ellipses
|
|
5335
|
+
X.beginPath(); X.ellipse(cx, cy, rx, ry, 0, 0, Math.PI*2); X.fill();
|
|
5336
|
+
X.beginPath(); X.ellipse(cx - rx*0.5, cy + ry*0.4, rx*0.7, ry*0.7, 0, 0, Math.PI*2); X.fill();
|
|
5337
|
+
X.beginPath(); X.ellipse(cx + rx*0.5, cy + ry*0.3, rx*0.7, ry*0.6, 0, 0, Math.PI*2); X.fill();
|
|
5338
|
+
}
|
|
5339
|
+
|
|
5340
|
+
// ── soft warm light wash on the upper third of the map ──
|
|
5341
|
+
// Sits in screen space too: a wide radial glow over the entire
|
|
5342
|
+
// map area that makes the grass + plaza read as if lit from the
|
|
5343
|
+
// direction of the sun (upper-right). Without this the map looks
|
|
5344
|
+
// like it was cut out and pasted in.
|
|
5345
|
+
var wash = X.createRadialGradient(sunX, sunY*0.7, 40, sunX, sunY*0.7, Math.max(W, H)*0.85);
|
|
5346
|
+
wash.addColorStop(0, 'rgba(255, 220, 150, 0.16)');
|
|
5347
|
+
wash.addColorStop(0.35, 'rgba(255, 200, 130, 0.06)');
|
|
5348
|
+
wash.addColorStop(1, 'rgba(255, 200, 130, 0)');
|
|
5349
|
+
X.fillStyle = wash;
|
|
5350
|
+
X.fillRect(0, 0, W, H);
|
|
5351
|
+
|
|
5352
|
+
// ── subtle haze band along the horizon ──
|
|
5353
|
+
// Sits just above the road so the cars pop against a soft
|
|
5354
|
+
// horizon-glow rather than a hard line.
|
|
5355
|
+
var haze = X.createLinearGradient(0, horizon - 30, 0, horizon + 50);
|
|
5356
|
+
haze.addColorStop(0, 'rgba(255, 220, 160, 0)');
|
|
5357
|
+
haze.addColorStop(0.5, 'rgba(255, 220, 160, 0.22)');
|
|
5358
|
+
haze.addColorStop(1, 'rgba(255, 220, 160, 0)');
|
|
5359
|
+
X.fillStyle = haze;
|
|
5360
|
+
X.fillRect(0, horizon - 30, W, 80);
|
|
5361
|
+
|
|
5362
|
+
// ── ambient particles — drifting motes / pollen / fireflies ──
|
|
5363
|
+
// 40 tiny dots that float across the scene. Each one is held in
|
|
5364
|
+
// an array; the array is seeded once (so they don't all start at
|
|
5365
|
+
// the same position) and advanced by a tiny per-frame velocity.
|
|
5366
|
+
// On first call we lazily seed it; on resize we re-distribute.
|
|
5367
|
+
if(!_atmos.particles){
|
|
5368
|
+
_atmos.particles = [];
|
|
5369
|
+
for(var pi=0; pi<40; pi++){
|
|
5370
|
+
_atmos.particles.push({
|
|
5371
|
+
x: Math.random()*W,
|
|
5372
|
+
y: H*0.20 + Math.random()*H*0.55,
|
|
5373
|
+
r: 0.8 + Math.random()*1.6,
|
|
5374
|
+
vx: -6 + Math.random()*12, // px/sec, mostly right
|
|
5375
|
+
vy: -2 + Math.random()*4, // slight up/down sway
|
|
5376
|
+
phase: Math.random()*Math.PI*2,
|
|
5377
|
+
twinkle: 0.5 + Math.random()*0.5, // brightness amplitude
|
|
5378
|
+
hue: Math.random() < 0.35 ? 'warm' : 'cool', // mix of gold + silver
|
|
5379
|
+
});
|
|
5380
|
+
}
|
|
5381
|
+
}
|
|
5382
|
+
var dt = _atmos.lastNow ? Math.min(64, now - _atmos.lastNow)/1000 : 0.016;
|
|
5383
|
+
_atmos.lastNow = now;
|
|
5384
|
+
for(var pj=0; pj<_atmos.particles.length; pj++){
|
|
5385
|
+
var pt = _atmos.particles[pj];
|
|
5386
|
+
// simple wrap
|
|
5387
|
+
pt.x += pt.vx * dt;
|
|
5388
|
+
pt.y += Math.sin(now/1200 + pt.phase) * 0.3;
|
|
5389
|
+
if(pt.x > W + 8) pt.x = -8;
|
|
5390
|
+
if(pt.x < -8) pt.x = W + 8;
|
|
5391
|
+
if(pt.y < H*0.10) pt.y = H*0.75;
|
|
5392
|
+
if(pt.y > H*0.80) pt.y = H*0.15;
|
|
5393
|
+
// soft twinkle alpha
|
|
5394
|
+
var a = 0.25 + 0.45 * Math.abs(Math.sin(now/700 + pt.phase)) * pt.twinkle;
|
|
5395
|
+
X.fillStyle = pt.hue === 'warm'
|
|
5396
|
+
? 'rgba(255, 230, 160, '+a.toFixed(3)+')'
|
|
5397
|
+
: 'rgba(220, 235, 250, '+a.toFixed(3)+')';
|
|
5398
|
+
X.beginPath(); X.arc(pt.x, pt.y, pt.r, 0, Math.PI*2); X.fill();
|
|
5399
|
+
// tiny halo
|
|
5400
|
+
X.fillStyle = pt.hue === 'warm'
|
|
5401
|
+
? 'rgba(255, 220, 140, '+(a*0.35).toFixed(3)+')'
|
|
5402
|
+
: 'rgba(200, 220, 245, '+(a*0.30).toFixed(3)+')';
|
|
5403
|
+
X.beginPath(); X.arc(pt.x, pt.y, pt.r*2.5, 0, Math.PI*2); X.fill();
|
|
5404
|
+
}
|
|
5405
|
+
}
|
|
5406
|
+
// Holds the persistent atmospheric state (particle positions + last tick time).
|
|
5407
|
+
var _atmos = { particles: null, lastNow: 0 };
|
|
5408
|
+
|
|
4988
5409
|
// Sun-shade pavilion: 4 thin white poles supporting a flat white square canopy.
|
|
4989
5410
|
function drawPavilion(wx,wy){
|
|
4990
5411
|
var p=iso(wx,wy), z=cam.z;
|
|
@@ -5839,6 +6260,26 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
5839
6260
|
}
|
|
5840
6261
|
}
|
|
5841
6262
|
}
|
|
6263
|
+
// Cars share the same elapsed-dt clock so a tab-switch doesn't
|
|
6264
|
+
// catapult every car off the road at once. speed is world units
|
|
6265
|
+
// per second, dt is milliseconds. cdt is the already-clamped dt.
|
|
6266
|
+
var cdt = Math.min(64, dt);
|
|
6267
|
+
for(var ci=0; ci<CARS.length; ci++){
|
|
6268
|
+
var car = CARS[ci];
|
|
6269
|
+
car.x += car.dir * car.speed * (cdt/1000);
|
|
6270
|
+
// Wrap the car around the road. When it leaves one end, it
|
|
6271
|
+
// re-enters the opposite end on the other lane so oncoming
|
|
6272
|
+
// traffic feels continuous.
|
|
6273
|
+
if(car.dir > 0 && car.x > PARK_W + 0.5){
|
|
6274
|
+
car.x = -0.5;
|
|
6275
|
+
car.y = (car.y < -0.9) ? -0.65 : -1.18;
|
|
6276
|
+
car.dir = (car.y < -0.9) ? +1 : -1;
|
|
6277
|
+
} else if(car.dir < 0 && car.x < -0.5){
|
|
6278
|
+
car.x = PARK_W + 0.5;
|
|
6279
|
+
car.y = (car.y < -0.9) ? -0.65 : -1.18;
|
|
6280
|
+
car.dir = (car.y < -0.9) ? +1 : -1;
|
|
6281
|
+
}
|
|
6282
|
+
}
|
|
5842
6283
|
}
|
|
5843
6284
|
function walkerPos(w){
|
|
5844
6285
|
// Decode w.distance (in units of "segments") into a (seg, t) so
|
|
@@ -5952,8 +6393,14 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
5952
6393
|
if(!running) return;
|
|
5953
6394
|
var now = performance.now();
|
|
5954
6395
|
X.clearRect(0,0,W,H);
|
|
5955
|
-
|
|
5956
|
-
//
|
|
6396
|
+
// ── atmosphere: sky + sun + clouds + distant mountains + haze ──
|
|
6397
|
+
// Painted first, behind everything else, so the rest of the map
|
|
6398
|
+
// sits on top of an actual horizon instead of on the empty page
|
|
6399
|
+
// background. Drawn every frame so the sun and clouds drift even
|
|
6400
|
+
// when the rest of the scene is static.
|
|
6401
|
+
drawAtmosphere(now);
|
|
6402
|
+
drawGround(now);
|
|
6403
|
+
// step the looping walkers (and road cars) along their paths
|
|
5957
6404
|
tickWalkers(now);
|
|
5958
6405
|
// Re-derive status every frame so the "ended" countdown (a wall-
|
|
5959
6406
|
// clock timer) decrements smoothly between telemetry polls (3s).
|
|
@@ -6721,6 +7168,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6721
7168
|
+ '<div class="pk-ctl" style="margin-top:0.55rem"><button class="rp-btn primary" id="pk-unattended-test">Run unattended readiness</button><button class="rp-btn" id="pk-pipeline-start">Start full test</button><button class="rp-btn danger" id="pk-pipeline-stop">Stop conversation</button></div>'
|
|
6722
7169
|
+ '<div class="pk-note" id="pk-unattended-result">Unattended readiness runs an acoustic speaker/mic round-trip, triggers the production motion path, and requires greeting playback on the robot.</div>'
|
|
6723
7170
|
+ '<div class="pk-pipeline" id="pk-pipeline"></div>')
|
|
7171
|
+
+ sect('Production run history','<div class="pk-pipe-history" style="margin-top:0;padding-top:0;border-top:0"><div class="pk-pipe-history-head"><b>Persistent sessions</b><div class="pk-history-pager"><button class="rp-btn" id="pk-history-prev">Prev</button><span id="pk-history-page">page 1</span><button class="rp-btn" id="pk-history-next">Next</button></div></div><div class="pk-pipe-history-list" id="pk-pipeline-history"><div class="pk-note">Loading persistent sessions...</div></div></div>')
|
|
6724
7172
|
+ sect('🎙 microphone','<div class="pk-meter"><span class="lab">input</span><div class="pk-bars" id="pk-micbars"></div><span class="pk-val" id="pk-micval">—</span></div><div class="pk-note" id="pk-micstate">awaiting robot audio plugin</div><div class="pk-ctl" style="margin-top:.5rem"><button class="rp-btn" id="pk-mic-listen">Listen in browser</button><label class="pk-note" style="display:flex;align-items:center;gap:.35rem;margin:0">volume <input id="pk-mic-volume" type="range" min="0" max="100" value="80" style="width:90px"></label></div><div class="pk-note" id="pk-mic-listen-note">Muted by default. Use headphones to prevent acoustic feedback.</div>')
|
|
6725
7173
|
+ sect('🔊 speaker','<canvas class="pk-wave" id="pk-spkwave" width="360" height="32"></canvas><div class="pk-meter"><span class="lab">output</span><div class="pk-bars" id="pk-spkbars"></div><span class="pk-val" id="pk-spkval">—</span></div><div class="pk-note" id="pk-spkstate">TTS plays on the robot speaker — use “Test speaker” below to round-trip a tone and confirm the audio path</div>'
|
|
6726
7174
|
+ '<div class="rp-form" id="pk-spk-test" style="grid-template-columns:1fr 1fr;gap:0.4rem;margin-top:0.55rem;padding:0.55rem;border-color:var(--border-strong);">'
|
|
@@ -6743,6 +7191,8 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6743
7191
|
+ sect('🩺 robot services','<div id="pk-svc-list" class="pk-svc-list"><div class="pk-note">waiting for supervisor report…</div></div><div class="pk-note" id="pk-svc-age" style="margin-top:0.35rem;"></div><div class="rp-field" style="margin-top:0.55rem;"><label>Quick run (on the robot, from packages/robopark/scheduler)</label><input id="pk-svc-clicmd" readonly value="python robot_supervisor.py"></div><button class="rp-btn" id="pk-svc-clicmd-copy" style="margin-top:0.35rem;">📋 Copy command</button>')
|
|
6744
7192
|
+ buildShellSection(schedId(n))
|
|
6745
7193
|
+ sect('🎭 character / voice','<div class="pk-note" id="pk-eff-note" style="margin-bottom:0.45rem;">loading effective config…</div><div class="rp-field"><label>Character preset</label><select class="rp-char" id="pk-eff-char" data-robot="'+esc(schedId(n))+'">'+effPresetOptions+'</select></div><div class="rp-field"><label>Voice stack</label><select class="rp-voice" id="pk-eff-voice" data-robot="'+esc(schedId(n))+'">'+effVoiceOptions+'</select></div><div class="rp-config-actions" style="margin-top:.45rem"><button class="rp-btn primary" id="pk-eff-save" data-robot="'+esc(schedId(n))+'">Apply to robot</button><button class="rp-btn" id="pk-edit-character">Edit character</button><button class="rp-btn" id="pk-edit-stack">Edit stack</button><button class="rp-btn" id="pk-new-stack">New stack</button></div>');
|
|
7194
|
+
b.innerHTML += sect('Microphone setup suite','<div class="pk-mic-suite"><div class="pk-mic-health" id="pk-mic-health"><div class="pk-mic-check"><div class="k">Hardware lock</div><div class="v">USB mic hw:3,0</div></div><div class="pk-mic-check"><div class="k">PCM capture</div><div class="v">not tested</div></div><div class="pk-mic-check"><div class="k">Production track</div><div class="v">waiting</div></div></div><div class="rp-form" style="grid-template-columns:1fr 1fr;gap:.4rem"><div class="rp-field"><label>Ground-truth capture</label><select id="pk-mic-duration"><option value="2">2 seconds</option><option value="4" selected>4 seconds</option><option value="8">8 seconds</option></select></div><div class="rp-field"><label>Locked input</label><div class="pk-note" style="margin:.45rem 0 0">USB microphone (plughw:3,0)</div></div></div><div class="pk-ctl"><button class="rp-btn primary" id="pk-mic-groundtruth">Run raw mic capture</button><button class="rp-btn" id="pk-mic-roundtrip">Run speaker + mic test</button><button class="rp-btn" id="pk-mic-apply">Reapply production profile</button></div><div class="pk-note" id="pk-mic-test-state">Raw capture bypasses LiveKit and tests the exact ALSA path. Round-trip then verifies the physical speaker-to-mic path.</div><div id="pk-mic-test-result"></div></div>');
|
|
7195
|
+
b.innerHTML += sect('Motor registry + sequence canvas','<div class="rp-field"><label>Robot-local motor server</label><input id="pk-motor-url" value="http://127.0.0.1:8001" placeholder="http://127.0.0.1:8001"></div><div class="pk-motor-registry" id="pk-motor-registry"></div><button class="rp-btn" id="pk-motor-add" style="margin-top:.4rem">Add registered motor</button><div class="rp-form" style="grid-template-columns:1fr 1fr;margin-top:.7rem"><div class="rp-field"><label>Saved sequence</label><select id="pk-motor-sequence"></select></div><div class="rp-field"><label>Sequence name</label><input id="pk-motor-sequence-name" placeholder="Greeting motion"></div></div><div class="pk-motor-canvas" id="pk-motor-canvas"></div><div class="pk-ctl"><button class="rp-btn" id="pk-motor-step-add">Add timed node</button><button class="rp-btn primary" id="pk-motor-save">Save motor profile</button><button class="rp-btn" id="pk-motor-run">Run selected sequence</button><button class="rp-btn danger" id="pk-motor-test-all">Test all registered GPIO</button></div><label class="pk-prodtoggle"><input type="checkbox" id="pk-motor-greeting"><span>Run this sequence automatically with the greeting pipeline</span></label><div class="pk-note">Full GPIO test pulses registered relays only: 300 ms on, 200 ms pause, one at a time.</div><div class="pk-motor-status" id="pk-motor-status">Loading motor profile...</div>');
|
|
6746
7196
|
b.innerHTML += sect('Production camera + audio','<div class="pk-note" id="pk-device-search" style="margin-bottom:.55rem">Hardware locked for launch. Camera, microphone, and speaker cannot drift between restarts.</div><div class="rp-field"><label>Camera input</label><select id="pk-video-device" disabled></select></div><div class="rp-field"><label>Microphone input</label><select id="pk-audio-input" disabled></select></div><div class="rp-field"><label>Speaker output</label><select id="pk-audio-output" disabled></select></div><button class="rp-btn primary" id="pk-device-test-speaker" style="margin-top:0.35rem">Test USB speaker now</button><div class="rp-field"><label>Preset greeting phrases <span class="pk-note">one per line, cached per voice</span></label><textarea id="pk-greeting-phrases" rows="3" placeholder="Hi, I am Jaguar! / Welcome to RoboPark."></textarea></div><button class="rp-btn" id="pk-device-save" style="margin-top:0.35rem">Save greetings + enforce hardware</button><div class="pk-note" id="pk-device-note">waiting for robot heartbeat inventory</div>');
|
|
6747
7197
|
// Put live hardware selection beside the camera and audio tests,
|
|
6748
7198
|
// ahead of persona and service administration in the Park drawer.
|
|
@@ -6765,6 +7215,18 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6765
7215
|
});
|
|
6766
7216
|
var micListen=$('pk-mic-listen');if(micListen)micListen.addEventListener('click',function(){setMicMonitorAudible(!micMonitorAudible);});
|
|
6767
7217
|
var micVolume=$('pk-mic-volume');if(micVolume)micVolume.addEventListener('input',function(){micMonitorVolume=Math.max(0,Math.min(1,Number(micVolume.value||80)/100));if(micMonitorGain)micMonitorGain.gain.value=micMonitorAudible?micMonitorVolume:0;});
|
|
7218
|
+
var micGround=$('pk-mic-groundtruth');if(micGround)micGround.addEventListener('click',function(){if(rpReadOnly){showOut('read-only node');return;}runMicGroundtruth(n);});
|
|
7219
|
+
var micRound=$('pk-mic-roundtrip');if(micRound)micRound.addEventListener('click',function(){if(rpReadOnly){showOut('read-only node');return;}runMicRoundtrip(n);});
|
|
7220
|
+
var micApply=$('pk-mic-apply');if(micApply)micApply.addEventListener('click',function(){if(rpReadOnly){showOut('read-only node');return;}saveDeviceSelectors(n);});
|
|
7221
|
+
var motorAdd=$('pk-motor-add');if(motorAdd)motorAdd.addEventListener('click',function(){motorEditorAddMotor();});
|
|
7222
|
+
var motorStep=$('pk-motor-step-add');if(motorStep)motorStep.addEventListener('click',function(){motorEditorAddStep();});
|
|
7223
|
+
var motorSave=$('pk-motor-save');if(motorSave)motorSave.addEventListener('click',function(){if(rpReadOnly){showOut('read-only node');return;}saveMotorProfile(n);});
|
|
7224
|
+
var motorRun=$('pk-motor-run');if(motorRun)motorRun.addEventListener('click',function(){if(rpReadOnly){showOut('read-only node');return;}runMotorSequence(n);});
|
|
7225
|
+
var motorTestAll=$('pk-motor-test-all');if(motorTestAll)motorTestAll.addEventListener('click',function(){if(rpReadOnly){showOut('read-only node');return;}testAllMotorRelays(n);});
|
|
7226
|
+
var motorSeq=$('pk-motor-sequence');if(motorSeq)motorSeq.addEventListener('change',function(){selectMotorSequence(motorSeq.value);});
|
|
7227
|
+
var motorRegistry=$('pk-motor-registry');if(motorRegistry)motorRegistry.addEventListener('click',function(ev){var button=ev.target.closest('[data-motor-action]');if(!button)return;var index=Number(button.dataset.index);if(button.dataset.motorAction==='remove'){motorProfile.registry.splice(index,1);renderMotorEditor();}else if(button.dataset.motorAction==='test'){testMotorRelay(n,index);}});
|
|
7228
|
+
var motorCanvas=$('pk-motor-canvas');if(motorCanvas)motorCanvas.addEventListener('click',function(ev){var button=ev.target.closest('[data-step-remove]');if(button){captureMotorEditor();motorEditorSequence().steps.splice(Number(button.dataset.stepRemove),1);renderMotorEditor();}});
|
|
7229
|
+
loadMotorProfile(n);
|
|
6768
7230
|
startCam(n); startMeters(n); loadEffectiveConfig(schedId(n)); refreshSupervisorPanel(n);
|
|
6769
7231
|
_bindShellSection(n);
|
|
6770
7232
|
_spkTestRenderHistory(schedId(n));
|
|
@@ -6792,6 +7254,11 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6792
7254
|
var pipelineStart=$('pk-pipeline-start'); if(pipelineStart) pipelineStart.addEventListener('click', function(){ if(rpReadOnly){ showOut('read-only node'); return; } startPipelineTest(n); });
|
|
6793
7255
|
var unattended=$('pk-unattended-test'); if(unattended) unattended.addEventListener('click', function(){ if(rpReadOnly){ showOut('read-only node'); return; } runUnattendedReadiness(n); });
|
|
6794
7256
|
var pipelineStop=$('pk-pipeline-stop'); if(pipelineStop) pipelineStop.addEventListener('click', function(){ if(rpReadOnly){ showOut('read-only node'); return; } stopPipelineTest(n); });
|
|
7257
|
+
var historyPrev=$('pk-history-prev');if(historyPrev)historyPrev.addEventListener('click',function(){pipelineHistoryPage=Math.max(1,pipelineHistoryPage-1);loadPipelineHistory(n);});
|
|
7258
|
+
var historyNext=$('pk-history-next');if(historyNext)historyNext.addEventListener('click',function(){pipelineHistoryPage+=1;loadPipelineHistory(n);});
|
|
7259
|
+
var historyList=$('pk-pipeline-history');if(historyList)historyList.addEventListener('click',function(ev){var card=ev.target.closest('.pk-run-card');if(card)openSessionDetailModal({session_id:card.dataset.session});});
|
|
7260
|
+
pipelineHistoryPage=1;
|
|
7261
|
+
loadPipelineHistory(n);
|
|
6795
7262
|
pollPipeline(n);
|
|
6796
7263
|
if(pipelineTimer) clearInterval(pipelineTimer);
|
|
6797
7264
|
pipelineTimer=setInterval(function(){ if(drawerId===n.id) pollPipeline(n); }, 1500);
|
|
@@ -6806,12 +7273,88 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6806
7273
|
refreshDrawer();
|
|
6807
7274
|
}
|
|
6808
7275
|
|
|
7276
|
+
var motorProfile={registry:[],sequences:[],greeting_sequence_id:null,motor_server_url:'http://127.0.0.1:8001'};
|
|
7277
|
+
var motorSequenceId='';
|
|
7278
|
+
function motorEditorSequence(){
|
|
7279
|
+
var sequence=(motorProfile.sequences||[]).find(function(item){return item.id===motorSequenceId;});
|
|
7280
|
+
if(!sequence){sequence={id:motorSequenceId||('sequence-'+Date.now()),name:'New sequence',steps:[]};motorSequenceId=sequence.id;motorProfile.sequences.push(sequence);}
|
|
7281
|
+
if(!Array.isArray(sequence.steps))sequence.steps=[];
|
|
7282
|
+
return sequence;
|
|
7283
|
+
}
|
|
7284
|
+
function captureMotorEditor(){
|
|
7285
|
+
var rows=document.querySelectorAll('#pk-motor-registry .pk-motor-row');
|
|
7286
|
+
if(rows.length)motorProfile.registry=Array.prototype.map.call(rows,function(row,index){
|
|
7287
|
+
var fields=row.querySelectorAll('input');
|
|
7288
|
+
return {id:slugify(fields[0].value)||('motor-'+(index+1)),name:fields[1].value.trim()||('Motor '+(index+1)),gpio:Number(fields[2].value),active_high:fields[3].checked,max_duration_ms:Number(fields[4].value)||3000};
|
|
7289
|
+
});
|
|
7290
|
+
var sequence=(motorProfile.sequences||[]).find(function(item){return item.id===motorSequenceId;});
|
|
7291
|
+
if(sequence){
|
|
7292
|
+
var name=$('pk-motor-sequence-name');if(name)sequence.name=name.value.trim()||sequence.name||'Sequence';
|
|
7293
|
+
sequence.steps=Array.prototype.map.call(document.querySelectorAll('#pk-motor-canvas .pk-motor-node'),function(node){
|
|
7294
|
+
return {motor_id:node.querySelector('[data-step-motor]').value,delay_ms:Number(node.querySelector('[data-step-delay]').value)||0,duration_ms:Number(node.querySelector('[data-step-duration]').value)||300};
|
|
7295
|
+
});
|
|
7296
|
+
var greeting=$('pk-motor-greeting');if(greeting)motorProfile.greeting_sequence_id=greeting.checked?sequence.id:(motorProfile.greeting_sequence_id===sequence.id?null:motorProfile.greeting_sequence_id);
|
|
7297
|
+
}
|
|
7298
|
+
var url=$('pk-motor-url');if(url)motorProfile.motor_server_url=url.value.trim()||'http://127.0.0.1:8001';
|
|
7299
|
+
}
|
|
7300
|
+
function renderMotorEditor(){
|
|
7301
|
+
var registry=$('pk-motor-registry'),canvas=$('pk-motor-canvas'),select=$('pk-motor-sequence');if(!registry||!canvas||!select)return;
|
|
7302
|
+
registry.innerHTML=(motorProfile.registry||[]).map(function(motor,index){return '<div class="pk-motor-row"><input value="'+esc(motor.id)+'" aria-label="Motor id"><input value="'+esc(motor.name)+'" aria-label="Motor name"><input type="number" min="2" max="27" value="'+esc(motor.gpio)+'" aria-label="BCM GPIO"><label title="Active high"><input type="checkbox" '+(motor.active_high!==false?'checked':'')+'> high</label><input type="number" min="50" max="10000" step="50" value="'+esc(motor.max_duration_ms||3000)+'" aria-label="Max pulse milliseconds"><button class="rp-btn" data-motor-action="test" data-index="'+index+'">Test</button><button class="rp-btn danger" data-motor-action="remove" data-index="'+index+'">Remove</button></div>';}).join('')||'<div class="pk-note">No relays registered. Add only wired BCM outputs.</div>';
|
|
7303
|
+
select.innerHTML=(motorProfile.sequences||[]).map(function(sequence){return '<option value="'+esc(sequence.id)+'" '+(sequence.id===motorSequenceId?'selected':'')+'>'+esc(sequence.name||sequence.id)+'</option>';}).join('')+'<option value="__new__">+ New sequence</option>';
|
|
7304
|
+
var sequence=motorEditorSequence(),name=$('pk-motor-sequence-name'),greeting=$('pk-motor-greeting'),url=$('pk-motor-url');
|
|
7305
|
+
if(name)name.value=sequence.name||'';if(greeting)greeting.checked=motorProfile.greeting_sequence_id===sequence.id;if(url)url.value=motorProfile.motor_server_url||'http://127.0.0.1:8001';
|
|
7306
|
+
var motorOptions=(motorProfile.registry||[]).map(function(motor){return '<option value="'+esc(motor.id)+'">'+esc(motor.name+' | BCM '+motor.gpio)+'</option>';}).join('');
|
|
7307
|
+
canvas.innerHTML=(sequence.steps||[]).map(function(step,index){return '<div class="pk-motor-node"><div class="pk-motor-node-num">'+(index+1)+'</div><label>Relay<select data-step-motor>'+motorOptions.replace('value="'+esc(step.motor_id)+'"','value="'+esc(step.motor_id)+'" selected')+'</select></label><label>Delay ms<input data-step-delay type="number" min="0" max="30000" step="50" value="'+esc(step.delay_ms||0)+'"></label><label>Pulse ms<input data-step-duration type="number" min="50" max="10000" step="50" value="'+esc(step.duration_ms||300)+'"></label><button class="rp-btn danger" data-step-remove="'+index+'">Remove</button></div>';}).join('')||'<div class="pk-note">No timed nodes. Add a node to build the sequence.</div>';
|
|
7308
|
+
}
|
|
7309
|
+
function selectMotorSequence(id){
|
|
7310
|
+
captureMotorEditor();
|
|
7311
|
+
if(id==='__new__'){var sequence={id:'sequence-'+Date.now(),name:'New sequence',steps:[]};motorProfile.sequences.push(sequence);motorSequenceId=sequence.id;}else motorSequenceId=id;
|
|
7312
|
+
renderMotorEditor();
|
|
7313
|
+
}
|
|
7314
|
+
function motorEditorAddMotor(){captureMotorEditor();var number=motorProfile.registry.length+1;motorProfile.registry.push({id:'motor-'+number,name:'Motor '+number,gpio:Math.min(27,Math.max(2,number+4)),active_high:true,max_duration_ms:3000});renderMotorEditor();}
|
|
7315
|
+
function motorEditorAddStep(){captureMotorEditor();if(!motorProfile.registry.length){motorStatus('Register a motor before adding a timed node','bad');return;}motorEditorSequence().steps.push({motor_id:motorProfile.registry[0].id,delay_ms:0,duration_ms:300});renderMotorEditor();}
|
|
7316
|
+
function motorStatus(message,kind){var host=$('pk-motor-status');if(host){host.textContent=message;host.className='pk-motor-status '+(kind||'');}}
|
|
7317
|
+
function loadMotorProfile(n){
|
|
7318
|
+
var robotId=schedId(n);motorStatus('Loading saved motor profile...');
|
|
7319
|
+
getJson('/robopark/api/robots/'+encodeURIComponent(robotId)+'/motor-profile').then(function(data){motorProfile={registry:data.registry||[],sequences:data.sequences||[],greeting_sequence_id:data.greeting_sequence_id||null,motor_server_url:(schedulerDevice(n)||{}).motor_server_url||'http://127.0.0.1:8001'};if(!motorProfile.sequences.length)motorProfile.sequences=[{id:'greeting-motion',name:'Greeting motion',steps:[]}];motorSequenceId=motorProfile.greeting_sequence_id||motorProfile.sequences[0].id;renderMotorEditor();motorStatus('Saved profile loaded | '+motorProfile.registry.length+' relay(s), '+motorProfile.sequences.length+' sequence(s)','ok');}).catch(function(error){motorStatus('Motor profile unavailable: '+error.message,'bad');});
|
|
7320
|
+
}
|
|
7321
|
+
function saveMotorProfile(n){
|
|
7322
|
+
captureMotorEditor();var robotId=schedId(n),url=motorProfile.motor_server_url;
|
|
7323
|
+
var parsedUrl=null;try{parsedUrl=new URL(url);}catch(e){}
|
|
7324
|
+
if(!parsedUrl||parsedUrl.protocol!=='http:'||(parsedUrl.hostname!=='127.0.0.1'&&parsedUrl.hostname!=='localhost')||!parsedUrl.port){motorStatus('Motor server must be robot-local: http://127.0.0.1:PORT','bad');return Promise.reject(new Error('motor server must be localhost'));}
|
|
7325
|
+
motorStatus('Validating and saving motor profile...');
|
|
7326
|
+
return rpAction('PATCH','/robopark/api/devices/'+encodeURIComponent(robotId),{motor_server_url:url}).then(function(){return rpAction('PUT','/robopark/api/robots/'+encodeURIComponent(robotId)+'/motor-profile',{registry:motorProfile.registry,sequences:motorProfile.sequences,greeting_sequence_id:motorProfile.greeting_sequence_id});}).then(function(data){motorProfile.registry=data.registry||[];motorProfile.sequences=data.sequences||[];motorProfile.greeting_sequence_id=data.greeting_sequence_id||null;renderMotorEditor();motorStatus('Profile saved. Greeting motion will start on first TTS playback.','ok');return data;}).catch(function(error){motorStatus('Save failed: '+error.message,'bad');throw error;});
|
|
7327
|
+
}
|
|
7328
|
+
function runMotorSequence(n){
|
|
7329
|
+
var robotId=schedId(n),sequence=motorEditorSequence();motorStatus('Saving and queueing '+sequence.name+'...');
|
|
7330
|
+
saveMotorProfile(n).then(function(){return rpAction('POST','/robopark/api/robots/'+encodeURIComponent(robotId)+'/motor-sequences/'+encodeURIComponent(sequence.id)+'/run',{});}).then(function(q){return _shellWaitForResult(robotId,q.request_id,q.device_id||robotId,'','motor-sequence');}).then(function(result){motorStatus(result.ok?'Sequence completed on robot.':'Sequence failed: '+(result.error||'unknown error'),result.ok?'ok':'bad');}).catch(function(error){motorStatus('Run failed: '+error.message,'bad');});
|
|
7331
|
+
}
|
|
7332
|
+
function testMotorRelay(n,index){
|
|
7333
|
+
captureMotorEditor();var motor=motorProfile.registry[index];if(!motor)return;var robotId=schedId(n);motorStatus('Saving profile before safe 300 ms '+motor.name+' test...');
|
|
7334
|
+
saveMotorProfile(n).then(function(){return rpAction('POST','/robopark/api/robots/'+encodeURIComponent(robotId)+'/motors/'+encodeURIComponent(motor.id)+'/test?duration_ms=300',{});}).then(function(q){return _shellWaitForResult(robotId,q.request_id,q.device_id||robotId,'','motor-test');}).then(function(result){motorStatus(result.ok?motor.name+' relay test completed.':'Relay test failed: '+(result.error||'unknown error'),result.ok?'ok':'bad');}).catch(function(error){motorStatus('Relay test failed: '+error.message,'bad');});
|
|
7335
|
+
}
|
|
7336
|
+
function testAllMotorRelays(n){
|
|
7337
|
+
captureMotorEditor();if(!motorProfile.registry.length){motorStatus('Register at least one relay before running the GPIO test.','bad');return;}
|
|
7338
|
+
if(!window.confirm('Pulse all '+motorProfile.registry.length+' registered relays one at a time? Ensure the motor area is clear.'))return;
|
|
7339
|
+
var robotId=schedId(n);motorStatus('Saving profile and testing '+motorProfile.registry.length+' registered GPIO relay(s), one at a time...');
|
|
7340
|
+
saveMotorProfile(n).then(function(){return rpAction('POST','/robopark/api/robots/'+encodeURIComponent(robotId)+'/motors/test-all?duration_ms=300&pause_ms=200',{});}).then(function(q){return _shellWaitForResult(robotId,q.request_id,q.device_id||robotId,'','gpio-test-all');}).then(function(result){motorStatus(result.ok?'Full registered GPIO test completed: '+(result.completed_steps||[]).length+' relay(s).':'GPIO test failed: '+(result.error||'unknown error'),result.ok?'ok':'bad');}).catch(function(error){motorStatus('GPIO test failed: '+error.message,'bad');});
|
|
7341
|
+
}
|
|
7342
|
+
|
|
7343
|
+
var pipelineHistoryPage=1;
|
|
6809
7344
|
var PIPELINE_LABELS={
|
|
6810
7345
|
robot_online:'Robot online',camera_ready:'Camera detected',microphone_ready:'Microphone detected',speaker_ready:'Speaker detected',
|
|
6811
7346
|
motion_detected:'Motion received',scheduler_session:'Scheduler session',livekit_join:'LiveKit joined',camera_published:'Camera published',
|
|
6812
7347
|
microphone_published:'Microphone published',voice_worker:'Voice worker',stt_listening:'STT listening',llm_response:'LLM response',
|
|
6813
|
-
tts_subscribed:'TTS subscribed',playback_started:'Robot playback',session_ended:'Session ended'
|
|
7348
|
+
tts_subscribed:'TTS subscribed',playback_started:'Robot playback',motor_sequence:'Greeting motion',session_ended:'Session ended'
|
|
6814
7349
|
};
|
|
7350
|
+
var PIPELINE_PHASES={robot_online:'hardware',camera_ready:'hardware',microphone_ready:'hardware',speaker_ready:'hardware',motion_detected:'trigger',scheduler_session:'session',livekit_join:'session',camera_published:'media',microphone_published:'media',voice_worker:'intelligence',stt_listening:'intelligence',llm_response:'intelligence',tts_subscribed:'playback',playback_started:'playback',motor_sequence:'playback',session_ended:'close'};
|
|
7351
|
+
function pipelineCurrentStage(stages,latest,running){
|
|
7352
|
+
for(var i=0;i<stages.length;i++){var bad=latest[stages[i]];if(bad&&(bad.status==='failed'||bad.status==='blocked'))return stages[i];}
|
|
7353
|
+
if(!running)return null;
|
|
7354
|
+
var lastOk=-1;
|
|
7355
|
+
for(var j=0;j<stages.length;j++){var ev=latest[stages[j]];if(ev&&(ev.status==='ok'||ev.status==='skipped'))lastOk=j;if(ev&&ev.status==='running')return stages[j];}
|
|
7356
|
+
return stages[Math.min(stages.length-1,lastOk+1)];
|
|
7357
|
+
}
|
|
6815
7358
|
function renderPipeline(data){
|
|
6816
7359
|
var host=$('pk-pipeline'), summary=$('pk-pipeline-summary'); if(!host||!summary) return;
|
|
6817
7360
|
var latest={}, prereqs=data.prerequisites||{}, session=data.latest_session||{}, sid=session.id||null;
|
|
@@ -6822,16 +7365,37 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6822
7365
|
['robot_online','camera_ready','microphone_ready','speaker_ready'].forEach(function(stage){
|
|
6823
7366
|
if(!latest[stage]) latest[stage]={stage:stage,status:prereqs[stage]?'ok':'blocked',message:prereqs[stage]?'Ready':'Not reported'};
|
|
6824
7367
|
});
|
|
6825
|
-
var running=data.overall==='running';
|
|
6826
|
-
host.innerHTML=
|
|
6827
|
-
var ev=latest[stage],
|
|
7368
|
+
var running=data.overall==='running',stages=data.stages||Object.keys(PIPELINE_LABELS),current=pipelineCurrentStage(stages,latest,running);
|
|
7369
|
+
host.innerHTML=stages.map(function(stage,index){
|
|
7370
|
+
var ev=latest[stage],isCurrent=stage===current,status=ev&&ev.status||(isCurrent?'running':'pending');
|
|
6828
7371
|
var detail=ev&&(ev.message||ev.timestamp)||'waiting';
|
|
6829
|
-
|
|
6830
|
-
|
|
7372
|
+
var time=ev&&ev.timestamp?new Date(ev.timestamp+'Z').toLocaleString():'';
|
|
7373
|
+
var phase=PIPELINE_PHASES[stage]||'session';
|
|
7374
|
+
return '<div class="pk-pipe-step phase-'+phase+' '+esc(status)+(isCurrent?' current':'')+'"><span class="step-index">'+String(index+1).padStart(2,'0')+'</span><span><span class="stage">'+esc(PIPELINE_LABELS[stage]||stage)+'</span><span class="phase">'+esc(phase)+'</span></span><span class="detail">'+esc(detail)+(time?'<span class="time">'+esc(time)+'</span>':'')+'</span></div>';
|
|
6831
7375
|
}).join('');
|
|
6832
7376
|
var blockers=data.blockers||[];
|
|
6833
7377
|
summary.className='pk-pipeline-summary '+esc(data.overall||'blocked');
|
|
6834
|
-
summary.textContent=blockers.length?('BLOCKED: '+blockers[0]):(running?'
|
|
7378
|
+
summary.textContent=blockers.length?('BLOCKED: '+blockers[0]):(running?('ACTIVE: '+(PIPELINE_LABELS[current]||current||'starting')+' | session '+(sid||'allocating')):'READY: camera-to-speaker remote test can start');
|
|
7379
|
+
var micHealth=$('pk-mic-health');if(micHealth){
|
|
7380
|
+
var micEvent=latest.microphone_published,trackOk=micEvent&&micEvent.status==='ok',inventoryOk=!!prereqs.microphone_ready;
|
|
7381
|
+
micHealth.innerHTML='<div class="pk-mic-check ok"><div class="k">Hardware lock</div><div class="v">USB mic hw:3,0</div></div><div class="pk-mic-check '+(inventoryOk?'ok':'bad')+'"><div class="k">Inventory</div><div class="v">'+(inventoryOk?'detected':'missing')+'</div></div><div class="pk-mic-check '+(trackOk?'ok':micEvent?'bad':'')+'"><div class="k">Production track</div><div class="v">'+esc(micEvent?(micEvent.message||micEvent.status):'waiting for session')+'</div></div>';
|
|
7382
|
+
}
|
|
7383
|
+
}
|
|
7384
|
+
function loadPipelineHistory(n){
|
|
7385
|
+
var host=$('pk-pipeline-history'),pageLabel=$('pk-history-page');if(!host)return;
|
|
7386
|
+
host.innerHTML='<div class="pk-note">Loading session page...</div>';
|
|
7387
|
+
getJson('/robopark/api/robots/'+encodeURIComponent(schedId(n))+'/pipeline-history?page='+pipelineHistoryPage+'&page_size=6'+(QS?'&'+QS.slice(1):''))
|
|
7388
|
+
.then(function(data){
|
|
7389
|
+
pipelineHistoryPage=data.page||1;if(pageLabel)pageLabel.textContent='page '+pipelineHistoryPage+' / '+(data.pages||1)+' | '+(data.total||0)+' runs';
|
|
7390
|
+
var prev=$('pk-history-prev'),next=$('pk-history-next');if(prev)prev.disabled=pipelineHistoryPage<=1;if(next)next.disabled=pipelineHistoryPage>=(data.pages||1);
|
|
7391
|
+
host.innerHTML=(data.items||[]).map(function(run){
|
|
7392
|
+
var events=run.events||[],latest={};events.forEach(function(ev){latest[ev.stage]=ev;});
|
|
7393
|
+
var hasError=Number(run.error_count||0)>0,state=!run.ended_at?'running':hasError?'bad':'ok',label=!run.ended_at?'active':hasError?'attention':(run.end_reason||'complete');
|
|
7394
|
+
var started=run.started_at?new Date(run.started_at+'Z'):null,duration=run.duration_seconds!=null?run.duration_seconds:(started?Math.max(0,Math.floor((Date.now()-started.getTime())/1000)):0);
|
|
7395
|
+
var stageBar=Object.keys(PIPELINE_LABELS).map(function(stage){var ev=latest[stage];return '<i class="'+esc(ev&&ev.status||'pending')+'" title="'+esc(PIPELINE_LABELS[stage])+'"></i>';}).join('');
|
|
7396
|
+
return '<div class="pk-run-card" data-session="'+esc(run.session_id)+'"><div class="pk-run-top"><div><div class="pk-run-title">'+esc(run.session_id)+'</div><div class="pk-run-meta">'+esc(started?started.toLocaleString():'unknown time')+' | '+esc(rpFmtDuration(duration))+' | '+esc(run.transcript_count||0)+' transcript turns | '+esc(run.pipeline_event_count||0)+' events</div></div><span class="pk-run-state '+state+'">'+esc(label)+'</span></div><div class="pk-run-stages">'+stageBar+'</div></div>';
|
|
7397
|
+
}).join('')||'<div class="pk-note">No production sessions recorded for this robot.</div>';
|
|
7398
|
+
}).catch(function(e){host.innerHTML='<div class="pk-note" style="color:var(--bad)">History unavailable: '+esc(e.message)+'</div>';});
|
|
6835
7399
|
}
|
|
6836
7400
|
function pollPipeline(n){
|
|
6837
7401
|
fetch('/robopark/api/robots/'+encodeURIComponent(schedId(n))+'/pipeline-status'+QS,{cache:'no-store'})
|
|
@@ -7225,7 +7789,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
7225
7789
|
}).join('');
|
|
7226
7790
|
host.innerHTML = '<div class="pk-spk-hist-head">last '+list.length+' tests</div>' + rows;
|
|
7227
7791
|
}
|
|
7228
|
-
function _spkTestRenderResult(robotId, result){
|
|
7792
|
+
function _spkTestRenderResult(robotId, result){
|
|
7229
7793
|
var host = $('pk-spk-test-result'); if(!host) return;
|
|
7230
7794
|
if(!result){
|
|
7231
7795
|
host.innerHTML = '';
|
|
@@ -7252,9 +7816,32 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
7252
7816
|
+ '<div><span class="l">format</span><span class="v">'+esc((result.output_sample_rate||result.sample_rate)+' Hz / '+(result.output_channels||1)+'ch out'+(result.playback_only?'':' · '+(result.input_sample_rate||result.sample_rate)+' Hz / '+(result.input_channels||1)+'ch in'))+'</span></div>'
|
|
7253
7817
|
+ '</div>';
|
|
7254
7818
|
var diag = result.diagnostic ? '<div class="pk-spk-diag">'+esc(result.diagnostic)+'</div>' : '';
|
|
7255
|
-
host.innerHTML = '<div class="pk-spk-result '+(result.pass?'ok':'bad')+'">'
|
|
7256
|
-
+ banner + meta + diag + '</div>';
|
|
7257
|
-
}
|
|
7819
|
+
host.innerHTML = '<div class="pk-spk-result '+(result.pass?'ok':'bad')+'">'
|
|
7820
|
+
+ banner + meta + diag + '</div>';
|
|
7821
|
+
}
|
|
7822
|
+
function _micTestRenderResult(result,label){
|
|
7823
|
+
var host=$('pk-mic-test-result'),state=$('pk-mic-test-state');if(!host)return;
|
|
7824
|
+
if(!result||!result.ok){var error=result&&result.error||'robot did not return a result';host.innerHTML='<div class="pk-mic-result bad"><b>TEST FAILED</b><div style="margin-top:.3rem">'+esc(error)+'</div></div>';if(state)state.textContent='Failed | '+error;return;}
|
|
7825
|
+
var passed=result.pass===true,mode=result.mode||label;
|
|
7826
|
+
host.innerHTML='<div class="pk-mic-result '+(passed?'ok':'bad')+'"><b>'+(passed?'MIC PATH VERIFIED':'NO USABLE SIGNAL')+'</b><div class="pk-mic-readings"><div class="pk-mic-reading"><span>Peak</span><b>'+esc(result.recorded_peak_db!=null?result.recorded_peak_db+' dB':'--')+'</b></div><div class="pk-mic-reading"><span>RMS</span><b>'+esc(result.recorded_rms_db!=null?result.recorded_rms_db+' dB':'--')+'</b></div><div class="pk-mic-reading"><span>Nonzero</span><b>'+esc(result.nonzero_percent!=null?result.nonzero_percent+'%':'--')+'</b></div><div class="pk-mic-reading"><span>Format</span><b>'+esc((result.sample_rate||result.input_sample_rate||'--')+' Hz / '+(result.channels||result.input_channels||1)+'ch')+'</b></div></div><div style="margin-top:.38rem;color:var(--muted)">'+esc(result.input_device||'USB microphone')+' | '+esc(result.duration_ms||0)+' ms | '+esc(result.diagnostic||mode)+'</div></div>';
|
|
7827
|
+
if(state)state.textContent=passed?'Passed | '+mode+' confirmed on the robot':'Capture completed but no usable PCM signal was detected';
|
|
7828
|
+
}
|
|
7829
|
+
function setMicTestBusy(busy,text){
|
|
7830
|
+
[$('pk-mic-groundtruth'),$('pk-mic-roundtrip'),$('pk-mic-apply')].forEach(function(button){if(button)button.disabled=busy;});
|
|
7831
|
+
var state=$('pk-mic-test-state');if(state&&text)state.textContent=text;
|
|
7832
|
+
}
|
|
7833
|
+
function runMicGroundtruth(n){
|
|
7834
|
+
var robotId=schedId(n),duration=parseFloat((($('pk-mic-duration')||{}).value)||'4');setMicTestBusy(true,'Recording raw PCM on the robot for '+duration+' seconds...');
|
|
7835
|
+
rpAction('POST','/robopark/api/robots/'+encodeURIComponent(robotId)+'/shell/speaker-test',{mode:'mic_groundtruth',duration:duration,input:'2',input_name:'Usb Audio Device: USB Audio (hw:3,0)'})
|
|
7836
|
+
.then(function(q){return _shellWaitForResult(robotId,q.request_id,q.device_id||robotId,'','mic-groundtruth');})
|
|
7837
|
+
.catch(function(e){_micTestRenderResult({ok:false,error:e.message},'mic_groundtruth');setMicTestBusy(false);});
|
|
7838
|
+
}
|
|
7839
|
+
function runMicRoundtrip(n){
|
|
7840
|
+
var robotId=schedId(n);setMicTestBusy(true,'Playing a reference tone and recording the locked USB microphone...');
|
|
7841
|
+
rpAction('POST','/robopark/api/robots/'+encodeURIComponent(robotId)+'/shell/speaker-test',{mode:'tone',playback_only:false,frequency:1000,duration:1,amplitude:.6,threshold_db:-30,output:'1',input:'2',output_name:'USB Audio Device: - (hw:2,0)',input_name:'Usb Audio Device: USB Audio (hw:3,0)'})
|
|
7842
|
+
.then(function(q){return _shellWaitForResult(robotId,q.request_id,q.device_id||robotId,'','mic-roundtrip');})
|
|
7843
|
+
.catch(function(e){_micTestRenderResult({ok:false,error:e.message},'roundtrip');setMicTestBusy(false);});
|
|
7844
|
+
}
|
|
7258
7845
|
function runSpeakerTest(n, forcedMode){
|
|
7259
7846
|
var robotId = schedId(n), mode=forcedMode||(($('pk-spk-mode')||{}).value||'tts');
|
|
7260
7847
|
var buttons=[$('pk-spk-runtest'),$('pk-spk-tone'),$('pk-device-test-speaker')];buttons.forEach(function(b){if(b)b.disabled=true;});
|
|
@@ -7310,7 +7897,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
7310
7897
|
var _origRenderShellResult = _renderShellResult;
|
|
7311
7898
|
_renderShellResult = function(robotId, result, label){
|
|
7312
7899
|
_origRenderShellResult(robotId, result, label);
|
|
7313
|
-
if(label === 'speaker-test'){
|
|
7900
|
+
if(label === 'speaker-test'){
|
|
7314
7901
|
_spkTestSetLive(robotId, {live:false});
|
|
7315
7902
|
_spkTestMark(robotId, result);
|
|
7316
7903
|
_spkTestRenderResult(robotId, result);
|
|
@@ -7323,8 +7910,12 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
7323
7910
|
else { status.textContent = '✗ no signal — '+result.recorded_peak_db+' dB peak'; }
|
|
7324
7911
|
}
|
|
7325
7912
|
var btn = $('pk-spk-runtest'); if(btn){ btn.disabled = false; btn.textContent = (($('pk-spk-mode')||{}).value==='tone'?'Run tone test':'Play cached voice'); }
|
|
7326
|
-
}
|
|
7327
|
-
|
|
7913
|
+
}
|
|
7914
|
+
if(label==='mic-groundtruth'||label==='mic-roundtrip'){
|
|
7915
|
+
_micTestRenderResult(result,label);
|
|
7916
|
+
setMicTestBusy(false);
|
|
7917
|
+
}
|
|
7918
|
+
};
|
|
7328
7919
|
|
|
7329
7920
|
function openDrawerForRobot(name){
|
|
7330
7921
|
var key=String(name||'').toLowerCase(), id='pad:'+name, found=null;
|