codebee 0.1.100 → 0.1.102

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/app/ui/hive3d.js CHANGED
@@ -1,242 +1,510 @@
1
- /* Fixed-view office: the reference artwork preserves its lighting/materials;
2
- * localized HTML screens share the same aspect ratio, pan and zoom transform.
3
- * No WebGL is required. The host supplies actual run/step data and log access. */
1
+ /* CodeBee Hive3D: dependency-free WebGL scene with live HTML task overlays. */
4
2
  window.Hive3D = (function () {
5
3
  "use strict";
6
-
7
4
  const tr = (key, ...args) => typeof window.t === "function" ? window.t(key, ...args) : key;
8
- const clamp = (value, low, high) => Math.max(low, Math.min(high, value));
9
- const STATUS = { running: "运行中", done: "完成", queued: "排队中", failed: "失败", timeout: "超时", cancelled: "已取消" };
10
- const STAGES = ["规划", "起草", "评审", "执行", "打磨", "合成"];
11
- // Glass content rectangles measured on the 2048 × 1151 reference image.
5
+ const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
6
+ const STATUS = { running:"运行中", done:"完成", queued:"排队中", failed:"失败", timeout:"超时", cancelled:"已取消" };
7
+ const STAGES = ["规划","起草","评审","执行","打磨","合成"];
12
8
  const SCREENS = [
13
- [163, 441, 127, 55], [389, 441, 126, 55], [616, 441, 125, 55], [843, 441, 125, 55],
14
- [1073, 441, 122, 55], [1298, 441, 125, 55], [1531, 441, 120, 55], [1750, 441, 125, 55],
15
- [166, 724, 165, 66], [470, 724, 165, 66], [778, 724, 165, 66],
16
- [1091, 724, 162, 66], [1392, 724, 163, 66], [1695, 724, 165, 66],
9
+ [163,441,127,55],[389,441,126,55],[616,441,125,55],[843,441,125,55],
10
+ [1073,441,122,55],[1298,441,125,55],[1531,441,120,55],[1750,441,125,55],
11
+ [166,724,165,66],[470,724,165,66],[778,724,165,66],[1091,724,162,66],[1392,724,163,66],[1695,724,165,66]
17
12
  ];
18
- const PLATES = [[278, 206], [578, 206], [876, 206], [1175, 206], [1473, 206], [1766, 206]];
19
-
20
- function node(tag, className, parent) {
21
- const el = document.createElement(tag);
22
- el.className = className;
23
- if (parent) parent.appendChild(el);
24
- return el;
13
+ const PLATES = [[278,206],[578,206],[876,206],[1175,206],[1473,206],[1766,206]];
14
+ const node = (tag, cls, parent) => { const el=document.createElement(tag); el.className=cls; if(parent)parent.appendChild(el); return el; };
15
+ const mat4 = {
16
+ perspective(fovy, aspect, near, far) { const f=1/Math.tan(fovy/2), nf=1/(near-far); return new Float32Array([f/aspect,0,0,0,0,f,0,0,0,0,(far+near)*nf,-1,0,0,2*far*near*nf,0]); },
17
+ multiply(a,b) { const o=new Float32Array(16); for(let c=0;c<4;c++)for(let r=0;r<4;r++)o[c*4+r]=a[r]*b[c*4]*1+a[4+r]*b[c*4+1]+a[8+r]*b[c*4+2]+a[12+r]*b[c*4+3]; return o; },
18
+ translate(x,y,z) { const o=new Float32Array([1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]);o[12]=x;o[13]=y;o[14]=z;return o; },
19
+ scale(x,y,z) { return new Float32Array([x,0,0,0,0,y,0,0,0,0,z,0,0,0,0,1]); },
20
+ rotateX(a) { const c=Math.cos(a),s=Math.sin(a);return new Float32Array([1,0,0,0,0,c,s,0,0,-s,c,0,0,0,0,1]); },
21
+ rotateY(a) { const c=Math.cos(a),s=Math.sin(a);return new Float32Array([c,0,-s,0,0,1,0,0,s,0,c,0,0,0,0,1]); },
22
+ lookAt(eye, center, up) { let z=norm(sub(eye,center)),x=norm(cross(up,z)),y=cross(z,x); return new Float32Array([x[0],y[0],z[0],0,x[1],y[1],z[1],0,x[2],y[2],z[2],0,-dot(x,eye),-dot(y,eye),-dot(z,eye),1]); }
23
+ };
24
+ const sub=(a,b)=>a.map((v,i)=>v-b[i]), dot=(a,b)=>a.reduce((s,v,i)=>s+v*b[i],0), cross=(a,b)=>[a[1]*b[2]-a[2]*b[1],a[2]*b[0]-a[0]*b[2],a[0]*b[1]-a[1]*b[0]], norm=a=>{const l=Math.hypot(...a)||1;return a.map(v=>v/l);};
25
+ function shader(gl,type,source){const s=gl.createShader(type);gl.shaderSource(s,source);gl.compileShader(s);if(!gl.getShaderParameter(s,gl.COMPILE_STATUS))throw Error(gl.getShaderInfoLog(s));return s;}
26
+ function geometry(gl, positions, indices, normals) {
27
+ const p=gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER,p);gl.bufferData(gl.ARRAY_BUFFER,new Float32Array(positions),gl.STATIC_DRAW);
28
+ const ix=gl.createBuffer();gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER,ix);gl.bufferData(gl.ELEMENT_ARRAY_BUFFER,new Uint16Array(indices),gl.STATIC_DRAW);
29
+ return {p,ix,count:indices.length,positions,indices,normals:normals||positions,smooth:Boolean(normals)};
25
30
  }
26
-
27
- function ReferenceScene(opts) {
28
- this.opts = opts;
29
- this.canvas = opts.canvas;
30
- this.overlay = opts.overlay;
31
- this.host = this.canvas.parentElement;
32
- this.onCellActivate = opts.onCellActivate || function () {};
33
- this.cellRefresh = opts.cellRefresh || function () { return {}; };
34
- this.model = null;
35
- this.page = 0;
36
- this.zoom = 1;
37
- this.pan = { x: 0, y: 0 };
38
- this.active = false;
39
- this.timer = null;
40
- this.screenMeta = [];
41
- this.listeners = [];
42
- this.layer = node("div", "hive-reference-layer", this.host);
43
- this.layer.appendChild(this.host.querySelector(".hive-reference"));
44
- this.layer.appendChild(this.overlay);
45
- this.overlay.replaceChildren();
46
- this.canvas.hidden = true;
47
- this.host.dataset.renderer = "reference";
48
-
49
- this.stageMeta = PLATES.map(([x, y], index) => {
50
- const el = node("button", "hg-badge", this.overlay);
51
- el.type = "button";
52
- el.style.left = (x / 2048 * 100) + "%";
53
- el.style.top = (y / 1151 * 100) + "%";
54
- const name = node("b", "hg-name", el);
55
- const meta = node("span", "hg-meta", el);
56
- this.listen(el, "click", () => {
57
- const lane = this.stageMeta[index].lane;
58
- if (!lane) return;
59
- const cell = lane.cells.find((item) => item.status === "running") || lane.cells[lane.cells.length - 1];
60
- if (cell) this.onCellActivate(this.model.runId, cell.rel, cell);
61
- });
62
- return { el, name, meta, lane: null };
63
- });
64
- this.monitors = SCREENS.map(([x, y, width, height], index) => {
65
- const el = node("button", "hg-monitor", this.overlay);
66
- el.type = "button";
67
- el.style.left = (x / 2048 * 100) + "%";
68
- el.style.top = (y / 1151 * 100) + "%";
69
- el.style.width = (width / 2048 * 100) + "%";
70
- el.style.height = (height / 1151 * 100) + "%";
71
- const role = node("b", "hg-monitor-role", el);
72
- const status = node("span", "hg-monitor-status", el);
73
- const tail = node("span", "hg-monitor-tail", el);
74
- const time = node("span", "hg-monitor-time", el);
75
- this.listen(el, "click", () => {
76
- const cell = this.screenMeta[index];
77
- if (cell) this.onCellActivate(this.model.runId, cell.rel, cell);
78
- });
79
- return { el, role, status, tail, time };
80
- });
81
- const toolbar = this.host.parentElement.querySelector(".hive-scene-tools");
82
- this.pager = node("div", "hive-scene-pager");
83
- this.pager.hidden = true;
84
- this.prev = node("button", "hive-scene-btn", this.pager);
85
- this.prev.type = "button"; this.prev.textContent = "‹";
86
- this.pageLabel = node("span", "hive-scene-page-label", this.pager);
87
- this.next = node("button", "hive-scene-btn", this.pager);
88
- this.next.type = "button"; this.next.textContent = "›";
89
- if (toolbar) toolbar.insertBefore(this.pager, toolbar.querySelector(".hive-scene-spacer"));
90
- this.listen(this.prev, "click", () => this.showPage(this.page - 1));
91
- this.listen(this.next, "click", () => this.showPage(this.page + 1));
92
- this.bindSurface();
93
- this.resizeObserver = new ResizeObserver(() => this.resize());
94
- this.resizeObserver.observe(this.host);
95
- this.languageObserver = new MutationObserver(() => this.updateOverlay());
96
- this.languageObserver.observe(document.documentElement, { attributes: true, attributeFilter: ["data-lang"] });
97
- this.resize();
98
- this.updateOverlay();
99
- window.__hive3d = this;
31
+ function boxGeometry(gl){
32
+ const p=[-.5,-.5,-.5,.5,-.5,-.5,.5,.5,-.5,-.5,.5,-.5,-.5,-.5,.5,.5,-.5,.5,.5,.5,.5,-.5,.5,.5];
33
+ const ix=[0,2,1,0,3,2,4,5,6,4,6,7,0,1,5,0,5,4,3,7,6,3,6,2,1,2,6,1,6,5,0,4,7,0,7,3];
34
+ return geometry(gl,p,ix);
100
35
  }
101
-
102
- ReferenceScene.prototype.listen = function (el, type, fn, options) {
103
- el.addEventListener(type, fn, options);
104
- this.listeners.push(() => el.removeEventListener(type, fn, options));
105
- };
106
-
107
- ReferenceScene.prototype.bindSurface = function () {
108
- let drag = null;
109
- this.listen(this.host, "contextmenu", (event) => event.preventDefault());
110
- this.listen(this.host, "pointerdown", (event) => {
111
- if (!this.active || event.target.closest("button") || event.button !== 0) return;
112
- drag = { x: event.clientX, y: event.clientY, panX: this.pan.x, panY: this.pan.y };
113
- this.host.setPointerCapture(event.pointerId);
114
- this.host.classList.add("is-dragging");
115
- });
116
- this.listen(this.host, "pointermove", (event) => {
117
- if (!drag) return;
118
- this.pan.x = drag.panX + event.clientX - drag.x;
119
- this.pan.y = drag.panY + event.clientY - drag.y;
120
- this.applyTransform();
121
- });
122
- const end = () => { drag = null; this.host.classList.remove("is-dragging"); };
123
- this.listen(this.host, "pointerup", end);
124
- this.listen(this.host, "pointercancel", end);
125
- this.listen(this.host, "dblclick", (event) => { if (!event.target.closest("button")) this.resetView(); });
126
- this.listen(this.host, "wheel", (event) => {
127
- if (!this.active || event.target.closest("button")) return;
128
- event.preventDefault();
129
- this.zoomAt(Math.exp(event.deltaY * 0.0011));
130
- }, { passive: false });
131
- };
132
-
133
- ReferenceScene.prototype.sync = function (model) {
134
- const changedRun = !this.model || this.model.runId !== model.runId;
135
- this.model = model;
136
- this.cells = model.lanes.flatMap((lane, stageIdx) => lane.cells.map((cell) => ({ ...cell, stageIdx })));
137
- if (changedRun) {
138
- const running = this.cells.findIndex((cell) => cell.status === "running");
139
- this.page = running < 0 ? 0 : Math.floor(running / SCREENS.length);
140
- this.resetView();
141
- }
142
- this.showPage(this.page);
143
- };
144
-
145
- ReferenceScene.prototype.showPage = function (page) {
146
- const pages = Math.max(1, Math.ceil((this.cells || []).length / SCREENS.length));
147
- this.page = clamp(page, 0, pages - 1);
148
- this.screenMeta = (this.cells || []).slice(this.page * SCREENS.length, (this.page + 1) * SCREENS.length);
149
- this.pager.hidden = pages <= 1;
150
- this.prev.disabled = this.page === 0;
151
- this.next.disabled = this.page === pages - 1;
152
- this.pageLabel.textContent = (this.page + 1) + " / " + pages;
153
- this.updateOverlay();
36
+ function roundedBoxGeometry(gl, segments=4, radius=.12){
37
+ const positions=[],normals=[],indices=[];
38
+ const clamp=(v,lo,hi)=>Math.max(lo,Math.min(hi,v));
39
+ const point=(p)=>{
40
+ const core=p.map(v=>clamp(v,-.5+radius,.5-radius));
41
+ const d=p.map((v,i)=>v-core[i]),len=Math.hypot(...d)||1;
42
+ return {p:core.map((v,i)=>v+d[i]/len*radius),n:d.map(v=>v/len)};
43
+ };
44
+ const faces=[
45
+ (u,v)=>[.5,u,v],(u,v)=>[-.5,u,v],
46
+ (u,v)=>[u,.5,v],(u,v)=>[u,-.5,v],
47
+ (u,v)=>[u,v,.5],(u,v)=>[u,v,-.5]
48
+ ];
49
+ const emitTriangle=(p0,p1,p2)=>{
50
+ const a=point(p0),b=point(p1),d=point(p2);
51
+ const u=b.p.map((v,i)=>v-a.p[i]),v=d.p.map((q,i)=>q-a.p[i]);
52
+ const cross=[u[1]*v[2]-u[2]*v[1],u[2]*v[0]-u[0]*v[2],u[0]*v[1]-u[1]*v[0]];
53
+ const n=[0,1,2].map(i=>a.n[i]+b.n[i]+d.n[i]);
54
+ if(cross[0]*n[0]+cross[1]*n[1]+cross[2]*n[2]<0){const tmp=b.p;b.p=d.p;d.p=tmp;const tn=b.n;b.n=d.n;d.n=tn;}
55
+ for(const vtx of [a,b,d]){positions.push(...vtx.p);normals.push(...vtx.n);indices.push(indices.length);}
56
+ };
57
+ for(const face of faces){
58
+ for(let row=0;row<segments;row++)for(let col=0;col<segments;col++){
59
+ const u0=-.5+col/segments,u1=-.5+(col+1)/segments;
60
+ const v0=-.5+row/segments,v1=-.5+(row+1)/segments;
61
+ const p00=face(u0,v0),p10=face(u1,v0),p11=face(u1,v1),p01=face(u0,v1);
62
+ emitTriangle(p00,p10,p11);emitTriangle(p00,p11,p01);
63
+ }
64
+ }
65
+ return geometry(gl,positions,indices,normals);
66
+ }
67
+ function sphereGeometry(gl, rows=9, cols=12){
68
+ const p=[],ix=[];for(let r=0;r<=rows;r++){const v=r/rows*Math.PI;for(let c=0;c<=cols;c++){const u=c/cols*Math.PI*2;p.push(Math.sin(v)*Math.cos(u),Math.cos(v),Math.sin(v)*Math.sin(u));}}
69
+ for(let r=0;r<rows;r++)for(let c=0;c<cols;c++){const a=r*(cols+1)+c,b=a+cols+1;ix.push(a,a+1,b,b,a+1,b+1);}
70
+ return geometry(gl,p,ix,p.slice());
71
+ }
72
+ function sphereBandGeometry(gl,lat0,lat1,segments=48){
73
+ // Map a narrow band to a unit ellipsoid. The color band wraps the torso surface
74
+ // rather than floating as a flat cuboid in front of a curved shell.
75
+ const positions=[],normals=[],indices=[];
76
+ for(let row=0;row<2;row++){
77
+ const lat=row===0?lat0:lat1,cl=Math.cos(lat),sl=Math.sin(lat);
78
+ for(let i=0;i<=segments;i++){
79
+ const theta=i/segments*Math.PI*2,st=Math.sin(theta),ct=Math.cos(theta);
80
+ const p=[cl*st,sl,cl*ct];
81
+ positions.push(...p);normals.push(...p);
82
+ }
83
+ }
84
+ for(let i=0;i<segments;i++){
85
+ const a=i,b=i+1,c=segments+1+i,d=segments+1+i+1;
86
+ // Outward winding: at the camera-facing +Z point normals face the viewer.
87
+ indices.push(a,d,c,a,b,d);
88
+ }
89
+ return geometry(gl,positions,indices,normals);
90
+ }
91
+ function organicGeometry(gl){
92
+ // A polished, tapered leaf/wing silhouette with a gently domed center and two-sided normals.
93
+ let outline=[
94
+ [-.5,0,0],[-.34,.17,.018],[-.12,.255,.040],[.14,.225,.044],
95
+ [.38,.13,.026],[.5,0,0],[.38,-.13,.026],[.14,-.225,.044],
96
+ [-.12,-.255,.040],[-.34,-.17,.018]
97
+ ];
98
+ const area=outline.reduce((sum,p,i)=>{const q=outline[(i+1)%outline.length];return sum+p[0]*q[1]-q[0]*p[1];},0);
99
+ if(area<0)outline=outline.reverse();
100
+ const positions=[],normals=[],indices=[];
101
+ const emitSide=(sign)=>{
102
+ const centerIndex=positions.length/3;
103
+ positions.push(0,0,sign*.045);normals.push(0,0,sign);
104
+ for(const p of outline){positions.push(p[0],p[1],p[2]*sign);normals.push(0,0,sign);}
105
+ for(let i=0;i<outline.length;i++){
106
+ const a=centerIndex+1+i,b=centerIndex+1+(i+1)%outline.length;
107
+ if(sign>0)indices.push(centerIndex,a,b);else indices.push(centerIndex,b,a);
108
+ }
109
+ };
110
+ emitSide(1);emitSide(-1);
111
+ return geometry(gl,positions,indices,normals);
112
+ }
113
+ function hexGroutGeometry(gl){
114
+ // Build the complete staggered flat-top hex lattice as one static mesh. Drawing only
115
+ // three consecutive edges per tile creates crosses and broken hexes; unique full edges
116
+ // preserve the six-sided tiling while keeping buffer size/draw cost low.
117
+ const positions=[],normals=[],indices=[],seen=new Set();
118
+ const radius=.44,stepX=1.5*radius,stepZ=Math.sqrt(3)*radius,halfWidth=.0038,y=-.031;
119
+ const keyPoint=(x,z)=>Math.round(x*10000)+","+Math.round(z*10000);
120
+ // Flat-top hexes stagger by column (not by row); this keeps every shared edge aligned.
121
+ for(let col=-17;col<=17;col++){
122
+ const cx=col*stepX;
123
+ if(Math.abs(cx)>10.6)continue;
124
+ for(let row=0;row<40;row++){
125
+ const cz=-6.45+row*stepZ+(Math.abs(col%2)?stepZ/2:0);
126
+ if(cz>22.5)break;
127
+ for(let edge=0;edge<6;edge++){
128
+ const a0=edge*Math.PI/3,a1=(edge+1)*Math.PI/3;
129
+ const x0=cx+radius*Math.cos(a0),z0=cz+radius*Math.sin(a0);
130
+ const x1=cx+radius*Math.cos(a1),z1=cz+radius*Math.sin(a1);
131
+ const k0=keyPoint(x0,z0),k1=keyPoint(x1,z1),key=k0<k1?k0+"|"+k1:k1+"|"+k0;
132
+ if(seen.has(key))continue;
133
+ seen.add(key);
134
+ const dx=x1-x0,dz=z1-z0,len=Math.hypot(dx,dz)||1;
135
+ const px=dz/len*halfWidth,pz=-dx/len*halfWidth,base=positions.length/3;
136
+ positions.push(x0+px,y,z0+pz,x1+px,y,z1+pz,x1-px,y,z1-pz,x0-px,y,z0-pz);
137
+ for(let i=0;i<4;i++)normals.push(0,1,0);
138
+ indices.push(base,base+2,base+1,base,base+3,base+2);
139
+ }
140
+ }
141
+ }
142
+ return geometry(gl,positions,indices,normals);
143
+ }
144
+ function cylinderGeometry(gl,segments=20){
145
+ const p=[],n=[],ix=[];
146
+ // Side wall has independent normals from the flat end caps, avoiding pinched highlights.
147
+ for(let end=0;end<2;end++){
148
+ const y=end?.5:-.5;
149
+ for(let i=0;i<segments;i++){
150
+ const a=i/segments*Math.PI*2,x=Math.cos(a),z=Math.sin(a);
151
+ p.push(x,y,z);n.push(x,0,z);
152
+ }
153
+ }
154
+ for(let i=0;i<segments;i++){
155
+ const j=(i+1)%segments,b0=i,b1=j,t0=segments+i,t1=segments+j;
156
+ ix.push(b0,t0,b1,b1,t0,t1);
157
+ }
158
+ const bottomCenter=p.length/3;p.push(0,-.5,0);n.push(0,-1,0);
159
+ const bottomRing=p.length/3;
160
+ for(let i=0;i<segments;i++){const a=i/segments*Math.PI*2;p.push(Math.cos(a),-.5,Math.sin(a));n.push(0,-1,0);}
161
+ const topCenter=p.length/3;p.push(0,.5,0);n.push(0,1,0);
162
+ const topRing=p.length/3;
163
+ for(let i=0;i<segments;i++){const a=i/segments*Math.PI*2;p.push(Math.cos(a),.5,Math.sin(a));n.push(0,1,0);}
164
+ for(let i=0;i<segments;i++){const j=(i+1)%segments;ix.push(bottomCenter,bottomRing+i,bottomRing+j);ix.push(topCenter,topRing+j,topRing+i);}
165
+ return geometry(gl,p,ix,n);
166
+ }
167
+ function Scene(opts){
168
+ this.opts=opts;this.canvas=opts.canvas;this.overlay=opts.overlay;this.host=this.canvas.parentElement;
169
+ this.onCellActivate=opts.onCellActivate||function(){};this.cellRefresh=opts.cellRefresh||function(){return{};};
170
+ this.model=null;this.cells=[];this.page=0;this.zoom=1;this.yaw=0;this.pitch=0.42;this.panX=0;this.panY=0;this.active=false;this.raf=0;this.timer=0;this.listeners=[];this.screenMeta=[];this.displayMode="reference";
171
+ this.host.dataset.renderer="webgl";this.canvas.hidden=false;this.canvas.classList.add("hive-gl-live");
172
+ this.referenceImage=this.host.querySelector(".hive-reference");
173
+ this.layer=node("div","hive-reference-layer",this.host);
174
+ this.artboard=node("div","hive-reference-artboard",this.layer);
175
+ if(this.referenceImage)this.artboard.appendChild(this.referenceImage);
176
+ this.artboard.appendChild(this.overlay);this.overlay.replaceChildren();
177
+ try { this.initGL(); } catch(e) { console.error("CodeBee Hive3D WebGL init failed",e); this.failed=true; this.failure=e; return; }
178
+ this.stageMeta=PLATES.map(([x,y],i)=>{const el=node("button","hg-badge",this.overlay);el.type="button";el.style.left="0%";el.style.top="0%";const name=node("b","hg-name",el),meta=node("span","hg-meta",el);this.listen(el,"click",()=>{const lane=this.stageMeta[i].lane;if(!lane)return;const cell=lane.cells.find(c=>c.status==="running")||lane.cells[lane.cells.length-1];if(cell)this.onCellActivate(this.model.runId,cell.rel,cell);});return{el,name,meta,lane:null};});
179
+ this.links=Array.from({length:PLATES.length-1},()=>node("span","hg-flow-link",this.overlay));
180
+ this.monitors=SCREENS.map(([x,y,w,h],i)=>{const el=node("button", "hg-monitor", this.overlay);el.type="button";el.style.left="0%";el.style.top="0%";el.style.width=(w/2048*100)+"%";el.style.height=(h/1151*100)+"%";const role=node("b","hg-monitor-role",el),status=node("span","hg-monitor-status",el),tail=node("span","hg-monitor-tail",el),time=node("span","hg-monitor-time",el);this.listen(el,"click",()=>{const cell=this.screenMeta[i];if(cell)this.onCellActivate(this.model.runId,cell.rel,cell);});return{el,role,status,tail,time};});
181
+ const toolbar=this.host.parentElement.querySelector(".hive-scene-tools");this.pager=node("div","hive-scene-pager");this.pager.hidden=true;this.prev=node("button","hive-scene-btn",this.pager);this.prev.type="button";this.prev.textContent="‹";this.pageLabel=node("span","hive-scene-page-label",this.pager);this.next=node("button","hive-scene-btn",this.pager);this.next.type="button";this.next.textContent="›";if(toolbar)toolbar.insertBefore(this.pager,toolbar.querySelector(".hive-scene-spacer"));
182
+ this.listen(this.prev,"click",()=>this.showPage(this.page-1));this.listen(this.next,"click",()=>this.showPage(this.page+1));this.bindSurface();
183
+ this.resizeObserver=new ResizeObserver(()=>this.resize());this.resizeObserver.observe(this.host);this.languageObserver=new MutationObserver(()=>this.updateOverlay());this.languageObserver.observe(document.documentElement,{attributes:true,attributeFilter:["data-lang"]});this.resize();this.updateOverlay();window.__hive3d=this;
184
+ }
185
+ Scene.prototype.initGL=function(){
186
+ const gl=this.canvas.getContext("webgl",{alpha:true,antialias:true,powerPreference:"high-performance"})||this.canvas.getContext("experimental-webgl");
187
+ if(!gl)throw Error("WebGL unavailable");this.gl=gl;
188
+ const vs="attribute vec3 aPosition; attribute vec3 aNormal; attribute vec4 aColor; uniform mat4 uViewProj; varying vec4 vColor; varying float vShade; varying vec3 vWorld; varying vec3 vNormal; void main(){gl_Position=uViewProj*vec4(aPosition,1.0);vColor=aColor;vWorld=aPosition;vNormal=normalize(aNormal);vec3 n=normalize(aNormal);float sky=clamp(n.y*0.5+0.5,0.0,1.0);float side=abs(n.x)*0.035;float heightShade=mix(0.94,1.0,smoothstep(-0.2,3.6,aPosition.y));vShade=(0.88+0.08*sky+side)*heightShade;}";
189
+ const fs="precision mediump float; varying vec4 vColor; varying float vShade; varying vec3 vWorld; varying vec3 vNormal; uniform vec3 uEyePosition; void main(){vec3 n=normalize(vNormal);vec3 v=normalize(uEyePosition-vWorld);vec3 key=normalize(vec3(-0.48,0.86,0.42));vec3 fill=normalize(vec3(0.62,0.32,-0.72));vec3 warm=normalize(vec3(0.34,0.46,0.82));float ndl=max(dot(n,key),0.0);float fillN=max(dot(n,fill),0.0);float warmN=max(dot(n,warm),0.0);float hemi=clamp(n.y*0.5+0.5,0.0,1.0);vec3 base=vColor.rgb;float wallBlue=step(0.38,base.b)*step(0.35,base.g)*step(base.r*1.65,base.g)*step(1.7,vWorld.y);float wallGradient=0.86+0.14*smoothstep(1.7,4.5,vWorld.y)+0.055*exp(-pow((vWorld.x+2.1)*0.24,2.0));base=mix(base,base*wallGradient,wallBlue);vec3 h=normalize(key+v);vec3 hf=normalize(fill+v);float chroma=max(max(base.r,base.g),base.b)-min(min(base.r,base.g),base.b);float gloss=mix(0.10,0.34,smoothstep(0.10,0.82,chroma));float spec=pow(max(dot(n,h),0.0),mix(24.0,58.0,gloss))*gloss*0.42;float specFill=pow(max(dot(n,hf),0.0),36.0)*0.075;float fresnel=pow(1.0-max(dot(n,v),0.0),3.0);vec3 ambient=base*(0.55+0.13*hemi);vec3 direct=base*(0.34*ndl+0.16*fillN+0.08*warmN);vec3 highlight=vec3(1.0,0.91,0.78)*spec+vec3(0.52,0.84,1.0)*specFill+vec3(0.18,0.48,0.68)*fresnel*0.075;float cyan=max(0.0,min(base.g,base.b)-base.r)*0.65;vec3 emissive=base*cyan;vec3 lit=ambient+direct+highlight+emissive;lit*=vShade;gl_FragColor=vec4(min(lit,vec3(1.0)),vColor.a);}";
190
+ const program=gl.createProgram();gl.attachShader(program,shader(gl,gl.VERTEX_SHADER,vs));gl.attachShader(program,shader(gl,gl.FRAGMENT_SHADER,fs));gl.linkProgram(program);if(!gl.getProgramParameter(program,gl.LINK_STATUS))throw Error(gl.getProgramInfoLog(program));this.program=program;gl.useProgram(program);
191
+ this.aPosition=gl.getAttribLocation(program,"aPosition");this.aNormal=gl.getAttribLocation(program,"aNormal");this.aColor=gl.getAttribLocation(program,"aColor");this.uViewProj=gl.getUniformLocation(program,"uViewProj");this.uEyePosition=gl.getUniformLocation(program,"uEyePosition");this.box=boxGeometry(gl);this.roundBox=roundedBoxGeometry(gl,6,.12);this.sphere=sphereGeometry(gl,16,24);this.cylinder=cylinderGeometry(gl,20);this.organic=organicGeometry(gl);this.grout=hexGroutGeometry(gl);this.bandGoldTop=sphereBandGeometry(gl,-.29,-.03);this.bandGoldBottom=sphereBandGeometry(gl,-.97,-.61);this.bandSeparator=sphereBandGeometry(gl,-.51,-.38);this.bandGoldTop.smooth=true;this.bandGoldBottom.smooth=true;this.bandSeparator.smooth=true;gl.enable(gl.DEPTH_TEST);gl.enable(gl.CULL_FACE);gl.cullFace(gl.BACK);gl.enable(gl.BLEND);gl.blendFunc(gl.SRC_ALPHA,gl.ONE_MINUS_SRC_ALPHA);gl.clearColor(0,0,0,0);
154
192
  };
155
-
156
- ReferenceScene.prototype.updateOverlay = function () {
157
- const lanes = this.model ? this.model.lanes : [];
158
- this.stageMeta.forEach((plate, index) => {
159
- const lane = lanes[index];
160
- plate.lane = lane || null;
161
- const name = lane ? tr(lane.nameKey || lane.name) : tr(STAGES[index]);
162
- plate.name.textContent = tr("阶段 {0} · {1}", index + 1, name);
163
- plate.meta.textContent = lane ? tr("{0} / {1} 步骤", lane.settled, lane.count) : tr("等待任务");
164
- plate.el.classList.toggle("hg-active", !!(lane && lane.active));
165
- plate.el.disabled = !lane || !lane.cells.length;
166
- plate.el.title = tr("点击查看实时日志");
167
- });
168
- this.monitors.forEach((monitor, index) => {
169
- const cell = this.screenMeta[index];
170
- const refresh = cell && cell.rel ? this.cellRefresh(cell.rel) || {} : {};
171
- const state = cell ? cell.status : "idle";
172
- monitor.el.className = "hg-monitor st-" + state;
173
- monitor.el.disabled = !cell;
174
- monitor.el.dataset.log = cell ? cell.rel : "";
175
- monitor.role.textContent = cell ? tr(cell.role) : tr("空闲工位");
176
- monitor.status.textContent = cell ? tr(STATUS[state] || "完成") : tr("待命");
177
- monitor.tail.textContent = cell ? refresh.tail || cell.displayTail ||
178
- (state === "running" ? tr("等待日志输出…") : state === "queued" ? tr("等待执行") : tr("(无输出)")) : tr("等待任务");
179
- monitor.time.textContent = cell ? refresh.elapsed || cell.displayElapsed || "" : "";
180
- monitor.el.title = cell ? [tr(cell.role), tr(STATUS[state] || "完成"), cell.displayAgent,
181
- monitor.tail.textContent, tr("点击查看实时日志")].filter(Boolean).join(" · ") : tr("空闲工位");
182
- monitor.el.setAttribute("aria-label", monitor.el.title);
183
- });
184
- this.prev.setAttribute("aria-label", tr("上一组工位"));
185
- this.next.setAttribute("aria-label", tr("下一组工位"));
193
+ Scene.prototype.listen=function(el,type,fn,opts){el.addEventListener(type,fn,opts);this.listeners.push(()=>el.removeEventListener(type,fn,opts));};
194
+ Scene.prototype.bindSurface=function(){
195
+ let drag=null;this.listen(this.host,"contextmenu",e=>e.preventDefault());
196
+ this.listen(this.host,"pointerdown",e=>{if(!this.active||e.target.closest("button")||e.button!==0)return;drag={x:e.clientX,y:e.clientY,yaw:this.yaw,pitch:this.pitch,px:this.panX,py:this.panY,shift:e.shiftKey};this.host.setPointerCapture(e.pointerId);this.host.classList.add("is-dragging");});
197
+ this.listen(this.host,"pointermove",e=>{if(!drag)return;const dx=e.clientX-drag.x,dy=e.clientY-drag.y;if(this.displayMode==="reference"){this.panX=drag.px+dx/this.cssW;this.panY=drag.py+dy/this.cssH;this.applyArtboardTransform();return;}if(drag.shift){this.panX=drag.px+dx/this.cssW*5;this.panY=drag.py-dy/this.cssH*4;}else{this.yaw=drag.yaw+dx*.006;this.pitch=clamp(drag.pitch+dy*.004,-.05,.85);}this.render();});
198
+ const end=()=>{drag=null;this.host.classList.remove("is-dragging");};this.listen(this.host,"pointerup",end);this.listen(this.host,"pointercancel",end);
199
+ this.listen(this.host,"dblclick",e=>{if(!e.target.closest("button"))this.resetView();});
200
+ this.listen(this.host,"wheel",e=>{if(!this.active||e.target.closest("button"))return;e.preventDefault();this.zoom=clamp(this.zoom*Math.exp(-e.deltaY*.001),.72,2.4);this.applyArtboardTransform();this.render();},{passive:false});
201
+ this.listen(this.canvas,"webglcontextlost",e=>{e.preventDefault();this.setActive(false);if(this.opts.onFatal)this.opts.onFatal(e);});
186
202
  };
203
+ Scene.prototype.add=function(mesh,x,y,z,sx,sy,sz,color,rotY,rotZ,rotX){this.objects.push({mesh,x,y,z,sx,sy,sz,color,rotY:rotY||0,rotZ:rotZ||0,rotX:rotX||0});};
204
+ Scene.prototype.addLink=function(a,b,radius,color){const dx=b[0]-a[0],dy=b[1]-a[1],dz=b[2]-a[2],length=Math.hypot(dx,dy,dz)||.001,rotX=Math.asin(clamp(dz/length,-1,1)),rotZ=Math.atan2(-dx,dy);this.add(this.cylinder,(a[0]+b[0])/2,(a[1]+b[1])/2,(a[2]+b[2])/2,radius,length,radius,color,0,rotZ,rotX);};
205
+ Scene.prototype.buildWorld=function(){
206
+ const b=this.box,rb=this.roundBox,s=this.sphere;this.objects=[];
207
+ // Bright, calm blue-white studio with a continuous floor and a single clean feature wall.
208
+ this.add(b,0,-.18,5.5,23,.28,34,[.97,.985,1,1]);
209
+ this.add(b,0,2.70,-6.45,19,3.65,.24,[.10,.52,.89,1]);
210
+ this.add(b,0,5.10,-6.30,19,.16,.18,[.98,.99,1,1]);
211
+ // Wall seams, lower trim, and glass side windows.
212
+ this.add(b,0,.90,-6.28,18.7,.12,.06,[.08,.42,.78,1]);
213
+ this.add(b,0,.82,-6.285,18.7,.035,.035,[.25,.67,.94,1]);
214
+ for(let x=-8;x<=8;x+=2.65)this.add(b,x,2.70,-6.29,.018,3.40,.025,[.12,.40,.72,1]);
215
+ for(const side of [-1,1]){
216
+ // Open-framed side glazing: avoid opaque side slabs that visually cut the room into boxes.
217
+ for(const y of [.12,4.62])this.add(b,side*8.95,y,1.0,.075,.075,15.2,[.78,.87,.93,1]);
218
+ for(let z=-5.55;z<=7.77;z+=2.22){
219
+ this.add(b,side*8.95,2.36,z,.075,4.48,.075,[.84,.91,.96,1]);
220
+ this.add(b,side*8.90,2.36,z-1.11,.018,4.34,2.10,[.55,.78,.92,.12]);
221
+ }
222
+ }
223
+ // Ceiling light fixtures and soft-colored illuminated panels.
224
+ for(let x=-6.3;x<=6.4;x+=6.3){
225
+ this.add(b,x,5.36,-3.35,2.65,.075,.30,[.12,.17,.23,1]);
226
+ this.add(b,x,5.29,-3.35,2.40,.045,.24,[1,.99,.91,1]);
227
+ }
228
+ // Six white phase controls are projected on this wall by the live HTML overlay.
229
+ for(let i=0;i<6;i++){
230
+ const x=-7.75+i*3.10;
231
+ }
232
+ // The reference uses a continuous luminous workflow rail below the six white phase pills.
233
+ this.add(b,0,2.10,-6.005,15.8,.035,.024,[.22,.89,1,1]);
234
+ for(let i=0;i<6;i++){
235
+ const x=-7.75+i*3.10;
236
+ this.add(b,x,2.34,-6.005,.035,.48,.024,[.22,.89,1,1]);
237
+ }
238
+ // Continuous, pale hex grout is one static mesh: no crossed edges and no thousands of scene objects.
239
+ this.add(this.grout,0,0,0,1,1,1,[.77,.83,.88,1]);
240
+ // Reference layout: eight compact rear stations and six wider front stations.
241
+ // Every monitor overlay is projected from these exact same coordinates below.
242
+ const deskRows=[
243
+ {row:0,z:-1.80,width:2.03,screenWidth:1.28,screenHeight:.70,xs:[-8.4,-6.0,-3.6,-1.2,1.2,3.6,6.0,8.4]},
244
+ {row:1,z:5.85,width:2.42,screenWidth:1.48,screenHeight:.82,xs:[-6.0,-3.6,-1.2,1.2,3.6,6.0]}
245
+ ];
246
+ this.deskPositions=[];
247
+ for(const config of deskRows)for(const x of config.xs){
248
+ const {row,z,width,screenWidth,screenHeight}=config,screenY=row===1?1.38:1.205;
249
+ this.deskPositions.push({row,x,z,width,screenWidth,screenHeight});
250
+ this.add(rb,x,.54,z,width,.15,1.20,[.985,.99,1,1]);
251
+ this.add(b,x,.49,z-.603,width*.72,.014,.012,[.07,.44,.62,1]);
252
+ // Front edge highlight and rear cable channel make the desktop read as layered furniture.
253
+ this.add(rb,x,.535,z+.594,width*.94,.035,.035,[.98,.99,1,1]);
254
+ this.add(b,x,.455,z-.565,width*.64,.028,.024,[.61,.70,.79,1]);
255
+ this.add(rb,x-width*.36,.25,z+.03,.43,.52,1.02,[.90,.93,.97,1]);
256
+ this.add(rb,x+width*.36,.25,z+.03,.43,.52,1.02,[.90,.93,.97,1]);
257
+ // A compact three-drawer pedestal gives each station the white office furniture silhouette of the reference.
258
+ this.add(rb,x+width*.30,.27,z+.445,width*.30,.42,.26,[.90,.935,.98,1]);
259
+ for(const drawerY of [.18,.28,.38])this.add(b,x+width*.30,drawerY,z+.579,width*.22,.012,.012,[.62,.71,.80,1]);
260
+ this.add(b,x+width*.30,.41,z+.581,width*.085,.018,.014,[.45,.58,.70,1]);
261
+ this.add(rb,x,.43,z-.22,width*.78,.045,.74,[.89,.93,.97,1]);
262
+ this.add(rb,x,screenY,z-.48,screenWidth+.18,screenHeight+.18,.13,[.035,.055,.075,1]);
263
+ this.add(rb,x,screenY,z-.432,screenWidth+.08,screenHeight+.08,.045,[.15,.20,.25,1]);
264
+ this.add(b,x,screenY,z-.405,screenWidth,screenHeight,.018,[.018,.105,.18,1]);
265
+ this.add(this.cylinder,x,.78,z-.43,.072,.40,.072,[.32,.39,.45,1]);
266
+ this.add(b,x,.49,z-.27,.54,.045,.34,[.16,.20,.24,1]);
267
+ this.add(b,x,screenY-screenHeight*.42,z-.382,screenWidth*.84,.018,.014,[.10,.83,.94,1]);
268
+ this.add(rb,x-.18,.635,z+.25,.60,.035,.20,[.11,.14,.17,1]);
269
+ this.add(s,x+.40,.648,z+.22,.060,.025,.075,[.055,.065,.075,1]);
270
+ this.add(b,x+.40,.674,z+.20,.009,.005,.024,[.30,.66,.78,1]);
271
+ for(let row=0;row<4;row++)for(let col=0;col<12;col++){
272
+ const keyColor=(row===0&&col%4===0)?[.54,.61,.68,1]:((row+col)%5===0?[.40,.48,.56,1]:[.22,.27,.32,1]);
273
+ this.add(b,x-.45+col*.049,.659,z+.185+row*.045,.030,.009,.024,keyColor);
274
+ }
275
+ this.add(b,x-.18,.62,z+.25,.66,.012,.24,[.68,.75,.82,1]);
276
+ const binderStart=x+width/2-.48;
277
+ for(let k=0;k<4;k++)this.add(b,binderStart+k*.095,.83,z+.35,.075,.43,.22,[[.13,.43,.78,1],[.95,.42,.24,1],[.16,.66,.46,1],[.93,.72,.28,1]][k]);
278
+ const blueBinderStart=x-width/2+.19;
279
+ for(let k=0;k<3;k++)this.add(b,blueBinderStart+k*.095,.79,z+.34,.075,.35,.18,[[.08,.34,.80,1],[.10,.51,.91,1],[.06,.25,.65,1]][k]);
280
+ this.add(this.cylinder,x,.08,z+1.10,.075,.28,.075,[.12,.15,.18,1]);
281
+ this.add(rb,x,.30,z+1.10,.94,.12,.68,[.11,.14,.17,1]);
282
+ this.add(rb,x,.50,z+.82,1.02,.60,.15,[.12,.17,.22,1]);
283
+ this.add(rb,x,.50,z+.91,.76,.43,.024,[.20,.25,.29,1]);
284
+ this.add(rb,x,.42,z+1.10,.48,.12,.12,[.18,.22,.25,1]);
285
+ // Contrasting graphite upholstery separates the chair from the bee chassis.
286
+ this.add(rb,x,.50,z+.90,.78,.43,.050,[.16,.20,.24,1]);
287
+ for(const ribY of [.34,.48,.62])this.add(rb,x,ribY,z+.93,.58,.026,.020,[.34,.39,.43,1]);
288
+ for(const side of [-1,1])this.add(rb,x+side*.49,.50,z+.93,.035,.48,.025,[.23,.30,.36,1]);
289
+ for(const side of [-1,1]){
290
+ this.add(rb,x+side*.39,.63,z+.96,.10,.075,.38,[.14,.18,.21,1]);
291
+ this.add(b,x+side*.39,.48,z+.97,.035,.28,.045,[.10,.13,.16,1]);
292
+ }
293
+ for(let k=0;k<5;k++){const a=k*Math.PI*2/5;const wx=x+Math.cos(a)*.48,wz=z+1.10+Math.sin(a)*.40;this.add(b,x+Math.cos(a)*.38,.035,z+1.10+Math.sin(a)*.32,.30,.06,.075,[.08,.10,.12,1],a);this.add(s,wx,.005,wz,.085,.065,.075,[.035,.045,.055,1]);}
294
+ const by=1.25,bz=z+.98;
295
+ // Bee assistant: distinct head, dark chassis, striped abdomen, headset and translucent wings.
296
+ this.add(s,x,by-.42,bz+.005,.31,.31,.235,[.075,.09,.11,1]);
297
+ // Slightly enlarge the band ellipsoid to avoid depth-buffer z-fighting with the shell.
298
+ const shellX=.315,shellY=.315,shellZ=.239,shellCenterY=by-.42,shellCenterZ=bz+.005;
299
+ this.add(this.bandGoldTop,x,shellCenterY,shellCenterZ,shellX,shellY,shellZ,[1,.66,.035,1]);
300
+ this.add(this.bandGoldBottom,x,shellCenterY,shellCenterZ,shellX,shellY,shellZ,[.99,.56,.018,1]);
301
+ this.add(this.bandSeparator,x,shellCenterY,shellCenterZ,shellX,shellY,shellZ,[.045,.055,.065,1]);
302
+ this.add(s,x,by,bz,.405,.37,.345,[1,.70,.045,1]);
303
+ // The face points toward the monitor; the rear silhouette carries the headset details.
304
+ for(const side of [-1,1]){
305
+ this.add(s,x+side*.382,by+.015,bz+.035,.078,.118,.094,[.045,.055,.065,1]);
306
+ this.add(s,x+side*.407,by+.015,bz+.052,.030,.052,.048,[.99,.68,.045,1]);
307
+ this.add(this.cylinder,x+side*.105,by+.43,bz+.105,.020,.30,.020,[.045,.055,.065,1],0,-side*.18);
308
+ this.add(s,x+side*.13,by+.57,bz+.108,.032,.032,.032,[.10,.12,.14,1]);
309
+ this.add(s,x+side*.135,by+.595,bz+.132,.024,.024,.024,[1,.73,.10,1]);
310
+ }
311
+ this.add(rb,x,by-.335,bz+.156,.32,.026,.018,[.045,.055,.065,1]);
312
+ this.add(s,x,by+.30,bz+.18,.115,.035,.06,[.045,.055,.065,1]);
313
+ // Short forward-projecting thighs tuck under the desktop and visually anchor the bee to its chair.
314
+ for(const side of [-1,1]){
315
+ this.add(s,x+side*.125,.465,z+.71,.075,.13,.14,[.075,.09,.11,1]);
316
+ this.add(rb,x+side*.13,.315,z+.66,.10,.060,.15,[.99,.66,.035,1]);
317
+ }
318
+ // Shoulders, articulated elbows and forearms form continuous 3D links down to the keyboard.
319
+ for(const side of [-1,1]){
320
+ const shoulder=[x+side*.32,.99,z+.56],elbow=[x+side*.43,.83,z+.30],wrist=[x+side*.36,.675,z-.12],hand=[x+side*.35,.655,z-.20];
321
+ this.addLink(shoulder,elbow,.039,[.19,.22,.25,1]);
322
+ this.addLink(elbow,wrist,.034,[.20,.24,.27,1]);
323
+ this.addLink(wrist,hand,.030,[.96,.63,.035,1]);
324
+ this.add(s,shoulder[0],shoulder[1],shoulder[2],.075,.075,.085,[.19,.22,.25,1]);
325
+ this.add(s,elbow[0],elbow[1],elbow[2],.065,.065,.065,[.32,.36,.39,1]);
326
+ this.add(s,wrist[0],wrist[1],wrist[2],.052,.052,.052,[.24,.29,.32,1]);
327
+ this.add(rb,hand[0],hand[1],hand[2],.085,.045,.070,[.96,.63,.035,1]);
328
+ this.add(s,x+side*.35,.655,z-.20,.070,.035,.055,[.98,.68,.045,1]);
329
+ for(let finger=0;finger<3;finger++)this.add(b,x+side*.35+(finger-1)*.026,.638,z-.255,.012,.010,.046,[.22,.29,.35,1]);
330
+ }
331
+ // Small cyan service badge on the back of the chassis.
332
+ this.add(rb,x,by-.585,bz+.205,.13,.068,.018,[.025,.10,.14,1]);
333
+ this.add(rb,x,by-.585,bz+.218,.082,.016,.010,[.16,.88,.98,1]);
334
+ // Smooth glassy oval wings sit at the shoulder line. Sphere geometry keeps their
335
+ // silhouette rounded from every camera angle and lets the opaque body occlude them.
336
+ this.add(s,x-.43,by-.245,bz+.12,.43,.105,.075,[.58,.84,1,.32],0,.10);
337
+ this.add(s,x+.43,by-.245,bz+.12,.43,.105,.075,[.58,.84,1,.32],0,-.10);
338
+ this.add(s,x-.40,by-.225,bz+.155,.28,.060,.040,[.93,.99,1,.18],0,.08);
339
+ this.add(s,x+.40,by-.225,bz+.155,.28,.060,.040,[.93,.99,1,.18],0,-.08);
187
340
 
188
- ReferenceScene.prototype.setActive = function (active) {
189
- this.active = !!active;
190
- this.layer.hidden = !this.active;
191
- if (this.timer) clearInterval(this.timer);
192
- this.timer = null;
193
- if (this.active) {
194
- this.updateOverlay();
195
- this.timer = setInterval(() => {
196
- if (!document.hidden && this.host.offsetParent !== null) this.updateOverlay();
197
- }, 1000);
341
+ }
342
+ // Planters and stylized leaves soften the room edges.
343
+ for(const x of [-9.05,9.05]){
344
+ this.add(s,x,-.032,-4.70,.66,.012,.70,[.055,.09,.14,.24]);
345
+ this.add(rb,x,.17,-4.70,.58,.34,.58,[.70,.77,.81,1]);
346
+ this.add(this.cylinder,x,.70,-4.70,.065,.78,.065,[.31,.28,.19,1]);
347
+ // Layered foliage with varied yaw and drooping leaf tips, so the planters read as broad indoor plants.
348
+ for(let k=0;k<18;k++){
349
+ const a=k/18*Math.PI*2,spread=Math.sin(a)*.62;
350
+ const y=.94+(k%5)*.145,rad=.20+(k%4)*.095;
351
+ const leafColor=[[.045,.30,.15,1],[.055,.38,.19,1],[.075,.45,.22,1],[.15,.50,.27,1]][k%4];
352
+ this.add(this.organic,x+Math.sin(a)*rad,y,-4.70+Math.cos(a)*rad*.62,
353
+ .58+(k%3)*.065,.78+(k%4)*.10,.05,leafColor,a*.62-Math.PI*.18,Math.PI/2+spread);
354
+ }
355
+ for(let k=0;k<7;k++){
356
+ const a=k/7*Math.PI*2,spread=Math.sin(a)*.48;
357
+ this.add(this.organic,x+Math.sin(a)*.12,1.62+(k%3)*.10,-4.70+Math.cos(a)*.14,
358
+ .38,.60,.035,[.07,.40,.20,1],a*.65,Math.PI/2+spread);
359
+ }
360
+ }
361
+ // Low-opacity contact shadows share the same 8+6 workstation positions.
362
+ for(const desk of this.deskPositions){
363
+ const {x,z,width}=desk;
364
+ this.add(s,x+.07,-.033,z+.04,width*.80,.012,.92,[.055,.09,.14,.30]);
365
+ this.add(s,x,-.032,z+1.00,.94,.010,.64,[.065,.11,.17,.24]);
198
366
  }
199
367
  };
200
-
201
- ReferenceScene.prototype.resize = function () {
202
- this.cssW = this.host.clientWidth;
203
- this.cssH = this.host.clientHeight;
204
- this.applyTransform();
368
+ Scene.prototype.buildBatches=function(){
369
+ const channels={opaque:{positions:[],normals:[],colors:[]},transparent:{positions:[],normals:[],colors:[]}};
370
+ const emit=(target,p,n,color)=>{
371
+ target.positions.push(p[0],p[1],p[2]);target.normals.push(n[0],n[1],n[2]);
372
+ target.colors.push(color[0],color[1],color[2],color[3]==null?1:color[3]);
373
+ };
374
+ const worldPoint=(o,v)=>{const x=v[0]*o.sx,y=v[1]*o.sy,z=v[2]*o.sz,a=o.rotY||0,ca=Math.cos(a),sa=Math.sin(a),rx=ca*x+sa*z,rzY=-sa*x+ca*z,cx=Math.cos(o.rotX||0),sx=Math.sin(o.rotX||0),ry=cx*y-sx*rzY,rz=sx*y+cx*rzY,b=o.rotZ||0,cb=Math.cos(b),sb=Math.sin(b);return [o.x+cb*rx-sb*ry,o.y+sb*rx+cb*ry,o.z+rz];};
375
+ const smoothNormal=(o,v)=>{const x=v[0]/Math.max(.0001,o.sx),y=v[1]/Math.max(.0001,o.sy),z=v[2]/Math.max(.0001,o.sz),a=o.rotY||0,ca=Math.cos(a),sa=Math.sin(a),rx=ca*x+sa*z,rzY=-sa*x+ca*z,cx=Math.cos(o.rotX||0),sx=Math.sin(o.rotX||0),ry=cx*y-sx*rzY,rz=sx*y+cx*rzY,b=o.rotZ||0,cb=Math.cos(b),sb=Math.sin(b),n=[cb*rx-sb*ry,sb*rx+cb*ry,rz],l=Math.hypot(n[0],n[1],n[2])||1;return n.map(v=>v/l);};
376
+ const faceNormal=(a,b,c)=>{const u=[b[0]-a[0],b[1]-a[1],b[2]-a[2]],v=[c[0]-a[0],c[1]-a[1],c[2]-a[2]],n=[u[1]*v[2]-u[2]*v[1],u[2]*v[0]-u[0]*v[2],u[0]*v[1]-u[1]*v[0]],l=Math.hypot(n[0],n[1],n[2])||1;return n.map(v=>v/l);};
377
+ for(const o of this.objects){
378
+ const mesh=o.mesh,src=mesh.positions,ix=mesh.indices,normals=mesh.normals||src;
379
+ const target=(o.color[3]!=null&&o.color[3]<.999)?channels.transparent:channels.opaque;
380
+ for(let i=0;i<ix.length;i+=3){
381
+ const local=[0,1,2].map(k=>[src[ix[i+k]*3],src[ix[i+k]*3+1],src[ix[i+k]*3+2]]);
382
+ const world=local.map(v=>worldPoint(o,v));
383
+ const smooth=mesh===this.sphere||mesh.smooth;
384
+ const face=smooth?null:faceNormal(world[0],world[1],world[2]);
385
+ for(let k=0;k<3;k++){
386
+ const ix3=ix[i+k]*3,normal=[normals[ix3],normals[ix3+1],normals[ix3+2]];
387
+ emit(target,world[k],face||smoothNormal(o,normal),o.color);
388
+ }
389
+ }
390
+ }
391
+ const gl=this.gl,upload=data=>{if(!data.length)return null;const buffer=gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER,buffer);gl.bufferData(gl.ARRAY_BUFFER,new Float32Array(data),gl.STATIC_DRAW);return buffer;};
392
+ this.batchBuffers={};
393
+ for(const key of ["opaque","transparent"]){
394
+ const channel=channels[key];
395
+ this.batchBuffers[key]={positions:upload(channel.positions),normals:upload(channel.normals),colors:upload(channel.colors),count:channel.positions.length/3};
396
+ }
397
+ for(const mesh of [this.box,this.roundBox,this.sphere,this.cylinder,this.organic,this.grout,this.bandGoldTop,this.bandGoldBottom,this.bandSeparator])if(mesh){if(mesh.p)gl.deleteBuffer(mesh.p);if(mesh.ix)gl.deleteBuffer(mesh.ix);mesh.p=null;mesh.ix=null;}
205
398
  };
206
- ReferenceScene.prototype.applyTransform = function () {
207
- const xLimit = this.cssW * (this.zoom - 1) / 2;
208
- const yLimit = this.cssH * (this.zoom - 1) / 2;
209
- this.pan.x = clamp(this.pan.x, -xLimit, xLimit);
210
- this.pan.y = clamp(this.pan.y, -yLimit, yLimit);
211
- this.layer.style.transform = "translate(" + this.pan.x + "px," + this.pan.y + "px) scale(" + this.zoom + ")";
399
+ Scene.prototype.drawBatch=function(batch){
400
+ if(!batch||!batch.count)return;
401
+ const gl=this.gl;
402
+ gl.bindBuffer(gl.ARRAY_BUFFER,batch.positions);gl.enableVertexAttribArray(this.aPosition);gl.vertexAttribPointer(this.aPosition,3,gl.FLOAT,false,0,0);
403
+ gl.bindBuffer(gl.ARRAY_BUFFER,batch.normals);gl.enableVertexAttribArray(this.aNormal);gl.vertexAttribPointer(this.aNormal,3,gl.FLOAT,false,0,0);
404
+ gl.bindBuffer(gl.ARRAY_BUFFER,batch.colors);gl.enableVertexAttribArray(this.aColor);gl.vertexAttribPointer(this.aColor,4,gl.FLOAT,false,0,0);
405
+ gl.drawArrays(gl.TRIANGLES,0,batch.count);
212
406
  };
213
- ReferenceScene.prototype.zoomAt = function (factor) {
214
- this.zoom = clamp(this.zoom / factor, 1, 3);
215
- this.applyTransform();
407
+ Scene.prototype.render=function(){
408
+ if(!this.gl||!this.active||this.displayMode!=="live3d")return;const gl=this.gl;const w=Math.max(1,this.cssW),h=Math.max(1,this.cssH);const dpr=Math.min(window.devicePixelRatio||1,1.6);
409
+ const bw=Math.floor(w*dpr),bh=Math.floor(h*dpr);if(this.canvas.width!==bw||this.canvas.height!==bh){this.canvas.width=bw;this.canvas.height=bh;}
410
+ gl.viewport(0,0,bw,bh);gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);gl.useProgram(this.program);
411
+ const radius=27.0/this.zoom;const eye=[Math.sin(this.yaw)*radius,3.1+Math.sin(this.pitch)*radius*.58,Math.cos(this.yaw)*radius-1.3];const view=mat4.lookAt(eye,[0,.62,0],[0,1,0]);const proj=mat4.perspective(.40,w/h,.1,70);const vp=mat4.multiply(mat4.multiply(proj,view),mat4.translate(this.panX,this.panY,0));this.viewProj=vp;
412
+ gl.uniformMatrix4fv(this.uViewProj,false,vp);gl.uniform3f(this.uEyePosition,eye[0],eye[1],eye[2]);
413
+ this.drawBatch(this.batchBuffers&&this.batchBuffers.opaque);
414
+ const translucent=this.batchBuffers&&this.batchBuffers.transparent;
415
+ if(translucent&&translucent.count){gl.depthMask(false);this.drawBatch(translucent);gl.depthMask(true);}
416
+ this.layoutOverlayPositions();
216
417
  };
217
- ReferenceScene.prototype.resetView = function () {
218
- this.zoom = 1;
219
- this.pan = { x: 0, y: 0 };
220
- this.applyTransform();
418
+ Scene.prototype.projectWorld=function(x,y,z){
419
+ if(!this.viewProj||!this.cssW||!this.cssH)return null;
420
+ const m=this.viewProj,px=x,py=y;
421
+ const cx=m[0]*px+m[4]*py+m[8]*z+m[12],cy=m[1]*px+m[5]*py+m[9]*z+m[13],cw=m[3]*px+m[7]*py+m[11]*z+m[15];
422
+ if(cw<=.05)return null;
423
+ const nx=cx/cw,ny=cy/cw;
424
+ return{x:(nx*.5+.5)*this.cssW,y:(1-(ny*.5+.5))*this.cssH,visible:nx> -1.3&&nx<1.3&&ny> -1.3&&ny<1.3};
221
425
  };
222
- ReferenceScene.prototype.info = function () {
223
- return { renderer: "reference", cells: (this.cells || []).length, screens: SCREENS.length,
224
- lanes: this.model ? this.model.lanes.length : 0, page: this.page, zoom: this.zoom, cssW: this.cssW, cssH: this.cssH };
426
+ Scene.prototype.projectRect=function(points){
427
+ const projected=points.map(p=>this.projectWorld(p[0],p[1],p[2]));
428
+ if(projected.some(p=>!p||!p.visible))return null;
429
+ const xs=projected.map(p=>p.x),ys=projected.map(p=>p.y);
430
+ const left=Math.min(...xs),top=Math.min(...ys),right=Math.max(...xs),bottom=Math.max(...ys);
431
+ if(right<0||bottom<0||left>this.cssW||top>this.cssH)return null;
432
+ return {left,top,right,bottom,width:right-left,height:bottom-top,centerX:(left+right)/2,centerY:(top+bottom)/2};
225
433
  };
226
- ReferenceScene.prototype.projectCell = function (rel) {
227
- const index = this.screenMeta.findIndex((cell) => cell.rel === rel);
228
- if (index < 0) return null;
229
- const [x, y, width, height] = SCREENS[index];
230
- return { x: (x + width / 2) / 2048 * this.cssW, y: (y + height / 2) / 1151 * this.cssH };
434
+ Scene.prototype.layoutOverlayPositions=function(){
435
+ if(!this.overlay||this.overlay.hidden||!this.cssW||!this.cssH)return;
436
+ if(this.displayMode==="reference"){
437
+ // The PNG is the clean artwork crop (the original supplied screenshot had a 15px/10px outer frame).
438
+ // These hit areas are measured against the artwork crop itself, not the screenshot frame.
439
+ const stageRects=[[89,86,116,46],[247,86,116,46],[404,86,116,46],[562,86,115,46],[718,86,116,46],[873,86,116,46]];
440
+ const monitorRects=[[80,223,78,40],[199,223,78,40],[320,223,78,40],[439,223,78,40],[559,223,78,40],[677,223,78,40],[795,223,78,40],[914,223,78,40],[79,368,103,63],[238,368,103,63],[401,368,103,63],[559,368,103,63],[724,368,103,63],[881,368,103,63]];
441
+ const place=(el,rect)=>{el.style.visibility="visible";el.style.left=(rect[0]/1080*100)+"%";el.style.top=(rect[1]/608*100)+"%";el.style.width=(rect[2]/1080*100)+"%";el.style.height=(rect[3]/608*100)+"%";el.style.transform="none";};
442
+ this.stageMeta.forEach((p,i)=>place(p.el,stageRects[i]));
443
+ this.links.forEach(link=>{link.style.visibility="hidden";});
444
+ this.monitors.forEach((m,i)=>{const rect=monitorRects[i];if(!rect){m.el.style.visibility="hidden";return;}place(m.el,rect);});
445
+ return;
446
+ }
447
+ // Stage labels occupy the front face of each physical wall panel, rather than a fixed HUD row.
448
+ this.stageMeta.forEach((p,i)=>{
449
+ const x=-7.75+i*3.10,z=-6.07;
450
+ const rect=this.projectRect([[x-1.34,2.58,z],[x+1.34,2.58,z],[x+1.34,3.52,z],[x-1.34,3.52,z]]);
451
+ if(!rect){p.el.style.visibility="hidden";return;}
452
+ p.el.style.visibility="visible";
453
+ p.el.style.left=(rect.centerX/this.cssW*100)+"%";
454
+ p.el.style.top=(rect.centerY/this.cssH*100)+"%";
455
+ p.el.style.width=Math.max(54,rect.width*.96)+"px";
456
+ p.el.style.height=Math.max(28,rect.height*.88)+"px";
457
+ p.el.style.transform="translate(-50%,-50%)";
458
+ });
459
+ // Connectors stay on the wall plane and follow the same camera transform as the cards.
460
+ this.links.forEach((link,i)=>{
461
+ const leftX=-7.75+i*3.10+1.40,rightX=-7.75+(i+1)*3.10-1.40;
462
+ const a=this.projectWorld(leftX,3.05,-6.03),b=this.projectWorld(rightX,3.05,-6.03);
463
+ if(!a||!b||!a.visible||!b.visible){link.style.visibility="hidden";return;}
464
+ const dx=b.x-a.x,dy=b.y-a.y;
465
+ link.style.visibility="visible";
466
+ link.style.left=(a.x/this.cssW*100)+"%";link.style.top=(a.y/this.cssH*100)+"%";
467
+ link.style.width=Math.hypot(dx,dy)+"px";
468
+ link.style.transform="translateY(-50%) rotate("+Math.atan2(dy,dx)+"rad)";
469
+ });
470
+ // Project all four corners of the actual glass. This keeps task text inside each screen
471
+ // while the camera rotates, pitches, pans or zooms; the former three-point average drifted.
472
+ this.monitors.forEach((m,i)=>{
473
+ const desk=this.deskPositions&&this.deskPositions[i];
474
+ if(!desk){m.el.style.visibility="hidden";return;}
475
+ const {x,z,screenWidth,screenHeight,row}=desk,screenY=row===1?1.38:1.205;
476
+ const halfW=screenWidth/2,halfH=screenHeight/2;
477
+ const visibleBottom=screenY-halfH*.56; // Reserve the lower screen rim so DOM text never paints across a bee silhouette.
478
+ const rect=this.projectRect([[x-halfW,visibleBottom,z-.418],[x+halfW,visibleBottom,z-.418],
479
+ [x+halfW,screenY+halfH,z-.418],[x-halfW,screenY+halfH,z-.418]]);
480
+ if(!rect||rect.width<8||rect.height<6){m.el.style.visibility="hidden";return;}
481
+ m.el.style.visibility="visible";
482
+ m.el.style.left=(rect.left/this.cssW*100)+"%";
483
+ m.el.style.top=(rect.top/this.cssH*100)+"%";
484
+ m.el.style.width=Math.max(8,rect.width-2)+"px";
485
+ m.el.style.height=Math.max(6,rect.height-2)+"px";
486
+ m.el.style.transform="none";
487
+ });
231
488
  };
232
- ReferenceScene.prototype.dispose = function () {
233
- this.setActive(false);
234
- this.resizeObserver.disconnect();
235
- this.languageObserver.disconnect();
236
- this.listeners.forEach((remove) => remove());
237
- this.pager.remove();
238
- if (window.__hive3d === this) delete window.__hive3d;
489
+ Scene.prototype.sync=function(model){const changed=!this.model||this.model.runId!==model.runId;this.model=model;this.cells=model.lanes.flatMap((lane,stageIdx)=>lane.cells.map(cell=>({...cell,stageIdx})));if(changed){const r=this.cells.findIndex(c=>c.status==="running");this.page=r<0?0:Math.floor(r/SCREENS.length);this.resetView();}this.showPage(this.page);};
490
+ Scene.prototype.showPage=function(page){const pages=Math.max(1,Math.ceil((this.cells||[]).length/SCREENS.length));this.page=clamp(page,0,pages-1);this.screenMeta=(this.cells||[]).slice(this.page*SCREENS.length,(this.page+1)*SCREENS.length);this.pager.hidden=pages<=1;this.prev.disabled=this.page===0;this.next.disabled=this.page===pages-1;this.pageLabel.textContent=(this.page+1)+" / "+pages;this.updateOverlay();};
491
+ Scene.prototype.updateOverlay=function(){
492
+ const lanes=this.model?this.model.lanes:[];
493
+ this.stageMeta.forEach((p,i)=>{const lane=lanes[i];p.lane=lane||null;p.name.textContent=tr("阶段 {0} · {1}",i+1,lane?tr(lane.nameKey||lane.name):tr(STAGES[i]));p.meta.textContent=lane?tr("{0} / {1} 步骤",lane.settled,lane.count):tr("等待任务");p.el.classList.toggle("hg-active",!!(lane&&lane.active));p.el.disabled=!lane||!lane.cells.length;p.el.title=tr("点击查看实时日志");});
494
+ this.monitors.forEach((m,i)=>{const c=this.screenMeta[i],refresh=c&&c.rel?this.cellRefresh(c.rel)||{}:{},state=c?c.status:"idle";m.el.className="hg-monitor st-"+state;m.el.disabled=!c;m.el.dataset.log=c?c.rel:"";m.role.textContent=c?tr(c.role):tr("空闲工位");m.status.textContent=c?tr(STATUS[state]||"完成"):tr("待命");m.tail.textContent=c?refresh.tail||c.displayTail||(state==="running"?tr("等待日志输出…"):state==="queued"?tr("等待执行"):tr("(无输出)")):tr("等待任务");m.time.textContent=c?refresh.elapsed||c.displayElapsed||"":"";m.el.title=c?[tr(c.role),tr(STATUS[state]||"完成"),c.displayAgent,m.tail.textContent,tr("点击查看实时日志")].filter(Boolean).join(" · "):tr("空闲工位");m.el.setAttribute("aria-label",m.el.title);});
495
+ this.prev.setAttribute("aria-label",tr("上一组工位"));this.next.setAttribute("aria-label",tr("下一组工位"));
239
496
  };
240
-
241
- return { create(opts) { return new ReferenceScene(opts); } };
242
- })();
497
+ Scene.prototype.setActive=function(active){this.active=!!active;if(this.timer)clearInterval(this.timer);if(this.raf)cancelAnimationFrame(this.raf);this.raf=0;this.layer.hidden=!this.active;this.canvas.hidden=!this.active||this.displayMode!=="live3d";if(this.active){this.updateOverlay();if(this.displayMode==="live3d")this.render();else if(this.displayMode==="reference")this.layoutOverlayPositions();this.timer=setInterval(()=>{if(!document.hidden&&this.host.offsetParent!==null)this.updateOverlay();},1000);}};
498
+ Scene.prototype.resize=function(){this.cssW=this.host.clientWidth;this.cssH=this.host.clientHeight;if(!this.cssW||!this.cssH)return;if(this.displayMode==="live3d")this.render();else if(this.displayMode==="reference")this.layoutOverlayPositions();};
499
+ Scene.prototype.zoomAt=function(factor){this.zoom=clamp(this.zoom/factor,.72,2.4);this.applyArtboardTransform();this.render();};
500
+ Scene.prototype.resetView=function(){this.zoom=1;this.yaw=0;this.pitch=.42;this.panX=0;this.panY=0;this.applyArtboardTransform();this.render();};
501
+ Scene.prototype.setDisplayMode=function(mode){const next=mode==="live3d"?"live3d":mode==="2d"?"2d":"reference";const changed=next!==this.displayMode;this.displayMode=next;if(changed){this.zoom=1;this.yaw=0;this.pitch=.42;this.panX=0;this.panY=0;}this.canvas.hidden=!this.active||this.displayMode!=="live3d";this.updateOverlay();this.applyArtboardTransform();if(this.displayMode==="live3d")this.render();else if(this.displayMode==="reference")this.layoutOverlayPositions();};
502
+ Scene.prototype.applyArtboardTransform=function(){if(!this.artboard)return;this.artboard.style.transform=this.displayMode==="reference"?"translate3d("+(this.panX*100)+"%,"+(this.panY*100)+"%,0) scale("+this.zoom+")":"none";};
503
+ Scene.prototype.info=function(){return{renderer:"webgl",cells:(this.cells||[]).length,screens:SCREENS.length,lanes:this.model?this.model.lanes.length:0,page:this.page,zoom:this.zoom,cssW:this.cssW,cssH:this.cssH,objects:this.objects?this.objects.length:0,deskRows:this.deskPositions?[this.deskPositions.filter(d=>d.row===0).length,this.deskPositions.filter(d=>d.row===1).length]:[]};};
504
+ Scene.prototype.projectCell=function(rel){const i=this.screenMeta.findIndex(c=>c.rel===rel),desk=this.deskPositions&&this.deskPositions[i];if(i<0||!desk)return null;const p=this.projectWorld(desk.x,desk.row===1?1.38:1.205,desk.z-.418);return p&&p.visible?{x:p.x,y:p.y}:null;};
505
+ Scene.prototype.dispose=function(){this.setActive(false);if(this.resizeObserver)this.resizeObserver.disconnect();if(this.languageObserver)this.languageObserver.disconnect();this.listeners.forEach(fn=>fn());if(this.pager)this.pager.remove();if(this.gl){const gl=this.gl;if(this.batchBuffers)for(const pass of Object.values(this.batchBuffers))for(const key of ["positions","normals","colors"])if(pass&&pass[key])gl.deleteBuffer(pass[key]);for(const o of [this.box,this.roundBox,this.sphere])if(o){if(o.p)gl.deleteBuffer(o.p);if(o.ix)gl.deleteBuffer(o.ix);}if(this.program)gl.deleteProgram(this.program);}if(window.__hive3d===this)delete window.__hive3d;};
506
+ const originalCreate=(opts)=>new Scene(opts);
507
+ // Initialize geometry after the constructor has successfully acquired WebGL.
508
+ const create=(opts)=>{const scene=originalCreate(opts);if(scene.failed||!scene.gl)return null;scene.buildWorld();scene.buildBatches();scene.render();return scene;};
509
+ return {create};
510
+ })();