feat: add review highlight controls (#6)

This commit is contained in:
dekim2 committed 2026-10-28 09:51:25 +09:00
1 parent 3a1069f08e
commit 3ed6b9def2
1 file changed
+27 -8
+27 -8
View File
@@ -1,14 +1,14 @@
import {vec,multiply,perspective,lookAt,project} from './math.js';
import {buildGeometry,layerOf,physicalPoint,conduitPath} from './geometry.js';
const vertex=`precision highp float;
attribute vec3 aBase;attribute vec3 aOffset;attribute vec3 aNormal;attribute vec3 aColor;attribute float aId;attribute float aKind;attribute float aSlice;
attribute vec3 aBase;attribute vec3 aOffset;attribute vec3 aNormal;attribute vec3 aColor;attribute float aId;attribute float aKind;attribute float aSlice;attribute float aAudit;
uniform mat4 uMatrix;uniform float uHeight;uniform float uSize;uniform float uDpr;uniform float uPointSize;
varying vec3 vNormal;varying vec3 vColor;varying float vId;varying float vKind;varying float vSlice;varying vec3 vOffset;
varying vec3 vNormal;varying vec3 vColor;varying float vId;varying float vKind;varying float vSlice;varying vec3 vOffset;varying float vAudit;
void main(){vec3 p=aBase+aOffset*uSize;p.z*=uHeight;gl_Position=uMatrix*vec4(p,1.0);gl_PointSize=uPointSize*uDpr;
vNormal=normalize(aNormal/vec3(1.0,1.0,uHeight));vColor=aColor;vId=aId;vKind=aKind;vSlice=aSlice;vOffset=aOffset;}`;
vNormal=normalize(aNormal/vec3(1.0,1.0,uHeight));vColor=aColor;vId=aId;vKind=aKind;vSlice=aSlice;vOffset=aOffset;vAudit=aAudit;}`;
const fragment=`precision highp float;
uniform vec3 uGlowColor;uniform float uGlow;uniform float uSelected;uniform float uPicking;uniform float uPoints;uniform float uSection;uniform vec2 uCameraDir;
varying vec3 vNormal;varying vec3 vColor;varying float vId;varying float vKind;varying float vSlice;varying vec3 vOffset;
uniform vec3 uGlowColor;uniform float uGlow;uniform float uSelected;uniform float uPicking;uniform float uPoints;uniform float uSection;uniform vec2 uCameraDir;uniform float uAuditPulse;
varying vec3 vNormal;varying vec3 vColor;varying float vId;varying float vKind;varying float vSlice;varying vec3 vOffset;varying float vAudit;
void main(){
if(uPoints>.5&&length(gl_PointCoord-vec2(.5))>.5)discard;
if(uSection>.5){if(vKind>.5&&vKind<1.5&&dot(vOffset.xy,uCameraDir)>0.0)discard;if(vKind>1.5&&vKind<2.5&&vSlice>.03)discard;}
@@ -17,6 +17,7 @@ void main(){
vec3 color=vColor;if(vKind<3.5&&uPoints<.5)color*=light;
if(abs(vId-uSelected)<.1&&uSelected>.5)color=mix(vec3(.97,.41,.14),vec3(1.0,.78,.3),.32*max(n.z,0.0));
if(uGlow>0.0&&abs(vId-uSelected)>.1)color=mix(color,min(vec3(1.0),uGlowColor*1.25+vec3(.08)),uGlow);
if(uAuditPulse>.5&&vAudit>.5&&abs(vId-uSelected)>.1)color=vec3(1.0,.10,.22);
gl_FragColor=vec4(color,1.0);
}`;
export class NetworkRenderer {
@@ -29,14 +30,30 @@ export class NetworkRenderer {
const gl=this.gl;
const shader=(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;};
const p=gl.createProgram();gl.attachShader(p,shader(gl.VERTEX_SHADER,vertex));gl.attachShader(p,shader(gl.FRAGMENT_SHADER,fragment));gl.linkProgram(p);if(!gl.getProgramParameter(p,gl.LINK_STATUS))throw Error(gl.getProgramInfoLog(p));this.program=p;
this.attributes=Object.fromEntries(['Base','Offset','Normal','Color','Id','Kind','Slice'].map(n=>[n,gl.getAttribLocation(p,'a'+n)]));
this.uniforms=Object.fromEntries(['Matrix','Height','Size','Dpr','PointSize','Selected','Picking','Points','Section','CameraDir','Glow','GlowColor'].map(n=>[n,gl.getUniformLocation(p,'u'+n)]));
this.attributes=Object.fromEntries(['Base','Offset','Normal','Color','Id','Kind','Slice','Audit'].map(n=>[n,gl.getAttribLocation(p,'a'+n)]));
this.uniforms=Object.fromEntries(['Matrix','Height','Size','Dpr','PointSize','Selected','Picking','Points','Section','CameraDir','Glow','GlowColor','AuditPulse'].map(n=>[n,gl.getUniformLocation(p,'u'+n)]));
const reviewIds=new Set(network.issues.filter(i=>['length_mismatch','adverse_slope'].includes(i.code)).flatMap(i=>i.ids));
this.auditPickIds=new Set(facilities.filter(f=>f.type==='conduit'&&f.renderable&&reviewIds.has(f.id)).map(f=>f.pickId));
this.auditEnabled=false;this.auditSteady=false;this.auditPhase=0;this.auditTimer=null;
this.batches={};const geometry=buildGeometry(network,facilities);
for(const [layer,groups] of Object.entries(geometry)){this.batches[layer]={};for(const [kind,batch] of Object.entries(groups)){
const buffer=gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER,buffer);gl.bufferData(gl.ARRAY_BUFFER,new Float32Array(batch.data),gl.STATIC_DRAW);this.batches[layer][kind]={buffer,count:batch.count};
const buffer=gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER,buffer);gl.bufferData(gl.ARRAY_BUFFER,new Float32Array(batch.data),gl.STATIC_DRAW);
const auditBuffer=gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER,auditBuffer);gl.bufferData(gl.ARRAY_BUFFER,Float32Array.from({length:batch.count},(_,i)=>this.auditPickIds.has(batch.data[i*15+12])?1:0),gl.STATIC_DRAW);
this.batches[layer][kind]={buffer,auditBuffer,count:batch.count};
}}
this.picking={fbo:gl.createFramebuffer(),texture:gl.createTexture(),depth:gl.createRenderbuffer()};
this.interactions();this.observer=new ResizeObserver(()=>this.resize());this.observer.observe(canvas);this.resize();this.fit();
document.addEventListener('visibilitychange',()=>this.setAuditHighlight(this.auditEnabled,this.auditSteady));
}
setAuditHighlight(enabled,steady=false){
clearTimeout(this.auditTimer);this.auditTimer=null;
this.auditEnabled=enabled;this.auditSteady=steady;this.auditPhase=enabled?1:0;
if(enabled&&!steady&&!document.hidden){
const started=performance.now();
const tick=()=>{const elapsed=performance.now()-started;this.auditPhase=Math.floor(elapsed/1000)%2===0?1:0;this.request();this.auditTimer=setTimeout(tick,1000-elapsed%1000);};
this.auditTimer=setTimeout(tick,1000);
}
this.request();
}
visible(f){return this.state.layers[layerOf(f)]&&(f.xy!=null||f.renderable);}
set(patch){const oldHeight=this.state.height;Object.assign(this.state,patch);if(patch.height||patch.size){if(this.autoFit)this.fit();else if(patch.height)this.camera.target[2]*=patch.height/oldHeight;}this.request();}
@@ -85,6 +102,7 @@ export class NetworkRenderer {
this.matrix=multiply(perspective(this.width/this.height,Math.max(.03,c.distance/15000),Math.max(30000,c.distance*5)),lookAt(eye,c.target));
gl.bindFramebuffer(gl.FRAMEBUFFER,picking?this.picking.fbo:null);gl.viewport(0,0,this.canvas.width,this.canvas.height);gl.clearColor(0,0,0,0);gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);gl.enable(gl.DEPTH_TEST);gl.depthFunc(gl.LEQUAL);gl.disable(gl.BLEND);gl.disable(gl.DITHER);gl.disable(gl.CULL_FACE);gl.useProgram(this.program);
gl.uniformMatrix4fv(u.Matrix,false,this.matrix);gl.uniform1f(u.Height,this.state.height);gl.uniform1f(u.Size,this.state.size);gl.uniform1f(u.Dpr,this.dpr);gl.uniform1f(u.PointSize,picking?9:5);gl.uniform1f(u.Selected,this.state.selected);gl.uniform1f(u.Picking,picking?1:0);gl.uniform1f(u.Section,this.state.section?1:0);gl.uniform2f(u.CameraDir,Math.cos(c.azimuth),Math.sin(c.azimuth));
gl.uniform1f(u.AuditPulse,picking?0:this.auditPhase);
for(const [layer,groups] of Object.entries(this.batches)){
if(layer==='grid'&&picking)continue;if(layer!=='grid'&&!this.state.layers[layer])continue;
gl.uniform1f(u.Glow,picking?0:(this.glowForLayer?.(layer)||0));
@@ -92,6 +110,7 @@ export class NetworkRenderer {
for(const [kind,b] of Object.entries(groups)){
if(!b.count)continue;gl.bindBuffer(gl.ARRAY_BUFFER,b.buffer);
let offset=0;for(const [name,size] of [['Base',3],['Offset',3],['Normal',3],['Color',3],['Id',1],['Kind',1],['Slice',1]]){const a=this.attributes[name];gl.enableVertexAttribArray(a);gl.vertexAttribPointer(a,size,gl.FLOAT,false,60,offset*4);offset+=size;}
gl.bindBuffer(gl.ARRAY_BUFFER,b.auditBuffer);gl.enableVertexAttribArray(this.attributes.Audit);gl.vertexAttribPointer(this.attributes.Audit,1,gl.FLOAT,false,4,0);
gl.uniform1f(u.Points,kind==='points'?1:0);gl.drawArrays(kind==='mesh'?gl.TRIANGLES:kind==='lines'?gl.LINES:gl.POINTS,0,b.count);
}
}