Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b716746579 | ||
|
|
a6ce439f37 | ||
|
|
535de1e38d | ||
|
|
fcb362e4ce | ||
|
|
84e420949c | ||
|
|
1a7311f30d | ||
|
|
9f0ee22096 | ||
|
|
56b1f2cb9f | ||
|
|
9332b1a95c | ||
|
|
324e5e0c77 | ||
|
|
49e8ccbcff | ||
|
|
3ed6b9def2 | ||
|
|
3a1069f08e | ||
|
|
a0d01aa41e | ||
|
|
98f3563b3d | ||
|
|
2e0460d464 |
No files matched your search
@@ -8,12 +8,14 @@ Python 3.10 이상과 WebGL을 지원하는 최신 Chrome 또는 Edge가 필요
|
||||
|
||||
```bash
|
||||
cd /home/user/hwacheon-network-viewer
|
||||
python3 start.py
|
||||
python3 start.py --port 8081
|
||||
```
|
||||
|
||||
브라우저에서 **http://127.0.0.1:8080/** 를 열면 화천 관망도가 바로 표시됩니다. 실행 터미널을 유지하고 종료할 때 `Ctrl+C`를 누릅니다. 포트가 사용 중이면 `python3 start.py --port 8081`로 실행합니다. 다른 위치에 복사한 경우 해당 프로젝트 폴더에서 실행하면 됩니다.
|
||||
위 명령을 이 프로젝트 폴더에서 실행한 뒤, 터미널에 표시된 주소로 접속하세요. 위 예제의 주소는 **http://127.0.0.1:8081/** 이며 화천 관망도 화면이 표시됩니다. 주소를 여는 것만으로 서버가 실행되지는 않습니다. 다른 위치에 복사했다면 해당 프로젝트 폴더에서 실행하세요.
|
||||
|
||||
내부망 공유는 `python3 start.py --lan`으로 실행합니다. WSL 환경에서는 Windows 포트 전달·방화벽 설정이 별도로 필요합니다. 공유 모드에는 로그인이 없습니다. 글꼴·스크립트·데이터는 모두 프로젝트 안에서 읽습니다.
|
||||
이 예제는 다른 서버와의 포트 충돌을 피하기 위해 8081을 사용합니다. `--port`를 생략하면 기본 포트는 8080입니다. 해당 주소에 다른 화면이 보이면 이미 그 포트에서 다른 서버가 실행 중일 수 있으므로, 이 프로젝트의 실행 결과와 포트 번호를 확인하세요. 실행 터미널을 유지하고 종료할 때 `Ctrl+C`를 누릅니다.
|
||||
|
||||
내부망 공유는 `python3 start.py --lan --port 8081`로 실행합니다. WSL 환경에서는 Windows 포트 전달·방화벽 설정이 별도로 필요합니다. 공유 모드에는 로그인이 없습니다. 글꼴·스크립트·데이터는 모두 프로젝트 안에서 읽습니다.
|
||||
|
||||
## 파일 구성
|
||||
|
||||
@@ -50,12 +52,12 @@ python3 -m unittest discover -s tests -p 'test_*.py' -v
|
||||
|
||||
첫 명령의 종료 코드는 0=일치, 1=불일치, 2=입력 오류입니다. ID·수량·좌표·표고·접속·길이·단면 및 배치 제외 사유를 비교합니다. 수리 계산이나 실측 형상의 정확성을 인증하는 검사는 아닙니다.
|
||||
|
||||
화면 검증은 개발용 Playwright를 별도로 설치하고, 다른 터미널에서 `python3 start.py`를 실행한 상태로 수행합니다. 웹사이트 실행 자체에는 Node.js가 필요하지 않습니다.
|
||||
화면 검증은 개발용 Playwright를 별도로 설치하고, 다른 터미널에서 `python3 start.py --port 8081`을 실행한 상태로 수행합니다. 웹사이트 실행 자체에는 Node.js가 필요하지 않습니다.
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npx playwright install chromium
|
||||
npm run test:ui
|
||||
VIEWER_URL=http://127.0.0.1:8081/ npm run test:ui
|
||||
```
|
||||
|
||||
Linux에서 브라우저 실행에 필요한 시스템 패키지가 없으면 Playwright의 설치 안내를 따릅니다. 다른 주소는 `VIEWER_URL`, 설치된 Chrome/Edge는 `PLAYWRIGHT_CHANNEL=chrome` 또는 `msedge` 환경변수로 지정합니다. 결과 JSON과 화면 이미지는 Git에서 제외되는 `test-results/`에 저장합니다. 검사 범위와 실행 결과는 이슈 #1에서 관리합니다.
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hwacheon-network-viewer-validation",
|
||||
"private": true,
|
||||
"scripts": { "test:ui": "node tests/viewer.cjs", "test:runner": "node tests/launch-failure.cjs" },
|
||||
"scripts": { "test:ui": "node tests/viewer.cjs", "test:runner": "node tests/launch-failure.cjs", "test:audit": "node tests/audit-highlight.cjs", "test:layout": "node tests/responsive-ui.cjs" },
|
||||
"devDependencies": { "playwright": "1.62.1" }
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
const {chromium}=require('playwright');
|
||||
const assert=require('node:assert/strict');
|
||||
const fs=require('node:fs/promises');
|
||||
const path=require('node:path');
|
||||
const out=path.resolve(__dirname,'../test-results');
|
||||
(async()=>{
|
||||
const browser=await chromium.launch({headless:true,...(process.env.PLAYWRIGHT_CHANNEL?{channel:process.env.PLAYWRIGHT_CHANNEL}:{})});
|
||||
const page=await browser.newPage({viewport:{width:1440,height:1000}});
|
||||
const errors=[];page.on('pageerror',e=>errors.push(e.message));
|
||||
const report={status:'failed'};
|
||||
await fs.mkdir(out,{recursive:true});
|
||||
const state=()=>page.evaluate(async()=>{
|
||||
const {app}=await import('./app.js');const r=app.renderer;
|
||||
r.draw();const gl=r.gl,b=new Uint8Array(r.canvas.width*r.canvas.height*4);
|
||||
gl.readPixels(0,0,r.canvas.width,r.canvas.height,gl.RGBA,gl.UNSIGNED_BYTE,b);
|
||||
let red=0,hash=0;for(let i=0;i<b.length;i+=4){if(b[i]>220&&b[i+1]<80&&b[i+2]<120)red++;hash=(hash+b[i]*3+b[i+1]*5+b[i+2]*7)>>>0;}
|
||||
return {phase:r.auditPhase,enabled:r.auditEnabled,timer:r.auditTimer!==null,red,hash,
|
||||
targetCount:r.auditPickIds.size,targets:r.facilities.filter(f=>r.auditPickIds.has(f.pickId)).map(f=>f.id)};
|
||||
});
|
||||
try{
|
||||
await page.clock.install({time:new Date('2026-10-28T00:00:00Z')});
|
||||
await page.goto(process.env.VIEWER_URL||'http://127.0.0.1:8081/',{waitUntil:'networkidle'});
|
||||
await page.waitForFunction(()=>document.querySelector('#loading').hidden);
|
||||
await page.clock.pauseAt(new Date('2026-10-28T00:01:00Z'));
|
||||
const before=await state();assert.equal(before.enabled,false);
|
||||
const data=await (await page.request.get(new URL('data/network.json',page.url()).href)).json();
|
||||
const ids=new Set(data.issues.filter(i=>['length_mismatch','adverse_slope'].includes(i.code)).flatMap(i=>i.ids));
|
||||
const expected=data.links.filter(l=>l.type==='conduit'&&l.renderable&&ids.has(l.id)).map(l=>l.id).sort();
|
||||
assert.deepEqual(before.targets.sort(),expected);
|
||||
await page.locator('#audit-highlight').check({force:true});
|
||||
const on=await state();assert.equal(on.phase,1);assert.ok(on.red>before.red+20);
|
||||
await page.screenshot({path:path.join(out,'audit-red.png'),fullPage:true});
|
||||
await page.clock.runFor(1000);
|
||||
const normal=await state();assert.equal(normal.red,before.red);assert.equal(normal.hash,before.hash);
|
||||
await page.clock.runFor(1000);assert.equal((await state()).phase,1);
|
||||
await page.locator('#audit-highlight').uncheck({force:true});const stopped=await state();
|
||||
assert.equal(stopped.timer,false);assert.equal(stopped.hash,before.hash);
|
||||
await page.locator('#audit-highlight').check({force:true});await page.locator('#audit-steady').check({force:true});
|
||||
const fixed=await state();assert.equal(fixed.timer,false);assert.equal(fixed.phase,1);
|
||||
await page.clock.runFor(1150);assert.equal((await state()).phase,1);
|
||||
await page.locator('#show-conduit').uncheck({force:true});assert.equal((await state()).red,0);await page.locator('#show-conduit').check({force:true});
|
||||
const target=expected[0];await page.locator('#search').fill(target);await page.locator('#search-results button').first().click({force:true});
|
||||
assert.ok((await page.locator('#details').textContent()).includes('점검 필요 · 오류 확정 아님'));
|
||||
await page.locator('#audit-unplaced').click({force:true});
|
||||
for(const id of ['OUT_PUMP','C_OUT_PUMP','UHR1-01411'])assert.ok((await page.locator('#audit-list').textContent()).includes(id));
|
||||
await page.locator('#audit-dialog .close-dialog').click({force:true});
|
||||
await page.locator('#reset').click({force:true});assert.equal((await state()).enabled,false);
|
||||
await page.emulateMedia({reducedMotion:'reduce'});await page.locator('#audit-highlight').check({force:true});
|
||||
const reduced=await state();assert.equal(reduced.timer,false);assert.equal(reduced.phase,1);
|
||||
assert.equal(await page.locator('#audit-steady').isDisabled(),true);
|
||||
await page.clock.resume();await page.waitForTimeout(150);
|
||||
await page.locator('#audit-highlight').scrollIntoViewIfNeeded();
|
||||
await page.screenshot({path:path.join(out,'audit-fixed.png'),fullPage:true});
|
||||
assert.deepEqual(errors,[]);Object.assign(report,{status:'passed',target_conduits:expected.length,
|
||||
red_pixels_on:on.red,red_pixels_off:normal.red,original_colors_restored:true,
|
||||
checks:['target IDs','red pixels','1 second phases','exact original pixel restoration','timer cancellation',
|
||||
'steady mode','layer visibility','selection reasons','3 unplaced facilities','reset','reduced motion','no page errors']});
|
||||
}catch(e){report.error=e.stack;process.exitCode=1;await page.screenshot({path:path.join(out,'audit-failure.png'),fullPage:true}).catch(()=>{});}
|
||||
finally{await fs.writeFile(path.join(out,'audit-highlight-validation.json'),JSON.stringify(report,null,2));console.log(JSON.stringify(report,null,2));await browser.close();}
|
||||
})().catch(e=>{console.error(e);process.exitCode=1;});
|
||||
@@ -0,0 +1,52 @@
|
||||
const {chromium}=require('playwright');
|
||||
const assert=require('node:assert/strict');
|
||||
const fs=require('node:fs/promises');
|
||||
const path=require('node:path');
|
||||
const out=path.resolve(__dirname,'../test-results');
|
||||
(async()=>{
|
||||
const browser=await chromium.launch({headless:true,...(process.env.PLAYWRIGHT_CHANNEL?{channel:process.env.PLAYWRIGHT_CHANNEL}:{})});
|
||||
const page=await browser.newPage();const errors=[];page.on('pageerror',e=>errors.push(e.message));
|
||||
const results=[];let failure;
|
||||
await fs.mkdir(out,{recursive:true});
|
||||
try{
|
||||
for(const [width,height] of [[1920,1080],[1366,768],[390,844]]){
|
||||
await page.setViewportSize({width,height});await page.goto(process.env.VIEWER_URL||'http://127.0.0.1:8081/',{waitUntil:'networkidle'});
|
||||
await page.waitForFunction(()=>document.querySelector('#loading').hidden);
|
||||
const bounds=await page.evaluate(()=>({width:innerWidth,scroll:document.documentElement.scrollWidth,
|
||||
mainBottom:document.querySelector('.app-grid').getBoundingClientRect().bottom,
|
||||
footerTop:document.querySelector('.statusbar').getBoundingClientRect().top,
|
||||
canvasWidth:document.querySelector('#scene').getBoundingClientRect().width}));
|
||||
assert.ok(bounds.scroll<=width+1,JSON.stringify(bounds));assert.ok(bounds.mainBottom<=bounds.footerTop+1);
|
||||
assert.ok(bounds.canvasWidth>width*(width<=1000?.9:.4));
|
||||
await page.screenshot({path:path.join(out,`ui9-${width}.png`),fullPage:true});
|
||||
if(width>1000){
|
||||
const original=bounds.canvasWidth;await page.locator('#toggle-controls').click();
|
||||
assert.equal(await page.locator('#controls-panel').isVisible(),false);
|
||||
assert.ok(await page.locator('#scene').evaluate(e=>e.getBoundingClientRect().width)>original);
|
||||
await page.locator('#toggle-controls').click();
|
||||
await page.locator('#toggle-inspector').click();assert.equal(await page.locator('#inspector-panel').isVisible(),false);
|
||||
await page.locator('#toggle-inspector').click();
|
||||
}else{
|
||||
assert.equal(await page.locator('#controls-panel').isVisible(),false);
|
||||
assert.equal(await page.locator('#inspector-panel').isVisible(),false);
|
||||
await page.locator('#toggle-controls').click();assert.equal(await page.locator('#controls-panel').isVisible(),true);
|
||||
await page.locator('#search').fill('UHR1-02901');await page.locator('#search-results button').first().click();
|
||||
assert.equal(await page.locator('#controls-panel').isVisible(),false);
|
||||
assert.equal(await page.locator('#inspector-panel').isVisible(),true);
|
||||
assert.equal(await page.locator('#selected-id').textContent(),'UHR1-02901');
|
||||
await page.screenshot({path:path.join(out,'ui9-mobile-details.png'),fullPage:true});
|
||||
await page.keyboard.press('Escape');assert.equal(await page.locator('#inspector-panel').isVisible(),false);
|
||||
assert.equal(await page.locator('#toggle-inspector').evaluate(e=>e===document.activeElement),true);
|
||||
await page.locator('#toggle-controls').click();await page.locator('#search').fill('__no_result__');
|
||||
assert.equal(await page.locator('#search-results button').count(),0);
|
||||
await page.screenshot({path:path.join(out,'ui9-mobile-controls.png'),fullPage:true});
|
||||
await page.locator('#panel-backdrop').click({position:{x:375,y:100}});
|
||||
assert.equal(await page.locator('#controls-panel').isVisible(),false);
|
||||
}
|
||||
const overflow=await page.evaluate(()=>document.documentElement.scrollWidth>innerWidth+1);assert.equal(overflow,false);
|
||||
results.push({width,height,status:'passed'});
|
||||
}
|
||||
assert.deepEqual(errors,[]);
|
||||
}catch(e){failure=e.stack;process.exitCode=1;await page.screenshot({path:path.join(out,'ui9-failure.png'),fullPage:true}).catch(()=>{});}
|
||||
finally{const report={status:failure?'failed':'passed',viewports:results,...(failure?{error:failure}:{})};await fs.writeFile(path.join(out,'ui9-validation.json'),JSON.stringify(report,null,2));console.log(JSON.stringify(report,null,2));await browser.close();}
|
||||
})().catch(e=>{console.error(e);process.exitCode=1;});
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"status": "passed",
|
||||
"target_conduits": 207,
|
||||
"red_pixels_on": 4457,
|
||||
"red_pixels_off": 0,
|
||||
"original_colors_restored": true,
|
||||
"checks": [
|
||||
"target IDs",
|
||||
"red pixels",
|
||||
"1 second phases",
|
||||
"exact original pixel restoration",
|
||||
"timer cancellation",
|
||||
"steady mode",
|
||||
"layer visibility",
|
||||
"selection reasons",
|
||||
"3 unplaced facilities",
|
||||
"reset",
|
||||
"reduced motion",
|
||||
"no page errors"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"status": "passed",
|
||||
"viewports": [
|
||||
{
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"width": 1366,
|
||||
"height": 768,
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"width": 390,
|
||||
"height": 844,
|
||||
"status": "passed"
|
||||
}
|
||||
],
|
||||
"existing_facility_panel_validation": {
|
||||
"status": "passed",
|
||||
"facilities_checked": 1662,
|
||||
"search_click_samples": [
|
||||
"MH3",
|
||||
"UHR1-0058",
|
||||
"UHR1-02901",
|
||||
"OUT_PUMP",
|
||||
"UHR1-01411",
|
||||
"C_OUT_PUMP"
|
||||
],
|
||||
"checks": [
|
||||
"all facility information panels",
|
||||
"summary counts",
|
||||
"real search/selection clicks",
|
||||
"missing-facility explanations",
|
||||
"unknown ID",
|
||||
"scale reset",
|
||||
"plan view",
|
||||
"layer toggles",
|
||||
"audit dialog",
|
||||
"reset",
|
||||
"no page errors"
|
||||
],
|
||||
"browser": "156.0.4314.42",
|
||||
"limitations": [
|
||||
"Information consistency; screenshot review is separate from dimensional geometry verification."
|
||||
]
|
||||
},
|
||||
"audit_highlight_validation": {
|
||||
"status": "passed",
|
||||
"target_conduits": 207,
|
||||
"red_pixels_on": 4127,
|
||||
"red_pixels_off": 0,
|
||||
"original_colors_restored": true,
|
||||
"checks": [
|
||||
"target IDs",
|
||||
"red pixels",
|
||||
"1 second phases",
|
||||
"exact original pixel restoration",
|
||||
"timer cancellation",
|
||||
"steady mode",
|
||||
"layer visibility",
|
||||
"selection reasons",
|
||||
"3 unplaced facilities",
|
||||
"reset",
|
||||
"reduced motion",
|
||||
"no page errors"
|
||||
]
|
||||
},
|
||||
"visual_review": [
|
||||
"ui9-1366.png",
|
||||
"ui9-390.png",
|
||||
"ui9-mobile-details.png"
|
||||
]
|
||||
}
|
||||
+41
@@ -33,8 +33,11 @@ function drawProfile(f){
|
||||
c.fillStyle='#587455';c.textAlign='left';c.fillText(`관저 ${fmt(z1,3)}`,13,142);c.textAlign='right';c.fillText(`${fmt(z2,3)} m`,227,142);
|
||||
}
|
||||
export function selectFacility(f){
|
||||
if(f&&window.innerWidth<=1000)setPanel('inspector',true);
|
||||
if(!f)return;app.selected=f;app.renderer.set({selected:f.pickId});$('#view-name').textContent=f.id;
|
||||
let html=`<span class="detail-type ${f.type}">${labels[f.type]}</span><h2 id="selected-id">${esc(f.id)}</h2>`;
|
||||
const reviewReasons=app.network.issues.filter(i=>['length_mismatch','adverse_slope','missing_coordinate','unplaced_link','coincident_endpoints'].includes(i.code)&&i.ids.includes(f.id));
|
||||
if(reviewReasons.length)html+=section('점검 필요 · 오류 확정 아님',`<ul class="review-reasons">${reviewReasons.map(i=>`<li>${esc(i.message)}</li>`).join('')}</ul>`);
|
||||
const placed=isPlaced(f),hidden=!app.renderer.state.layers[layerOf(f)];
|
||||
html+=`<button class="focus-button" id="focus-selected" ${placed?'':'disabled'}>${placed?'선택 시설로 이동 ⌖':'좌표·형상 확인 필요 · 이동 불가'}</button>`;
|
||||
if(!placed)html+=`<p class="notice warning">${f.xy===null?'좌표가 없어 위치를 배치하지 않았습니다.':f.id==='UHR1-01411'?'양 끝의 평면 좌표가 같아 관로 형상을 생략했습니다.':'연결 노드 좌표 또는 단면이 없어 형상을 배치하지 않았습니다.'}</p>`;
|
||||
@@ -85,7 +88,38 @@ function updateScales(){
|
||||
const height=Number($('#height-scale').value),size=Number($('#size-scale').value);app.renderer.set({height,size,section:$('#section-mode').checked});
|
||||
$('#height-output').value=`×${height}`;$('#size-output').value=`×${size}`;$('#height-badge').textContent=`높이 ×${height}`;$('#size-badge').textContent=`단면·맨홀 폭 ×${size}`;$('#section-badge').hidden=!$('#section-mode').checked;
|
||||
}
|
||||
function setPanel(panel,open){
|
||||
const narrow=window.innerWidth<=1000;
|
||||
if(narrow&&open)document.body.classList.add(panel==='controls'?'hide-inspector':'hide-controls');
|
||||
document.body.classList.toggle(`hide-${panel}`,!open);
|
||||
for(const name of ['controls','inspector']){
|
||||
const visible=!document.body.classList.contains(`hide-${name}`);
|
||||
$(`#toggle-${name}`).setAttribute('aria-expanded',String(visible));
|
||||
$(`#${name}-panel`).inert=!visible;
|
||||
}
|
||||
$('#panel-backdrop').hidden=!narrow||['controls','inspector'].every(n=>document.body.classList.contains(`hide-${n}`));
|
||||
}
|
||||
function initPanels(){
|
||||
const narrow=matchMedia('(max-width: 1000px)');
|
||||
const layout=()=>{setPanel('controls',!narrow.matches);setPanel('inspector',!narrow.matches);};
|
||||
layout();narrow.addEventListener('change',layout);
|
||||
for(const name of ['controls','inspector'])$(`#toggle-${name}`).onclick=()=>setPanel(name,document.body.classList.contains(`hide-${name}`));
|
||||
$('#panel-backdrop').onclick=()=>{setPanel('controls',false);setPanel('inspector',false);};
|
||||
document.addEventListener('keydown',e=>{if(e.key==='Escape'&&narrow.matches&&!$('dialog[open]')){
|
||||
const wasControls=!document.body.classList.contains('hide-controls');
|
||||
setPanel('controls',false);setPanel('inspector',false);
|
||||
$(`#toggle-${wasControls?'controls':'inspector'}`).focus();
|
||||
}});
|
||||
}
|
||||
function syncAuditHighlight(){
|
||||
const enabled=$('#audit-highlight').checked,reduced=matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
const steady=$('#audit-steady').checked||reduced;
|
||||
$('#audit-steady').disabled=!enabled||reduced;
|
||||
$('#audit-highlight-note').textContent=reduced?'동작 줄이기 설정에 따라 고정 강조합니다. 오류 확정이 아닌 점검 필요 표시입니다.':steady?'고정 빨간색으로 강조합니다. 오류 확정이 아닌 점검 필요 표시입니다.':'빨간색 1초 · 원래 색 1초 반복. 오류 확정이 아닌 점검 필요 표시입니다.';
|
||||
app.renderer.setAuditHighlight(enabled,steady);
|
||||
}
|
||||
function reset(){
|
||||
$('#audit-highlight').checked=false;$('#audit-steady').checked=false;syncAuditHighlight();
|
||||
$('#height-scale').value=3;$('#size-scale').value=3;$('#section-mode').checked=false;$$('[data-layer]').forEach(b=>{b.checked=true;app.renderer.setLayer(b.dataset.layer,true);});updateScales();app.renderer.set({selected:0});app.selected=null;$('#search').value='';updateSearch();clearDetails();$('#view-name').textContent='전체 관망';app.renderer.setView('iso');app.renderer.fit();toast('표시·배율·시점을 초기화했습니다.');
|
||||
}
|
||||
function frame(renderer,error){
|
||||
@@ -104,11 +138,18 @@ function updateAudit(){
|
||||
}
|
||||
function fatal(message){$('#loading').hidden=true;$('#fatal-error').hidden=false;$('#fatal-error').textContent=message;$('#render-status').textContent='실행 확인 필요';}
|
||||
async function main(){
|
||||
initPanels();
|
||||
try{
|
||||
const response=await fetch('data/network.json',{cache:'no-store'});if(!response.ok)throw Error('관망 데이터를 읽을 수 없습니다. web/data/network.json 파일이 포함되어 있는지 확인해 주세요.');
|
||||
app.network=await response.json();const n=app.network,s=n.summary;
|
||||
app.facilities=[...n.nodes,...n.links].map((f,i)=>({...f,pickId:i+1}));
|
||||
app.renderer=new NetworkRenderer($('#scene'),n,app.facilities,selectFacility,frame);
|
||||
const unplaced=app.facilities.filter(f=>!isPlaced(f));
|
||||
$('#audit-highlight-count').textContent=`표시 가능한 점검 관로 ${app.renderer.auditPickIds.size}개`;
|
||||
$('#audit-unplaced').textContent=`위치 표시 불가 ${unplaced.length}개 · 사유 보기 ↗`;
|
||||
$('#audit-unplaced').onclick=()=>openAudit('placement');
|
||||
$('#audit-highlight').onchange=syncAuditHighlight;$('#audit-steady').onchange=syncAuditHighlight;
|
||||
matchMedia('(prefers-reduced-motion: reduce)').addEventListener('change',syncAuditHighlight);syncAuditHighlight();
|
||||
$('#node-count').textContent=s.counts.junction;$('#pipe-count').textContent=s.counts.conduit;
|
||||
for(const [layer,count] of Object.entries({junction:s.counts.junction,conduit:s.counts.conduit,outfall:s.counts.outfall,other:s.counts.storage+s.counts.pump+s.counts.weir}))$(`#layer-${layer}-count`).textContent=count;
|
||||
$('#circle-count').textContent=s.shapes.CIRCULAR;$('#rect-count').textContent=s.shapes.RECT_CLOSED;$('#source-name').textContent=n.meta.sourceName;
|
||||
|
||||
+12
-5
@@ -12,12 +12,13 @@
|
||||
<link rel="stylesheet" href="viewer-readability.css">
|
||||
</head>
|
||||
<body class="network-page">
|
||||
<header class="topbar"><a class="brand" href="./"><span class="brand-icon" aria-hidden="true">⌘</span><span>HWACHEON<small>STORMWATER NETWORK</small></span></a><div class="title-block"><h1>화천군 맨홀 및 우수관로 3D 뷰어</h1><span>입력 제원 기반 관망 탐색</span></div><span class="local-badge"><i></i> LOCAL ONLY</span><nav class="studio-nav" aria-label="화천군 관망"><a href="./" aria-current="page">화천군 관망</a></nav></header>
|
||||
<header class="topbar"><a class="brand" href="./"><span class="brand-icon" aria-hidden="true">⌘</span><span>HWACHEON<small>STORMWATER NETWORK</small></span></a><div class="title-block"><h1>화천군 관망도</h1><span>SWMM 입력 제원 · 3D 검토</span></div><span class="local-badge"><i></i> 입력 자료 기반</span><nav class="panel-actions" aria-label="화면 패널"><button id="toggle-controls" aria-controls="controls-panel" aria-expanded="true">시설·점검</button><button id="toggle-inspector" aria-controls="inspector-panel" aria-expanded="true">시설 제원</button></nav></header>
|
||||
<button id="panel-backdrop" class="panel-backdrop" aria-label="열린 패널 닫기" hidden></button>
|
||||
<main class="app-grid">
|
||||
<aside class="controls" aria-label="관망 제어">
|
||||
<aside class="controls" id="controls-panel" aria-label="관망 제어" tabindex="-1">
|
||||
<div class="section-kicker">NETWORK EXPLORER <span>01</span></div>
|
||||
<h2>관망을 입체로,<br><span>연결을 더 분명하게.</span></h2>
|
||||
<p class="subtle">SWMM 입력 제원을 공간으로 살펴봅니다.<br>형상과 연결 높이를 선택해 확인하세요.</p>
|
||||
<h2>시설 탐색과 점검</h2>
|
||||
<p class="subtle">ID로 찾고, 연결과 제원을 확인하세요.</p>
|
||||
<div class="summary-cards"><div><span>접합부</span><strong id="node-count">—</strong></div><div><span>관로</span><strong id="pipe-count">—</strong></div></div>
|
||||
<section><div class="section-heading"><h3>시설 찾기</h3><span class="small">ID 검색</span></div><label class="search-wrap"><span aria-hidden="true">⌕</span><input id="search" type="search" placeholder="MH3, UHR1-0249…" autocomplete="off" aria-label="시설 ID 검색"></label><div id="search-status" class="small" aria-live="polite"></div><div id="search-results" class="search-results" aria-label="검색 결과"></div></section>
|
||||
<section><div class="section-heading"><h3>시설 표시</h3><button id="show-all" class="text-button">모두 표시</button></div><div class="layer-list">
|
||||
@@ -26,6 +27,12 @@
|
||||
<label><span><i class="legend-dot outfall"></i>방류구 <small id="layer-outfall-count"></small></span><input id="show-outfall" data-layer="outfall" type="checkbox" checked></label>
|
||||
<label><span><i class="legend-dot other"></i>저류·펌프·위어 <small id="layer-other-count"></small></span><input id="show-other" data-layer="other" type="checkbox" checked></label>
|
||||
</div></section>
|
||||
<section class="audit-highlight-controls" aria-label="점검 대상 강조">
|
||||
<label class="toggle-label"><span>점검 대상 강조<small id="audit-highlight-count">점검 목록 확인 중</small></span><input id="audit-highlight" type="checkbox"></label>
|
||||
<label class="audit-steady-label"><input id="audit-steady" type="checkbox"> 고정 강조 (깜빡임 없이)</label>
|
||||
<p class="fine-print" id="audit-highlight-note">빨간색 1초 · 원래 색 1초 반복<br>오류 확정이 아닌 점검 필요 표시입니다.</p>
|
||||
<button id="audit-unplaced" class="text-button">위치 표시 불가 시설 확인</button>
|
||||
</section>
|
||||
<section><div class="section-heading"><h3>높이와 형상</h3><button id="actual-scale" class="text-button">실제 제원 비율</button></div>
|
||||
<label class="range-heading" for="height-scale">표고·높이 배율 <output id="height-output">×3</output></label><input id="height-scale" type="range" min="1" max="10" step="0.5" value="3"><div class="range-ends"><span>×1</span><span>×10</span></div>
|
||||
<label class="range-heading" for="size-scale">관로 단면·맨홀 폭 배율 <output id="size-output">×3</output></label><input id="size-scale" type="range" min="1" max="8" step="0.5" value="3"><div class="range-ends"><span>×1</span><span>×8</span></div>
|
||||
@@ -48,7 +55,7 @@
|
||||
<div class="viewer-footer"><span>드래그 회전 <i>·</i> 휠 확대 <i>·</i> Shift / 우클릭 드래그 이동</span><button id="open-help" class="text-button">조작 안내 ⓘ</button></div>
|
||||
<div class="datum-note">기준 그리드 · 지형 아님</div>
|
||||
</section>
|
||||
<aside class="inspector" aria-label="시설 제원"><div class="section-kicker">FACILITY DETAILS <span>02</span></div><div id="details"></div></aside>
|
||||
<aside class="inspector" id="inspector-panel" aria-label="시설 제원" tabindex="-1"><div class="section-kicker">시설 제원 <span>INSPECTOR</span></div><div id="details"></div></aside>
|
||||
</main>
|
||||
<footer class="statusbar"><span><i class="status-light"></i> <span id="render-status">준비 중</span></span><span id="model-status">단위 · m / XY 단위는 가정</span><span>수위·유량·실제 흐름 미해석</span></footer>
|
||||
<div id="toast" role="status" aria-live="polite"></div>
|
||||
|
||||
+27
-8
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,3 +12,11 @@
|
||||
.legend-panel{background:#18282be6;border-color:#3b514b}.legend-title{color:#c1d6c9}.legend-panel>div{color:#a9c5b6}.legend-panel>div span{color:#91a79b}.legend-panel small{color:#8aa296}.line-key.circular{background:#64bdb9}.line-key.rectangular{background:#d7b47a}
|
||||
.axes>span{color:#839d92}.view-toolbar{background:#152126ed;border-color:#3a4a4d;box-shadow:0 8px 30px #0003}.view-toolbar button{color:#a4bcb0}.view-toolbar button:hover{background:#2d403e}.view-toolbar button[aria-pressed=true]{background:#a7e3cb;color:#1b3a2c}.view-toolbar i{background:#354446}
|
||||
.viewer-footer>span{color:#8aa69b}.viewer .text-button{color:#a5b8b0}.viewer .text-button:hover{color:#a7e3cb}.datum-note{color:#839d92}
|
||||
.audit-highlight-controls .toggle-label{align-items:center;gap:12px}
|
||||
.audit-highlight-controls .toggle-label small{display:block;margin-top:5px}
|
||||
.audit-highlight-controls .audit-steady-label{display:flex;align-items:center;gap:7px;font-size:12px;margin:12px 0;color:#52695d}
|
||||
.audit-highlight-controls input{accent-color:#e83650}
|
||||
.audit-highlight-controls .fine-print{line-height:1.65}
|
||||
.audit-highlight-controls .text-button{font-size:12px;text-align:left;white-space:normal}
|
||||
.review-reasons{padding-left:18px;color:#a6384b;font-size:12px;line-height:1.7}
|
||||
.review-reasons li+li{margin-top:6px}
|
||||
@@ -58,3 +58,112 @@ button, input, select { font-family: inherit; }
|
||||
.network-page .legend-panel small { display: none; }
|
||||
.network-page .legend-panel > div { margin-top: 6px; }
|
||||
}
|
||||
|
||||
/* Workbench layout: keep the model in view; panels scroll independently. */
|
||||
.network-page{--header-height:72px;--footer-height:32px;height:100dvh;overflow:hidden;background:#eef3f1;color:#203b35}
|
||||
.network-page .topbar{height:var(--header-height);min-height:0;display:flex;padding:0 22px;gap:22px;position:relative;z-index:30;box-shadow:0 1px 0 #d9e4df}
|
||||
.network-page .brand{min-width:0;font-size:13px;letter-spacing:1.3px;gap:10px}
|
||||
.network-page .brand-icon{width:36px;height:36px;font-size:26px;border-radius:10px}
|
||||
.network-page .brand small{font-size:8px;letter-spacing:1px;margin-top:2px}
|
||||
.network-page .title-block{padding-left:22px;border-left:1px solid #dce6e0;gap:2px}
|
||||
.network-page .topbar .title-block h1{font-size:18px;font-weight:700;letter-spacing:-.6px}
|
||||
.network-page .title-block>span{font-size:11px;display:block}
|
||||
.network-page .local-badge{font-size:10px;letter-spacing:0;padding:5px 9px}
|
||||
.panel-actions{display:flex;gap:6px;margin-left:0}
|
||||
.panel-actions button{border:1px solid #d9e3dd;background:#fff;border-radius:8px;padding:9px 13px;min-height:38px;font-size:12px;white-space:nowrap;font-weight:600}
|
||||
.panel-actions button[aria-expanded=true]{background:#e9f4ee;color:#216a55;border-color:#bcd9ca}
|
||||
.network-page .app-grid{height:calc(100dvh - var(--header-height) - var(--footer-height));min-height:0;grid-template-columns:280px minmax(0,1fr) 320px;gap:0;display:grid}
|
||||
.network-page.hide-controls .app-grid{grid-template-columns:minmax(0,1fr) 320px}
|
||||
.network-page.hide-inspector .app-grid{grid-template-columns:280px minmax(0,1fr)}
|
||||
.network-page.hide-controls.hide-inspector .app-grid{grid-template-columns:minmax(0,1fr)}
|
||||
.network-page.hide-controls .controls,.network-page.hide-inspector .inspector{display:none}
|
||||
.network-page .controls,.network-page .inspector{min-height:0;min-width:0;max-height:none;overflow:auto;overscroll-behavior:contain;padding:22px 20px;background:#fbfdfb;grid-column:auto;grid-row:auto;scrollbar-gutter:stable}
|
||||
.network-page .controls>.section-kicker{font-size:9px;letter-spacing:1.6px;color:#7d9387}
|
||||
.network-page .controls h2{font-size:21px;line-height:1.3;margin:12px 0 7px;font-weight:700;letter-spacing:-.7px}
|
||||
.network-page .controls>.subtle{font-size:12px;line-height:1.6;margin:0 0 16px}
|
||||
.network-page .summary-cards{border-radius:10px;margin:0 0 16px;background:#f0f6f2}
|
||||
.network-page .summary-cards div{padding:11px 14px}
|
||||
.network-page .summary-cards strong{font-size:25px;line-height:1.2;font-weight:650}
|
||||
.network-page .summary-cards span{font-size:11px;margin-bottom:4px}
|
||||
.network-page .controls section{padding:16px 0}
|
||||
.network-page .controls h3{font-size:13px}
|
||||
.network-page .section-heading{margin-bottom:10px}
|
||||
.network-page .search-wrap{background:#fff;min-height:42px;border-radius:9px}
|
||||
.network-page .search-wrap input{font-size:13px}
|
||||
.network-page .layer-list{display:flex;gap:3px}
|
||||
.network-page .layer-list label{min-height:36px;padding:4px 0;font-size:13px}
|
||||
.network-page .layer-list small{font-size:11px;color:#607d70}
|
||||
.network-page input[type=checkbox]{width:17px;height:17px;flex-shrink:0}
|
||||
.network-page .text-button{font-size:12px;min-height:28px}
|
||||
.network-page .toggle-label{margin-top:0;font-size:13px}
|
||||
.network-page .toggle-label small{font-size:11px;line-height:1.5}
|
||||
.network-page .audit-highlight-controls{position:relative}
|
||||
.network-page .audit-highlight-controls .audit-steady-label{font-size:12px;min-height:32px;margin:6px 0}
|
||||
.network-page .audit-highlight-controls .fine-print{font-size:11px;line-height:1.65;margin:7px 0}
|
||||
.network-page .range-heading{font-size:12px;margin-top:12px}
|
||||
.network-page .range-heading output{font-size:11px}
|
||||
.network-page .fine-print{font-size:11px;line-height:1.7}
|
||||
.network-page .source-label{font-size:10px;line-height:1.7}
|
||||
.network-page .viewer{min-height:0;height:auto;max-height:none;order:0;grid-column:auto;grid-row:auto;border:0}
|
||||
.network-page .viewport-heading{top:20px;left:24px;right:20px}
|
||||
.network-page .viewport-heading .section-kicker{font-size:9px;letter-spacing:1.6px}
|
||||
.network-page .viewport-heading h2{font-size:25px;margin:5px 0;max-width:100%;overflow-wrap:anywhere;line-height:1.2}
|
||||
.network-page .viewport-heading p{font-size:11px}
|
||||
.network-page .view-badge{font-size:10px;padding:5px 9px}
|
||||
.network-page .scale-badges{top:107px;left:24px;gap:5px}
|
||||
.network-page .scale-badges span{font-size:10px;padding:4px 6px}
|
||||
.network-page .coordinate-note{top:141px;left:24px;font-size:10px}
|
||||
.network-page .legend-panel{left:20px;bottom:117px;padding:11px 13px;border-radius:10px;background:#15262be8;max-width:210px}
|
||||
.network-page .legend-title{font-size:10px;margin-bottom:7px}
|
||||
.network-page .legend-panel>div{font-size:11px;margin:6px 0}
|
||||
.network-page .legend-panel small{font-size:10px;display:none}
|
||||
.network-page .axes{bottom:105px;right:10px}
|
||||
.network-page .datum-note{bottom:97px;font-size:9px}
|
||||
.network-page .view-toolbar{bottom:43px;border-radius:12px;gap:2px;padding:5px;max-width:calc(100% - 24px)}
|
||||
.network-page .view-toolbar button{min-height:36px;padding:8px 9px;font-size:12px}
|
||||
.network-page .viewer-footer{bottom:12px;left:20px;right:20px}
|
||||
.network-page .viewer-footer>span{font-size:10px;max-width:none}
|
||||
.network-page .viewer-footer .text-button{font-size:11px;min-height:20px}
|
||||
.network-page .inspector{border-top:0;background:#fff;padding:22px}
|
||||
.network-page .inspector>.section-kicker{margin-bottom:18px;font-size:12px;color:#416b5c}
|
||||
.network-page .inspector>.section-kicker span{font-size:9px;letter-spacing:1.2px}
|
||||
.network-page .inspector h2{font-size:23px;line-height:1.35;margin:10px 0 15px;font-weight:650}
|
||||
.network-page .overview-icon{width:42px;height:42px;border-radius:12px;font-size:22px;margin-bottom:16px}
|
||||
.network-page .inspector-intro{font-size:13px;line-height:1.8}
|
||||
.network-page .overview-list{gap:10px;margin-top:20px}
|
||||
.network-page .overview-list>div{font-size:13px}
|
||||
.network-page .detail-section{padding-top:16px;margin-top:16px}
|
||||
.network-page .detail-section h3{font-size:13px;margin-bottom:10px}
|
||||
.network-page .inspector .properties>div{font-size:12px;padding:7px 0;gap:12px;border-bottom:1px solid #edf2ef}
|
||||
.network-page .properties dt{flex-basis:42%;overflow-wrap:anywhere}
|
||||
.network-page .properties dd{font-weight:550;overflow-wrap:anywhere}
|
||||
.network-page .notice{font-size:11px;padding:10px 12px;line-height:1.8}
|
||||
.network-page .connection-link{font-size:12px;padding:10px;min-width:0}
|
||||
.network-page .connection-link span{min-width:0;overflow-wrap:anywhere}
|
||||
.network-page .connection-link small{font-size:10px}
|
||||
.network-page .focus-button{font-size:12px;min-height:40px;margin:8px 0 12px}
|
||||
.network-page .statusbar{height:var(--footer-height);min-height:0;font-size:10px;padding:0 18px;overflow:hidden;white-space:nowrap}
|
||||
.network-page #toast{bottom:85px;font-size:12px;z-index:60}
|
||||
.panel-backdrop{position:fixed;inset:var(--header-height) 0 var(--footer-height);background:#11282066;backdrop-filter:blur(2px);border:0;z-index:35;cursor:default}
|
||||
@media(min-width:1700px){.network-page .app-grid{grid-template-columns:300px minmax(0,1fr) 350px}.network-page.hide-controls .app-grid{grid-template-columns:minmax(0,1fr) 350px}.network-page.hide-inspector .app-grid{grid-template-columns:300px minmax(0,1fr)}.network-page.hide-controls.hide-inspector .app-grid{grid-template-columns:minmax(0,1fr)}}
|
||||
@media(max-width:1200px) and (min-width:1001px){.network-page .app-grid{grid-template-columns:250px minmax(0,1fr) 280px}.network-page.hide-controls .app-grid{grid-template-columns:minmax(0,1fr) 280px}.network-page.hide-inspector .app-grid{grid-template-columns:250px minmax(0,1fr)}.network-page.hide-controls.hide-inspector .app-grid{grid-template-columns:minmax(0,1fr)}.network-page .controls,.network-page .inspector{padding:18px 16px}.network-page .title-block{display:none}.network-page .viewer-footer>span{display:none}}
|
||||
@media(max-width:1000px){
|
||||
.network-page .app-grid,.network-page.hide-controls .app-grid,.network-page.hide-inspector .app-grid,.network-page.hide-controls.hide-inspector .app-grid{display:grid;grid-template-columns:minmax(0,1fr);height:calc(100dvh - var(--header-height) - var(--footer-height));min-height:0}
|
||||
.network-page .viewer{height:100%;min-height:0;grid-column:1;grid-row:1}
|
||||
.network-page .controls,.network-page .inspector{position:fixed;top:var(--header-height);bottom:var(--footer-height);width:min(360px,calc(100vw - 32px));z-index:40;height:auto;box-shadow:0 10px 40px #0e252b33;border:0;border-radius:0}
|
||||
.network-page .controls{left:0}.network-page .inspector{right:0}
|
||||
.network-page .title-block{padding-left:15px}.network-page .statusbar>span:nth-child(2){display:none}
|
||||
}
|
||||
@media(max-width:650px){
|
||||
.network-page{--header-height:64px;--footer-height:28px}
|
||||
.network-page .topbar{padding:0 12px;gap:8px;display:flex;height:64px;min-height:0}
|
||||
.network-page .brand{gap:7px;font-size:10px;letter-spacing:.5px}.network-page .brand-icon{width:29px;height:29px;font-size:21px;border-radius:8px}.network-page .brand small{font-size:6px;letter-spacing:.4px}
|
||||
.network-page .title-block,.network-page .local-badge{display:none}.panel-actions{margin-left:auto;gap:5px}.panel-actions button{font-size:11px;padding:8px;min-height:36px}
|
||||
.network-page .viewport-heading{left:16px;right:16px;top:20px}.network-page .viewport-heading h2{font-size:23px;max-width:245px}.network-page .viewport-heading p{font-size:10px}.network-page .viewport-heading .section-kicker{font-size:8px;letter-spacing:1px}
|
||||
.network-page .scale-badges{left:16px;top:108px}.network-page .coordinate-note{left:16px;top:142px;font-size:9px}.network-page .legend-panel{left:14px;bottom:123px;max-width:185px;padding:9px 11px}.network-page .legend-panel>div{font-size:10px}
|
||||
.network-page .axes{right:0;bottom:110px}.network-page .datum-note{right:15px;bottom:100px;font-size:8px}
|
||||
.network-page .view-toolbar{bottom:45px;gap:0;padding:4px;max-width:calc(100% - 20px)}.network-page .view-toolbar button{font-size:11px;padding:8px 7px;min-height:36px}.network-page .view-toolbar i{margin:0 1px}
|
||||
.network-page .viewer-footer{left:15px;right:15px;bottom:12px}.network-page .viewer-footer>span{display:none}.network-page .viewer-footer .text-button{margin-left:auto}
|
||||
.network-page .statusbar{font-size:9px;padding:0 12px;flex-wrap:nowrap}.network-page .statusbar>span:last-child{display:none}
|
||||
.network-page dialog{padding:18px;max-height:85dvh;overflow:auto}.network-page .audit-filter-label{align-items:flex-start;gap:10px;flex-direction:column}.network-page .audit-filter-label select{width:100%;max-width:100%}.network-page .audit-summary-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.network-page .audit-summary-grid div{min-width:0;overflow-wrap:anywhere}.network-page .audit-row{gap:8px;font-size:12px}.network-page .audit-row>span{min-width:0;overflow-wrap:anywhere}
|
||||
}
|
||||
Reference in new issue
Block a user