fix(ground): 측량 레이어로 운곡교 지형 추출 대상 제한
This commit is contained in:
1 parent
38854cbe4c
commit
4ef8412d89
5 files changed
+254
No files matched your search
@@ -12,6 +12,8 @@ import numpy as np
|
|||||||
from ezdxf import recover
|
from ezdxf import recover
|
||||||
from pyproj import Transformer
|
from pyproj import Transformer
|
||||||
from pierzainer_ground.dxf_terrain import _densify,read_source_document
|
from pierzainer_ground.dxf_terrain import _densify,read_source_document
|
||||||
|
from pierzainer_ground.config import GroundConfig
|
||||||
|
from pierzainer_ground.dxf_terrain import read_terrain_dxf
|
||||||
from pierzainer_ground.dtm import DTM
|
from pierzainer_ground.dtm import DTM
|
||||||
|
|
||||||
ROOT=Path(__file__).resolve().parent.parent
|
ROOT=Path(__file__).resolve().parent.parent
|
||||||
@@ -63,6 +65,33 @@ def read_survey_block(source, project, doc=None):
|
|||||||
block=doc.blocks.get(project['terrain_block'])
|
block=doc.blocks.get(project['terrain_block'])
|
||||||
if block is None:
|
if block is None:
|
||||||
raise ValueError(f"측량 블록이 없습니다: {project['terrain_block']}")
|
raise ValueError(f"측량 블록이 없습니다: {project['terrain_block']}")
|
||||||
|
# A plan's modelspace mixes contours with borehole symbols and design
|
||||||
|
# outlines that also carry Z. Prefer the established semantic reader when
|
||||||
|
# this drawing declares known survey layers; retain the block reader for
|
||||||
|
# original survey XREFs with their own layer conventions.
|
||||||
|
cfg=GroundConfig(cad_axes=project.get('cad_axes','YX'),use_profile_points=False,
|
||||||
|
z_min=project.get('z_min',0),z_max=project.get('z_max',200),
|
||||||
|
contour_step=project['contour_step_m'],dedup_xy=project['dedup_m'])
|
||||||
|
allowed=cfg.layers.spot_block+cfg.layers.spot_text+cfg.layers.contour
|
||||||
|
if project.get('layers'):
|
||||||
|
allowed=tuple(layer for layer in allowed if layer in project['layers'])
|
||||||
|
if (block.name.lower()=='*model_space' and project.get('prefer_recognized_layers',True)
|
||||||
|
and any(e.dxf.layer in allowed for e in block)):
|
||||||
|
cfg.allowed_terrain_layers=allowed
|
||||||
|
terrain=read_terrain_dxf(source,cfg,document=doc)
|
||||||
|
a=terrain.points
|
||||||
|
if project.get('exclude_zero'):
|
||||||
|
a=a[np.abs(a[:,2])>1e-6]
|
||||||
|
if len(a)<3:
|
||||||
|
raise ValueError('인식된 측량 레이어에서 실제 표고점·등고선을 3점 이상 읽지 못했습니다.')
|
||||||
|
if project.get('terrain_crs') and project.get('crs'):
|
||||||
|
east,north=Transformer.from_crs(project['terrain_crs'],project['crs'],always_xy=True).transform(a[:,1],a[:,0])
|
||||||
|
a=np.column_stack([north,east,a[:,2]])
|
||||||
|
if not np.isfinite(a).all():
|
||||||
|
raise ValueError('측량 레이어 좌표 변환 결과가 유효하지 않습니다.')
|
||||||
|
sources=terrain.stats['by_source']
|
||||||
|
return a,{'semantic_layer_filter':1,'survey_points':sources['spot'],
|
||||||
|
'spot_text_points':sources['text'],'contour_points':sources['contour']},terrain.stats['contours']
|
||||||
to_model=(Transformer.from_crs(project['terrain_crs'],project['crs'],always_xy=True)
|
to_model=(Transformer.from_crs(project['terrain_crs'],project['crs'],always_xy=True)
|
||||||
if project.get('terrain_crs') and project.get('crs') else None)
|
if project.get('terrain_crs') and project.get('crs') else None)
|
||||||
points=[]; contours=[]; types=Counter()
|
points=[]; contours=[]; types=Counter()
|
||||||
|
|||||||
@@ -44,4 +44,27 @@ class SurveyPolylineTests(unittest.TestCase):
|
|||||||
points,counts,_=read_survey_block(Path('unused.dxf'),self.options(),doc=doc)
|
points,counts,_=read_survey_block(Path('unused.dxf'),self.options(),doc=doc)
|
||||||
self.assertNotIn('rejected_height_outliers',counts);self.assertEqual(points[:,2].max(),84)
|
self.assertNotIn('rejected_height_outliers',counts);self.assertEqual(points[:,2].max(),84)
|
||||||
|
|
||||||
|
def test_known_plan_layers_exclude_near_height_bore_and_base(self):
|
||||||
|
doc=ezdxf.new();msp=doc.modelspace()
|
||||||
|
for y,z in [(0,30),(100,32)]:
|
||||||
|
msp.add_lwpolyline([(0,y),(100,y)],dxfattribs={'layer':'CAA001','elevation':z})
|
||||||
|
doc.blocks.new('SPOT').add_circle((0,0),.1)
|
||||||
|
msp.add_blockref('SPOT',(20,50,31),dxfattribs={'layer':'CA002'})
|
||||||
|
msp.add_text('30.5',dxfattribs={'layer':'CA0021','insert':(80,50)})
|
||||||
|
for layer in ['CV-BORE-SYMB','CZ-BASE','CF-BRDG-NEWC']:
|
||||||
|
msp.add_lwpolyline([(45,45),(55,45),(55,55)],dxfattribs={'layer':layer,'elevation':27.5})
|
||||||
|
points,counts,_=read_survey_block(Path('unused.dxf'),self.options(),doc=doc)
|
||||||
|
self.assertGreaterEqual(points[:,2].min(),30)
|
||||||
|
self.assertEqual(counts['semantic_layer_filter'],1)
|
||||||
|
self.assertEqual(counts['survey_points'],1)
|
||||||
|
self.assertEqual(counts['spot_text_points'],1)
|
||||||
|
self.assertTrue(any(np.array_equal(p,[50,20,31]) for p in points))
|
||||||
|
self.assertTrue(any(np.array_equal(p,[50,80,30.5]) for p in points))
|
||||||
|
|
||||||
|
def test_known_layers_do_not_fall_back_to_symbols_when_empty(self):
|
||||||
|
doc=ezdxf.new();msp=doc.modelspace()
|
||||||
|
msp.add_lwpolyline([(0,0),(10,0)],dxfattribs={'layer':'CAA001','elevation':0})
|
||||||
|
msp.add_lwpolyline([(0,0),(10,0),(10,10)],dxfattribs={'layer':'CV-BORE-SYMB','elevation':28})
|
||||||
|
with self.assertRaises(ValueError):read_survey_block(Path('unused.dxf'),self.options(),doc=doc)
|
||||||
|
|
||||||
if __name__=='__main__':unittest.main()
|
if __name__=='__main__':unittest.main()
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
"""Regenerate the user's two inputs in a separate folder, reusing exact caches."""
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
root=Path(__file__).resolve().parents[1]
|
||||||
|
sys.path.insert(0,str(root/'pierzainer_ground'))
|
||||||
|
from pierzainer_ground.drawing_job import DrawingJob,build_drawing
|
||||||
|
|
||||||
|
old=root/'pierzainer_ground/data/output/app_b614afc0-6829-4aef-96e3-3ff1365ff5db'
|
||||||
|
target=root/'pierzainer_ground/data/output/ungok_corrected_20261110'
|
||||||
|
evidence=root/'.verify/ungok-terrain-fixed-20261110'
|
||||||
|
evidence.mkdir(parents=True,exist_ok=True)
|
||||||
|
settings=json.loads((old/'generation_settings.json').read_text())
|
||||||
|
old_file=old/'ground_model_3d_with_maps.json'
|
||||||
|
sources=[Path(settings['drawing']),Path(settings['piers']),old_file]
|
||||||
|
digest=lambda p:hashlib.sha256(p.read_bytes()).hexdigest()
|
||||||
|
before={str(p):digest(p) for p in sources}
|
||||||
|
if (target/'ground_model_3d_with_maps.json').exists():
|
||||||
|
raise SystemExit('Corrected result already exists; preserve it rather than overwrite.')
|
||||||
|
(target/'cache').mkdir(parents=True,exist_ok=True)
|
||||||
|
for path in (old/'cache').iterdir():
|
||||||
|
if path.name.endswith(('.dxf','.dxf.source.json')) or path.name=='map_layers.json':
|
||||||
|
shutil.copyfile(path,target/'cache'/path.name)
|
||||||
|
settings.update(output=str(target),allow_download=False)
|
||||||
|
log=[]
|
||||||
|
data=build_drawing(DrawingJob(**settings),lambda text:(log.append(text),print(text,flush=True)))
|
||||||
|
original=json.loads(old_file.read_text())
|
||||||
|
def metrics(d):
|
||||||
|
g=d['terrain_grid'];z=np.array([np.nan if v is None else v for v in g['z']]).reshape(g['ny'],g['nx'])
|
||||||
|
edges=np.concatenate([np.abs(np.diff(z,axis=0)).ravel(),np.abs(np.diff(z,axis=1)).ravel()]);edges=edges[np.isfinite(edges)]
|
||||||
|
return {'valid_nodes':int(np.isfinite(z).sum()),'min_el':float(np.nanmin(z)),'max_el':float(np.nanmax(z)),
|
||||||
|
'max_adjacent_dz':float(edges.max()),'edges_over_5m':int((edges>5).sum()),'edges_over_8m':int((edges>8).sum())}
|
||||||
|
checks={
|
||||||
|
'original_inputs_and_previous_result_unchanged':all(digest(p)==before[str(p)] for p in sources),
|
||||||
|
'all_16_centers_have_ground':len(data['piers'])==16 and all(p['ground']['center'] is not None for p in data['piers']),
|
||||||
|
'same_structure_ids_and_coordinates':[(p['id'],p['X'],p['Y']) for p in data['piers']]==[(p['id'],p['X'],p['Y']) for p in original['piers']],
|
||||||
|
'same_grid_layout':all(data['terrain_grid'][k]==original['terrain_grid'][k] for k in ('origin','step','nx','ny')),
|
||||||
|
'semantic_source_filter_used':data['terrain']['source_counts'].get('semantic_layer_filter')==1,
|
||||||
|
'no_adjacent_jumps_over_5m':metrics(data)['edges_over_5m']==0,
|
||||||
|
'both_embedded_maps_retained':data['map_status']['status']=='mapped' and {l['id'] for l in data['visualization']['layers']}=={'satellite','ground'},
|
||||||
|
'original_source_provenance_matches':data['surface_provenance']['sha256']==before[str(sources[0])] and data['surface_provenance']['pier_sha256']==before[str(sources[1])],
|
||||||
|
}
|
||||||
|
report={'result':'PASS' if all(checks.values()) else 'FAIL','checks':checks,'before':metrics(original),'after':metrics(data),'output':str(target/'ground_model_3d_with_maps.json'),'inputs_sha256':before,'terrain':data['terrain'],'log':log,
|
||||||
|
'centers':[{'id':new['id'],'before':prior['ground']['center'],'after':new['ground']['center']} for prior,new in zip(original['piers'],data['piers'])]}
|
||||||
|
(evidence/'report.json').write_text(json.dumps(report,ensure_ascii=False,indent=2))
|
||||||
|
print(json.dumps(report,ensure_ascii=False))
|
||||||
|
if not all(checks.values()):raise SystemExit(1)
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
"""Read-only terrain diagnostics: grid slopes, isolated peaks, and source TIN."""
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
import numpy as np
|
||||||
|
from collections import Counter
|
||||||
|
|
||||||
|
ROOT=Path(__file__).resolve().parents[1]
|
||||||
|
sys.path.insert(0,str(ROOT/'pierzainer_ground'))
|
||||||
|
from pierzainer_ground.survey_terrain import read_survey_block
|
||||||
|
from pierzainer_ground.dxf_terrain import read_source_document, read_terrain_dxf
|
||||||
|
from pierzainer_ground.dtm import DTM
|
||||||
|
from pierzainer_ground.config import GroundConfig
|
||||||
|
|
||||||
|
out=ROOT/'.verify/ungok-terrain-review-20261110'
|
||||||
|
out.mkdir(parents=True,exist_ok=True)
|
||||||
|
folder=ROOT/'pierzainer_ground/data/output/app_b614afc0-6829-4aef-96e3-3ff1365ff5db'
|
||||||
|
data=json.loads((folder/'ground_model_3d_with_maps.json').read_text())
|
||||||
|
settings=json.loads((folder/'generation_settings.json').read_text())
|
||||||
|
g=data['terrain_grid']; z=np.array([np.nan if v is None else v for v in g['z']]).reshape(g['ny'],g['nx'])
|
||||||
|
edges=[]
|
||||||
|
for j in range(g['ny']):
|
||||||
|
for i in range(g['nx']):
|
||||||
|
for dj,di in [(0,1),(1,0)]:
|
||||||
|
if j+dj<g['ny'] and i+di<g['nx'] and np.isfinite(z[j,i]) and np.isfinite(z[j+dj,i+di]):
|
||||||
|
edges.append({'row':j,'col':i,'to_row':j+dj,'to_col':i+di,'z':float(z[j,i]),'to_z':float(z[j+dj,i+di]),'dz':float(abs(z[j+dj,i+di]-z[j,i]))})
|
||||||
|
edges.sort(key=lambda e:-e['dz'])
|
||||||
|
summary={'file':str(folder),'terrain':data['terrain'],'provenance':data.get('surface_provenance'),'grid':{k:v for k,v in g.items() if k!='z'},'valid_nodes':int(np.isfinite(z).sum()),'z_range':[float(np.nanmin(z)),float(np.nanmax(z))],'adjacent_edge_count':len(edges),'edges_over_5m':sum(e['dz']>5 for e in edges),'edges_over_8m':sum(e['dz']>8 for e in edges),'worst_edges':edges[:12]}
|
||||||
|
source=next((folder/'cache').glob('*.dxf'))
|
||||||
|
doc,_=read_source_document(source)
|
||||||
|
cfg=GroundConfig()
|
||||||
|
points,counts,_=read_survey_block(source,{'terrain_block':settings['terrain_block'],'terrain_crs':settings['terrain_crs'],'crs':settings['crs'],'cad_axes':settings['cad_axes'],'contour_step_m':cfg.contour_step,'dedup_m':cfg.dedup_xy,'z_min':settings['z_min'],'z_max':settings['z_max'],'exclude_zero':True,'prefer_recognized_layers':False},doc=doc)
|
||||||
|
lo=np.array(g['origin'])-settings['max_edge'];hi=np.array(g['origin'])+[(g['nx']-1)*g['step'],(g['ny']-1)*g['step']]+settings['max_edge']
|
||||||
|
points=points[np.all((points[:,:2]>=lo)&(points[:,:2]<=hi),axis=1)]
|
||||||
|
dtm=DTM(points,settings['max_edge'])
|
||||||
|
summary['source_counts']=counts
|
||||||
|
summary['raw_height_layers']=dict(Counter(e.dxf.layer for e in doc.blocks.get(settings['terrain_block']) if e.dxftype() in ('POLYLINE','LWPOLYLINE')))
|
||||||
|
# Counterfactual only: use the drawing's two contour layers, without replacing
|
||||||
|
# the generated user model or changing source geometry.
|
||||||
|
contours,contour_counts,_=read_survey_block(source,{'terrain_block':settings['terrain_block'],'terrain_crs':settings['terrain_crs'],'crs':settings['crs'],'cad_axes':settings['cad_axes'],'contour_step_m':cfg.contour_step,'dedup_m':cfg.dedup_xy,'z_min':settings['z_min'],'z_max':settings['z_max'],'exclude_zero':True,'layers':['CAA001','CAA002']},doc=doc)
|
||||||
|
contours=contours[np.all((contours[:,:2]>=lo)&(contours[:,:2]<=hi),axis=1)]
|
||||||
|
clean_dtm=DTM(contours,settings['max_edge'])
|
||||||
|
gx,gy=np.meshgrid(g['origin'][0]+np.arange(g['nx'])*g['step'],g['origin'][1]+np.arange(g['ny'])*g['step'])
|
||||||
|
clean=clean_dtm.z_many(np.column_stack([gx.ravel(),gy.ravel()])).reshape(z.shape)
|
||||||
|
clean_edges=np.concatenate([np.abs(np.diff(clean,axis=0)).ravel(),np.abs(np.diff(clean,axis=1)).ravel()])
|
||||||
|
clean_edges=clean_edges[np.isfinite(clean_edges)]
|
||||||
|
summary['contour_only_diagnostic']={'layers':['CAA001','CAA002'],'counts':contour_counts,'valid_nodes':int(np.isfinite(clean).sum()),'z_range':[float(np.nanmin(clean)),float(np.nanmax(clean))],'max_adjacent_dz':float(clean_edges.max()),'edges_over_5m':int((clean_edges>5).sum()),'edges_over_8m':int((clean_edges>8).sum()),'not_applied':True}
|
||||||
|
typed_cfg=GroundConfig(use_profile_points=False,z_min=settings['z_min'],z_max=settings['z_max'])
|
||||||
|
typed_cfg.allowed_terrain_layers=typed_cfg.layers.spot_block+typed_cfg.layers.spot_text+typed_cfg.layers.contour
|
||||||
|
typed=read_terrain_dxf(source,typed_cfg,document=doc)
|
||||||
|
typed_local=typed.points[np.all((typed.points[:,:2]>=lo)&(typed.points[:,:2]<=hi),axis=1)]
|
||||||
|
typed_dtm=DTM(typed_local,settings['max_edge'])
|
||||||
|
typed_grid=typed_dtm.z_many(np.column_stack([gx.ravel(),gy.ravel()])).reshape(z.shape)
|
||||||
|
typed_edges=np.concatenate([np.abs(np.diff(typed_grid,axis=0)).ravel(),np.abs(np.diff(typed_grid,axis=1)).ravel()])
|
||||||
|
typed_edges=typed_edges[np.isfinite(typed_edges)]
|
||||||
|
summary['typed_layer_diagnostic']={'layers':list(typed_cfg.allowed_terrain_layers),'counts':typed.stats['by_source'],'valid_nodes':int(np.isfinite(typed_grid).sum()),'z_range':[float(np.nanmin(typed_grid)),float(np.nanmax(typed_grid))],'max_adjacent_dz':float(typed_edges.max()),'edges_over_5m':int((typed_edges>5).sum()),'edges_over_8m':int((typed_edges>8).sum()),'covered_piers':sum(typed_dtm.z_at(p['X'],p['Y']) is not None for p in data['piers']),'not_applied':True}
|
||||||
|
summary['pier_diagnostic']=[]
|
||||||
|
for pier in data['piers']:
|
||||||
|
observed=clean_dtm.z_at(pier['X'],pier['Y'])
|
||||||
|
summary['pier_diagnostic'].append({'id':pier['id'],'generated':pier['ground']['center'],'contour_only':observed,'difference':None if observed is None else observed-pier['ground']['center']})
|
||||||
|
for e in summary['worst_edges']:
|
||||||
|
triangles=[]
|
||||||
|
for row,col in [(e['row'],e['col']),(e['to_row'],e['to_col'])]:
|
||||||
|
xy=np.array(g['origin'])+np.array([col,row])*g['step']; k=int(dtm.tri.find_simplex(xy-dtm.origin))
|
||||||
|
triangles.append({'xy':xy.tolist(),'vertices':points[dtm.tri.simplices[k]].tolist() if k>=0 else [],'predicted':dtm.z_at(*xy)})
|
||||||
|
e['source_triangles']=triangles
|
||||||
|
# Raw constant-height CAD entities retained in the selected TIN: look for
|
||||||
|
# closed design outlines vs survey contours near the worst interpolation.
|
||||||
|
raw=[]
|
||||||
|
for entity in doc.blocks.get(settings['terrain_block']):
|
||||||
|
if entity.dxftype() not in ('LWPOLYLINE','POLYLINE'):continue
|
||||||
|
pts=np.array([tuple(p) for p in (entity.vertices_in_wcs() if entity.dxftype()=='LWPOLYLINE' else entity.points_in_wcs())])
|
||||||
|
if len(pts)<2 or np.ptp(pts[:,2])>1e-6:continue
|
||||||
|
raw.append({'handle':entity.dxf.handle,'layer':entity.dxf.layer,'z':float(pts[0,2]),'vertices':len(pts),'closed':bool(entity.closed if entity.dxftype()=='LWPOLYLINE' else entity.is_closed),'xy_min':pts[:,:2].min(0).tolist(),'xy_max':pts[:,:2].max(0).tolist(),'xyz':pts.tolist()})
|
||||||
|
for e in summary['worst_edges'][:6]:
|
||||||
|
for tri in e['source_triangles']:
|
||||||
|
tri['entities']=[]
|
||||||
|
for north,east,height in tri['vertices']:
|
||||||
|
candidates=[]
|
||||||
|
for entity in raw:
|
||||||
|
if abs(entity['z']-height)>1e-3:continue
|
||||||
|
xy=np.array(entity['xyz'])[:,:2]; d=float(np.linalg.norm(xy-[east,north],axis=1).min())
|
||||||
|
candidates.append((d,entity))
|
||||||
|
candidates.sort(key=lambda r:r[0])
|
||||||
|
tri['entities'].append([{'distance_to_vertex':round(d,4),**{k:v for k,v in ent.items() if k!='xyz'}} for d,ent in candidates[:2]])
|
||||||
|
(out/'report.json').write_text(json.dumps(summary,ensure_ascii=False,indent=2))
|
||||||
|
print(json.dumps({k:v for k,v in summary.items() if k not in ('worst_edges','provenance')},ensure_ascii=False))
|
||||||
|
for e in summary['worst_edges'][:3]:print(json.dumps(e,ensure_ascii=False))
|
||||||
|
try:
|
||||||
|
import matplotlib
|
||||||
|
matplotlib.use('Agg')
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
fig,axes=plt.subplots(1,3,figsize=(16,5),constrained_layout=True)
|
||||||
|
extent=[g['origin'][0],g['origin'][0]+(g['nx']-1)*g['step'],g['origin'][1],g['origin'][1]+(g['ny']-1)*g['step']]
|
||||||
|
for ax,values,title in [(axes[0],z,'Generated: all height-bearing layers'),(axes[1],typed_grid,'Diagnostic: survey spots + contour layers')]:
|
||||||
|
im=ax.imshow(values,origin='lower',extent=extent,vmin=20,vmax=47,aspect='auto',cmap='terrain');ax.set_title(title);ax.set_xlabel('Northing (m)');ax.set_ylabel('Easting (m)')
|
||||||
|
fig.colorbar(im,ax=axes[:2],label='EL (m)',shrink=.7)
|
||||||
|
row=summary['worst_edges'][0]['to_row'];axis=g['origin'][0]+np.arange(g['nx'])*g['step']
|
||||||
|
axes[2].plot(axis,z[row],label='Generated');axes[2].plot(axis,typed_grid[row],label='Survey spots + contours');axes[2].set_title(f'Transect E={g["origin"][1]+row*g["step"]:.3f} m');axes[2].set_xlabel('Northing (m)');axes[2].set_ylabel('EL (m)');axes[2].legend();axes[2].grid(alpha=.25)
|
||||||
|
fig.savefig(out/'comparison.png',dpi=150);plt.close(fig)
|
||||||
|
except ImportError:
|
||||||
|
print('Plot dependency unavailable; numeric report preserved.')
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
// Run with Windows Node against isolated Chrome CDP 9444 and Vite 5173.
|
||||||
|
const fs=require('node:fs'),path=require('node:path'),assert=require('node:assert/strict');
|
||||||
|
const root=path.resolve(__dirname,'..'),out=path.join(root,'.verify/ungok-terrain-fixed-20261110/browser');
|
||||||
|
fs.mkdirSync(out,{recursive:true});
|
||||||
|
const report={checks:[],errors:[]};let ws;
|
||||||
|
const delay=ms=>new Promise(r=>setTimeout(r,ms));
|
||||||
|
(async()=>{
|
||||||
|
const tab=await(await fetch('http://127.0.0.1:9444/json/new?about:blank',{method:'PUT'})).json();
|
||||||
|
ws=new WebSocket(tab.webSocketDebuggerUrl);await new Promise((r,j)=>{ws.onopen=r;ws.onerror=j;});
|
||||||
|
let seq=0;const pending=new Map();
|
||||||
|
ws.onmessage=({data})=>{const m=JSON.parse(data),p=pending.get(m.id);if(p){clearTimeout(p.timer);pending.delete(m.id);m.error?p.reject(Error(JSON.stringify(m.error))):p.resolve(m.result);}if(m.method==='Runtime.exceptionThrown')report.errors.push(m.params.exceptionDetails.exception?.description||m.params.exceptionDetails.text);};
|
||||||
|
const send=(method,params={})=>new Promise((resolve,reject)=>{const id=++seq,timer=setTimeout(()=>reject(Error('Timeout '+method)),30000);pending.set(id,{resolve,reject,timer});ws.send(JSON.stringify({id,method,params}));});
|
||||||
|
const ev=async expression=>{const r=await send('Runtime.evaluate',{expression,awaitPromise:true,returnByValue:true});if(r.exceptionDetails)throw Error(r.exceptionDetails.exception?.description||r.exceptionDetails.text);return r.result.value;};
|
||||||
|
const until=async expression=>{for(let i=0;i<150;i++){if(await ev(expression))return;await delay(200);}throw Error('Wait '+expression);};
|
||||||
|
const check=(name,value)=>{assert.ok(value,name);report.checks.push(name);console.log('PASS',name);};
|
||||||
|
const click=expression=>ev(`(()=>{const e=${expression};if(!e||e.disabled)throw Error('Missing target '+${JSON.stringify(expression)});e.click();})()`);
|
||||||
|
const file=async(selector,name)=>{const d=await send('DOM.getDocument'),{nodeId}=await send('DOM.querySelector',{nodeId:d.root.nodeId,selector});await send('DOM.setFileInputFiles',{nodeId,files:[name]});};
|
||||||
|
const shot=async name=>{const s=await send('Page.captureScreenshot',{format:'png'});fs.writeFileSync(path.join(out,name+'.png'),Buffer.from(s.data,'base64'));};
|
||||||
|
await send('Page.enable');await send('Runtime.enable');await send('DOM.enable');
|
||||||
|
await send('Emulation.setDeviceMetricsOverride',{width:1600,height:1000,deviceScaleFactor:1,mobile:false});
|
||||||
|
await send('Page.navigate',{url:'http://127.0.0.1:5173/'});await until(`typeof window.openSampleProject==='function'`);
|
||||||
|
const settings=JSON.parse(fs.readFileSync(path.join(root,'pierzainer_ground/data/output/ungok_corrected_20261110/generation_settings.json')));
|
||||||
|
const workbook='\\\\wsl.localhost\\Ubuntu'+settings.piers.replaceAll('/','\\');
|
||||||
|
await ev('window.openSampleProject()');await file('#file',workbook);
|
||||||
|
await until(`document.getElementById('file-name').textContent.includes('piers.xlsx')`);
|
||||||
|
await ev(`(()=>{const e=document.getElementById('project-name');e.value='운곡교 지반 수정 검증';e.dispatchEvent(new Event('input'));document.getElementById('import-dialog').showModal();})()`);
|
||||||
|
await until(`!document.getElementById('dlg-apply').disabled`);
|
||||||
|
await click(`document.getElementById('dlg-apply')`);await click(`document.getElementById('nav-shape')`);await click(`document.getElementById('project-ground-open')`);
|
||||||
|
check('actual workbook lists all 16 structures',await ev(`document.querySelectorAll('[data-ground-pier]').length===16`));
|
||||||
|
await file('#ground-import',path.join(root,'pierzainer_ground/data/output/ungok_corrected_20261110/ground_model_3d_with_maps.json'));
|
||||||
|
await until(`document.getElementById('project-ground-status').textContent.includes('16/16')`);
|
||||||
|
check('all structures have applicable corrected ground',await ev(`[...document.querySelectorAll('[data-ground-pier]')].every(e=>!e.disabled)`));
|
||||||
|
await click(`document.getElementById('project-ground-select-all')`);
|
||||||
|
check('all 16 structures are selected for application',await ev(`document.querySelectorAll('[data-ground-pier]:checked').length===16`));
|
||||||
|
await click(`document.getElementById('project-ground-apply')`);
|
||||||
|
await click(`document.getElementById('project-ground-regenerate')`);
|
||||||
|
await until(`document.getElementById('project-ground-regenerate').disabled`);
|
||||||
|
check('all changes regenerate successfully',await ev(`document.getElementById('project-ground-regenerate').disabled`));
|
||||||
|
await shot('01-corrected-manager');await click(`document.querySelector('#project-ground-dialog .viewer-head button')`);
|
||||||
|
await delay(1000);await shot('02-corrected-terrain');
|
||||||
|
await send('Browser.setDownloadBehavior',{behavior:'deny'});
|
||||||
|
await ev(`(()=>{window.__ungokSaved=null;const original=URL.createObjectURL;URL.createObjectURL=function(blob){blob.text().then(t=>{try{window.__ungokSaved=JSON.parse(t);}catch{}});return original.call(this,blob);};})()`);
|
||||||
|
await click(`document.getElementById('save-project')`);await until(`document.getElementById('save-dialog').open`);await click(`document.getElementById('sv-save')`);await until(`!!window.__ungokSaved?.groundModel`);
|
||||||
|
const saved=await ev(`window.__ungokSaved`);fs.writeFileSync(path.join(out,'verified.pzr.json'),JSON.stringify(saved));
|
||||||
|
check('saved project retains 16 corrected ground records and two maps',saved.groundModel.piers.length===16&&saved.groundModel.mapLayers.length===2);
|
||||||
|
const expected=JSON.parse(fs.readFileSync(path.join(root,'pierzainer_ground/data/output/ungok_corrected_20261110/ground_model_3d_with_maps.json')));
|
||||||
|
check('saved terrain elevations equal generated JSON',JSON.stringify(saved.groundModel.terrainGrid.z)===JSON.stringify(expected.terrain_grid.z));
|
||||||
|
check('browser runtime has no exceptions',report.errors.length===0);report.result='PASS';
|
||||||
|
})().catch(e=>{report.result='FAIL';report.failure=e.stack;process.exitCode=1;}).finally(()=>{fs.writeFileSync(path.join(out,'report.json'),JSON.stringify(report,null,2));console.log(JSON.stringify(report));ws?.close();});
|
||||||
Reference in new issue
Block a user