Improve drawing export and foundation quantity views
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
1 parent
a0ec74b09b
commit
d22c5008f0
72 files changed
+24346
-12
No files matched your search
@@ -0,0 +1,9 @@
|
||||
# DWG 호환성 점검 (2026-11-06)
|
||||
|
||||
현재 번들 LibreDWG dxf2dwg는 R2000/R2004까지만 지원하고 최신 버전은 지원하지 않는다. 기존 구현은 R2004 객체 누락 때문에 R2000을 사용한다. 이번에 버전 표기만 변경하거나 검증되지 않은 최신 버전을 생성하지 않았다.
|
||||
|
||||
최근 출력 원본 source.dxf를 재변환했다. R2000 미지원 MATERIAL 및 ACAD_MATERIAL 사전을 변환용 중간 DXF에서 제외하고, 재질 참조를 초기화했다. 원본 DXF와 프로젝트 데이터는 변경하지 않는다. ezdxf 저장에는 기본 재질이 필요하므로 저장 후 변환 입력에만 적용한다. 색상은 기존 ACI 변환을 유지한다.
|
||||
|
||||
결과: output/compatibility-20261106/general.dwg. 모델 8911객체, 배치 9463객체, 치수 44개 및 객체 지문/레이어/문자스타일/출력설정 일치. 이전 잘못된 MATERIAL placeholder 복구 3건은 0건으로 줄었다. 최대 치수 오차 1.1e-11mm.
|
||||
|
||||
단, 동일 LibreDWG 재읽기 검증이며 실제 사용자 CAD 열기 검증이 아니다. decoder에는 중복 핸들 #144 및 ENDBLK 경고가 남는다. 따라서 이 수정이 열리지 않는 원인을 확정 해결했다고 판정하지 않는다. CAD 제품·버전·오류 메시지에 따라 독립 CAD 읽기 또는 신뢰할 수 있는 최신 DWG 변환기가 필요하다.
|
||||
@@ -121,3 +121,13 @@ npx vite build --outDir 도면작성/.cache/build
|
||||
- A-A는 첫 기둥의 일정한 원형 단면이다. 생략부에 함께 표시하고 개별 단면 보기로도 확인한다. 임의 절단 위치, 복잡한 부재 단면, 모든 원본 상세의 자동 인식은 미구현이다.
|
||||
- 부재 간 완전한 가림 판정, CAD 실제 글꼴을 포함한 완전한 간섭 판정, 다주 교각의 모든 개별 높이 연쇄 치수, 모델 공간과 연동되는 DXF 뷰포트, AutoCAD 실검증은 후속 범위이다.
|
||||
- 대호지교/감북교의 기존 현장타설말뚝 후속 화면 오류와 승천천교의 원본 방위각 누락 등 기존 샘플 제약은 그대로 유지한다. 도면작성 때문에 외부 기능을 수정하지 않았다.
|
||||
|
||||
|
||||
### 아이소메트릭 높이·EL 정보
|
||||
|
||||
표고 EL 설정이 켜져 있으면 아이소메트릭 구조물의 실제 투영점에서 지반고, 말뚝/기초 하단, 교좌면(블럭 상면), 몰탈 상면으로 지시선을 연결한다. 기둥 높이 H는 실제 상·하단 사이 치수선(mm)으로 표시하고 EL은 m 단위다. 방향 변경 시 동일한 투영식을 사용하며 구조물 아래 정보 목록은 제거했다. 현재 각 부재 종류의 첫 부재를 지시하며 없는 부재는 지시선을 생성하지 않는다. 지반고는 입력 지반고가 첫 기둥 수직선과 만나는 높이를 가리키며 지표면 형상을 새로 생성하지 않는다. 치수/EL 표시 설정을 따르고 모델은 변경하지 않는다.
|
||||
|
||||
|
||||
### 기초 좌표·말뚝 주석
|
||||
|
||||
기초 평면에는 현재 직사각형 기초의 실제 네 모서리 X(N)·Y(E)를 m 단위 소수 3자리로 지시한다. 로컬 모델점을 회전·위치변환한 좌표를 사용하며, 참고 도면의 모따기 형상·좌표를 복사하지 않는다. 횡단면도에는 기초 상단의 C-C 절단 위치 지시선이 있고 기초 평면 제목과 연결한다. 기존 A-A 기둥 단면 지시선도 유지한다. 말뚝 생략 경계의 간격을 좁혀 이중 꺾임으로 표시하며 모델 공간과 길이값은 그대로 둔다. 횡단면/측면에는 입력 말뚝 종류(없는 경우 미확인), 본수, 실제 외경, 확인 가능한 두께 및 모델 길이 범위를 지시선 주석으로 표시한다. '내부굴착복합말뚝' 등 미입력 공법명은 추정하지 않는다.
|
||||
+1
-1
@@ -12,7 +12,7 @@ export function modelBreaks(model:PierModel,scale:number): ViewBreak[] {
|
||||
const keepBottom=Math.max(member==='column'?900:350,diameter*.65);
|
||||
const keepTop=Math.max(member==='column'?1300:500,diameter*.9);
|
||||
const from=bottom+keepBottom,to=top-keepTop;
|
||||
const gap=member==='column'?diameter+6*scale:3*scale;
|
||||
const gap=member==='column'?diameter+6*scale:1.5*scale;
|
||||
if(to-from>Math.max(gap+500,scale*10))result.push({from,to,gap,member});
|
||||
}
|
||||
return result.sort((a,b)=>a.from-b.from).filter((b,i,all)=>i===0||b.from>=all[i-1].to);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { foundationNotes } from './foundation-notes';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { readFileSync, readdirSync, mkdirSync, writeFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
@@ -114,6 +115,17 @@ describe('실제 모델 → 일반도',()=>{
|
||||
expect(JSON.stringify(model)).toBe(before);
|
||||
});
|
||||
|
||||
it('기초 좌표와 말뚝 주석은 모델 실좌표·실제 길이를 사용한다',()=>{
|
||||
const m=buildPier(input),f=m.footings[0],bounds={minX:-10000,maxX:10000,minY:0,maxY:5000};
|
||||
const notes=foundationNotes(m,input,'footing',bounds,100);
|
||||
const corner=localPoint(m,f.k,f.s-f.length/2,f.top,-f.width/2);
|
||||
expect(notes.some(e=>e.kind==='text'&&e.text===`X(N) ${(-corner.z+m.pivot.n).toFixed(3)}`)).toBe(true);
|
||||
expect(notes.some(e=>e.kind==='text'&&e.text===`Y(E) ${(corner.x+m.pivot.e).toFixed(3)}`)).toBe(true);
|
||||
expect(notes.filter(e=>e.kind==='text')).toHaveLength(m.footings.length*8);
|
||||
const pileNotes=foundationNotes(m,input,'front',bounds,100);
|
||||
expect(pileNotes.some(e=>e.kind==='text'&&e.text.startsWith('L=')&&e.text.endsWith(' m'))).toBe(true);
|
||||
});
|
||||
|
||||
it('참고 치수표 항목은 모델의 받침 제원·실제 높이를 사용하고 미확인 값을 만들지 않는다',()=>{
|
||||
const m=buildPier(input),rows=bearingSchedule(m);
|
||||
expect(rows).toHaveLength(m.blocks.length);expect(rows[0].A).toBeNull();expect(rows[0].T1).toBeNull();
|
||||
@@ -200,6 +212,13 @@ describe('실제 모델 → 일반도',()=>{
|
||||
|
||||
it('전체 치수는 외측 레인에 배치하고 출력 문자 크기와 실제 점유 범위를 검증한다',()=>{
|
||||
const doc=generateDrawing(buildPier(input),input,project,DEFAULT_SETTINGS),front=doc.views[0];
|
||||
const info=doc.views.find(v=>v.id==='iso')!.entities.filter(e=>e.kind==='text'&&e.source.member==='iso-info');
|
||||
expect(info.some(e=>e.kind==='text'&&e.text==='지반고')).toBe(true);
|
||||
expect(info.some(e=>e.kind==='text'&&e.text===`EL. ${input.groundEl.toFixed(3)}`)).toBe(true);
|
||||
expect(info.some(e=>e.kind==='text'&&e.text==='말뚝 하단')).toBe(true);
|
||||
const iso=doc.views.find(v=>v.id==='iso')!;
|
||||
expect(iso.entities.some(e=>e.kind==='dimension'&&e.label?.startsWith('H='))).toBe(true);
|
||||
expect(iso.entities.some(e=>e.kind==='line'&&e.source.member==='iso-info')).toBe(true);
|
||||
expect(doc.quality?.geometryUnchanged).toBe(true);expect(doc.quality?.after).toBe(0);expect(doc.quality!.minimumTextMm).toBeGreaterThanOrEqual(2.5);
|
||||
const vertical=front.entities.filter((e):e is Extract<typeof e,{kind:'dimension'}>=>e.kind==='dimension'&&e.axis==='y');
|
||||
const overall=vertical.reduce((a,b)=>a.value>b.value?a:b);
|
||||
|
||||
+10
-6
@@ -1,3 +1,4 @@
|
||||
import { foundationNotes } from './foundation-notes';
|
||||
import { sectionCut } from './reference-symbols';
|
||||
import { Vector3 } from 'three';
|
||||
import type { PierInput } from '../src/core/types';
|
||||
@@ -6,7 +7,7 @@ import { entityBounds, localPoint, modelPrimitives, projectModel, projectPoint }
|
||||
import { modelBreaks, shortenedBounds } from './breaks';
|
||||
import { bearingDetail, scheduleEntities } from './bearing-detail';
|
||||
import { templateSlots, defaultSheet, wrappedLines } from './template';
|
||||
import { isometricEntities, keyplanEntities } from './project-views';
|
||||
import { isometricEntities, isometricInfo, keyplanEntities } from './project-views';
|
||||
import { hatchGeometry } from './hatches';
|
||||
import { componentDimensions } from './component-dimensions';
|
||||
import { optimiseLayout } from './quality-layout';
|
||||
@@ -15,7 +16,7 @@ import type { Bounds, DrawingDocument, DrawingSettings, DrawingView, Entity, Poi
|
||||
|
||||
export const DEFAULT_SETTINGS: DrawingSettings = { paper:'A3',scale:'auto',dimensions:true,centers:true,elevations:true,shorten:true,hatches:true,isoDirection:0 };
|
||||
const SCALES=[10,20,25,30,40,50,75,100,150,200,250,300,400,500,750,1000,1500,2000];
|
||||
export const VIEW_TITLES: Record<ViewId,string>={front:'횡단면도',side:'단면도 · 측면',top:'단면도 · 두부보 평면',footing:'단면도 · 기초 평면',section:'A-A 기둥 단면',detail:'상세 A · 받침 및 교좌면',schedule:'받침 및 교좌면 상세 변화 치수표',heights:'받침 높이·표고표',iso:'ISOMETRIC VIEW',keyplan:'KEY PLAN'};
|
||||
export const VIEW_TITLES: Record<ViewId,string>={front:'횡단면도',side:'단면도 · 측면',top:'두부보 평면도',footing:'단면도 C-C · 기초 평면',section:'A-A 기둥 단면',detail:'상세 A · 받침 및 교좌면',schedule:'받침 및 교좌면 상세 변화 치수표',heights:'받침 높이·표고표',iso:'ISOMETRIC VIEW',keyplan:'KEY PLAN'};
|
||||
|
||||
function slots(width: number,height: number): Record<ViewId,Rect> {
|
||||
return templateSlots(width,height);
|
||||
@@ -25,7 +26,7 @@ function recommend(bounds: Bounds,slot: Rect,id: ViewId): number {
|
||||
const need=Math.max((bounds.maxX-bounds.minX)/Math.max(10,slot.width-(side?36:24)),(bounds.maxY-bounds.minY)/Math.max(10,slot.height-(id==='top'?47:31)));
|
||||
return SCALES.find(s=>s>=need)??Math.ceil(need/500)*500;
|
||||
}
|
||||
function annotate(model: PierModel,id: ViewId,b: Bounds,scale: number,settings: DrawingSettings,geometry?:Entity[]): Entity[] {
|
||||
function annotate(model: PierModel,id: ViewId,b: Bounds,scale: number,settings: DrawingSettings,input:PierInput,geometry?:Entity[]): Entity[] {
|
||||
const out: Entity[]=[],source: Source={member:'annotation',index:0,pier:model.name},h=2.3*scale;
|
||||
const line=(a:Point,c:Point,layer:Entity['layer']='CENTER')=>out.push({kind:'line',a,b:c,layer,source});
|
||||
const text=(p:Point,value:string,layer:Entity['layer']='TEXT',align:'left'|'center'='left')=>out.push({kind:'text',point:p,text:value,height:h,layer,source,align});
|
||||
@@ -110,6 +111,8 @@ function annotate(model: PierModel,id: ViewId,b: Bounds,scale: number,settings:
|
||||
out.push({kind:'hatch',points:Array.from({length:16},(_,i)=>[p[0]+.5*scale*Math.cos(i*Math.PI/8),p[1]+.5*scale*Math.sin(i*Math.PI/8)] as Point),pattern:'SOLID',spacing:scale,angle:0,color:'#ff0000',layer:'SECTION',source});
|
||||
text([end[0]+scale,end[1]+scale],`말뚝 Ø${formatMm(pile.diameter*1000)} · ${model.piles.length}본`);
|
||||
}
|
||||
out.push(...foundationNotes(model,input,id,b,scale));
|
||||
if(id==='front'&&model.footings[0]){const y=model.footings[0].top*1000;line([b.minX-3*scale,y],[b.maxX+3*scale,y],'SECTION');out.push(...sectionCut([b.minX-3*scale,y],scale,'C',false,source).filter(e=>e.kind!=='text'),...sectionCut([b.maxX+3*scale,y],scale,'C',true,source).filter(e=>e.kind!=='text'));out.push({kind:'text',point:[b.minX-5*scale,y+2*scale],text:'C',height:2.5*scale,align:'right',layer:'SECTION',source});}
|
||||
if(settings.dimensions)out.push(...componentDimensions(model,id,geometry??projectModel(model,id),scale));
|
||||
return settings.hatches===false?out.filter(e=>e.kind!=='hatch'):out;
|
||||
}
|
||||
@@ -129,7 +132,7 @@ export function generateDrawing(model: PierModel,input: PierInput,project: {name
|
||||
let scale=settings.scale==='auto'?recommend(geometryBounds,slot,id):settings.scale;
|
||||
if(settings.scale==='auto'&&wide&&settings.shorten!==false)for(let i=0;i<3;i++)scale=recommend(shortenedBounds(geometryBounds,modelBreaks(model,scale)),slot,id);
|
||||
const breaks=wide&&settings.shorten!==false?modelBreaks(model,scale):undefined;
|
||||
const entities=[...geometry,...annotate(model,id,geometryBounds,scale,settings)];
|
||||
const entities=[...geometry,...annotate(model,id,geometryBounds,scale,settings,input)];
|
||||
const bounds={minX:geometryBounds.minX-(wide?17:12)*scale,maxX:geometryBounds.maxX+(wide&&settings.elevations?18:10)*scale,
|
||||
minY:geometryBounds.minY-(id==='top'?27:20)*scale,maxY:geometryBounds.maxY+(id==='top'?19:10)*scale};
|
||||
return{id,title:VIEW_TITLES[id],entities,geometryBounds,bounds,scale,slot,breaks};
|
||||
@@ -137,7 +140,7 @@ export function generateDrawing(model: PierModel,input: PierInput,project: {name
|
||||
if(settings.scale==='auto'){
|
||||
const common=Math.max(views[0].scale,views[1].scale);
|
||||
for(const v of views.slice(0,2)){
|
||||
const b=v.geometryBounds;v.entities=[...v.entities.filter(e=>e.source.member!=='annotation'),...annotate(model,v.id,b,common,settings)];
|
||||
const b=v.geometryBounds;v.entities=[...v.entities.filter(e=>e.source.member!=='annotation'),...annotate(model,v.id,b,common,settings,input)];
|
||||
v.scale=common;v.breaks=settings.shorten===false?undefined:modelBreaks(model,common);
|
||||
v.bounds={minX:b.minX-17*common,maxX:b.maxX+(settings.elevations?18:10)*common,minY:b.minY-20*common,maxY:b.maxY+10*common};
|
||||
}
|
||||
@@ -173,8 +176,9 @@ export function generateDrawing(model: PierModel,input: PierInput,project: {name
|
||||
const originals=new Map(views.map(v=>[v.id,v])),geometry=new Map(views.slice(0,5).map(v=>[v.id,v.entities.filter(e=>e.kind!=='hatch'&&e.source.member!=='annotation'&&e.source.member!=='bearing-location')]));
|
||||
optimiseLayout(doc,(id,scale)=>{
|
||||
const original=originals.get(id)!;let entities=original.entities;
|
||||
if(geometry.has(id))entities=[...geometry.get(id)!,...annotate(model,id,original.geometryBounds,scale,settings,geometry.get(id))];
|
||||
if(geometry.has(id))entities=[...geometry.get(id)!,...annotate(model,id,original.geometryBounds,scale,settings,input,geometry.get(id))];
|
||||
else if(id==='detail')entities=bearingDetail(model,scale,settings.detailBearing,settings.dimensions,settings.centers).entities;
|
||||
else if(id==='iso')entities=[...original.entities,...(settings.elevations?isometricInfo(model,input,original.entities,scale,settings.isoDirection??0,settings.dimensions):[])];
|
||||
else if(id==='keyplan')entities=keyplanEntities(models,model,scale,cfg.startLabel,cfg.endLabel);
|
||||
const breaks=['front','side'].includes(id)&&settings.shorten!==false?modelBreaks(model,scale):undefined;
|
||||
if(settings.hatches!==false&&id!=='iso')entities=[...hatchGeometry(entities,scale),...entities];
|
||||
|
||||
@@ -56,6 +56,13 @@ def convert(source,output):
|
||||
original=ezdxf.readfile(source);audit=original.audit()
|
||||
if audit.errors:raise ValueError('Input DXF audit failed')
|
||||
doc=original
|
||||
# R2000 has no MATERIAL objects. LibreDWG used to turn the default material
|
||||
# dictionary into invalid ACDBPLACEHOLDER entries that its own reader repairs.
|
||||
# Remove unsupported metadata before encoding, preserving indexed colours.
|
||||
materials=[e for e in doc.objects if e.dxftype()=='MATERIAL']
|
||||
material_dictionary=doc.rootdict.get('ACAD_MATERIAL')
|
||||
unsupported_handles={e.dxf.handle for e in materials}
|
||||
if material_dictionary:unsupported_handles.add(material_dictionary.dxf.handle)
|
||||
# The bundled writer's R2004 path drops entities. R2000 is used after a full round-trip check.
|
||||
# R2000 uses indexed colours; retain the intended greys with the nearest ACI colour.
|
||||
colour_count=0
|
||||
@@ -69,6 +76,25 @@ def convert(source,output):
|
||||
expected={layout.name:Counter(fingerprint(e) for e in layout) for layout in doc.layouts}
|
||||
expected_blocks={block.name:Counter(fingerprint(e) for e in block) for block in doc.blocks if not block.name.startswith('*')}
|
||||
compatibility_angles(intermediate)
|
||||
# Strip only converter-input metadata after ezdxf has finished serialization;
|
||||
# ezdxf requires its default materials while saving even an R2000 document.
|
||||
lines=intermediate.read_text(encoding='cp949').splitlines()
|
||||
pairs=list(zip(lines[::2],lines[1::2]));groups=[];group=[]
|
||||
for code,value in pairs:
|
||||
if code.strip()=='0' and group:groups.append(group);group=[]
|
||||
group.append((code,value))
|
||||
if group:groups.append(group)
|
||||
cleaned=[]
|
||||
for group in groups:
|
||||
handle=next((v for c,v in group if c.strip()=='5'),None)
|
||||
if handle in unsupported_handles:continue
|
||||
skip=False
|
||||
for i,(code,value) in enumerate(group):
|
||||
if skip:skip=False;continue
|
||||
if code.strip()=='3' and value=='ACAD_MATERIAL':skip=True;continue
|
||||
if code.strip()=='347':value='0'
|
||||
cleaned.extend([code,value])
|
||||
intermediate.write_text('\n'.join(cleaned)+'\n',encoding='cp949')
|
||||
logs=[]
|
||||
for args in [[str(TOOLS/'dxf2dwg.exe'),'--as','r2000','-o',str(output),str(intermediate)],
|
||||
[str(TOOLS/'dwgread.exe'),'-O','DXF','-o',str(output.with_suffix('.roundtrip.dxf')),str(output)]]:
|
||||
@@ -109,6 +135,7 @@ def convert(source,output):
|
||||
object_fingerprints_match=True,source_links_match=True,dimension_measurements_match=True,max_dimension_error_mm=max_error,lineweights_match=True,dimstyles_match=True,paper_plot_settings_match=True,font_styles_match=True,audit_errors=len(audit.errors),
|
||||
decoder_metadata_repairs=[str(e.message) for e in reader_audit.fixes],indexed_colour_conversions=colour_count,
|
||||
native_autocad_open_tested=False)
|
||||
report['unsupported_materials_removed']=len(materials)
|
||||
output.with_suffix('.validation.json').write_text(json.dumps(report,ensure_ascii=False,indent=2),encoding='utf-8')
|
||||
output.with_suffix('.conversion.log').write_text('\n'.join(logs),encoding='utf-8')
|
||||
return report
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import {it,expect} from 'vitest';
|
||||
import {readFileSync,writeFileSync} from 'node:fs';
|
||||
import {buildPier} from '../src/core/geometry';
|
||||
import {generateDrawing,DEFAULT_SETTINGS} from './drawing';
|
||||
it('회전된 다주 기초의 좌표 주석은 겹치지 않는다',()=>{
|
||||
const s=JSON.parse(readFileSync(new URL('../랜딩페이지/assets/samples/sample06.pzr.json',import.meta.url),'utf8'));
|
||||
const p=s.piers[1],m=buildPier(p.input),d=generateDrawing(m,p.input,s.project,DEFAULT_SETTINGS);
|
||||
if(d.quality?.after)writeFileSync(new URL('./.cache/foundation-debug.json',import.meta.url),JSON.stringify(d.quality.issues.map(q=>({q,a:d.views.find(v=>v.id===q.view)?.entities[q.first],b:d.views.find(v=>v.id===q.view)?.entities[q.second??-1]})),null,2));
|
||||
expect(d.quality?.after).toBe(0);
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { PierModel } from '../src/core/geometry';
|
||||
import type { PierInput } from '../src/core/types';
|
||||
import { localPoint,projectPoint } from './projection';
|
||||
import type { Entity,ViewId,Bounds,Point } from './types';
|
||||
|
||||
/** Survey labels refer to actual model corners; X=Northing, Y=Easting. */
|
||||
export function foundationNotes(model:PierModel,input:PierInput,id:ViewId,b:Bounds,scale:number):Entity[]{
|
||||
const out:Entity[]=[],source={pier:model.name,member:'annotation',index:0};
|
||||
const text=(point:Point,text:string,align:'left'|'right'='left')=>out.push({kind:'text',point,text,height:2.5*scale,align,layer:'TEXT',source});
|
||||
const line=(a:Point,b:Point)=>out.push({kind:'line',a,b,layer:'TEXT',source});
|
||||
if(id==='footing')model.footings.forEach((f,index)=>{
|
||||
const corners=[[-1,-1],[-1,1],[1,-1],[1,1]].map(([s,n])=>{
|
||||
const world=localPoint(model,f.k,f.s+s*f.length/2,f.top,n*f.width/2);
|
||||
return {point:projectPoint(model,world,id),e:world.x+model.pivot.e,n:-world.z+model.pivot.n};
|
||||
});
|
||||
corners.sort((a,b)=>b.point[1]-a.point[1]);
|
||||
const ordered=[...corners.slice(0,2).sort((a,b)=>a.point[0]-b.point[0]),...corners.slice(2).sort((a,b)=>a.point[0]-b.point[0])];
|
||||
for(const [i,c] of ordered.entries()){const right=i%2===1,upper=i<2,sign=right?1:-1;
|
||||
const x=(right?b.maxX:b.minX),y=(upper?b.maxY+(8+index*8)*scale:b.minY-(12+index*8)*scale);
|
||||
line(c.point,[x-sign*scale,y]);line([x-sign*scale,y],[x,y]);
|
||||
text([x,y+3.5*scale],`X(N) ${c.n.toFixed(3)}`,right?'right':'left');
|
||||
text([x,y],`Y(E) ${c.e.toFixed(3)}`,right?'right':'left');
|
||||
}
|
||||
});
|
||||
if((id==='front'||id==='side')&&model.piles.length){
|
||||
const pile=model.piles[0],anchor=projectPoint(model,localPoint(model,pile.k,pile.s,pile.top-.2,pile.n),id);
|
||||
const x=b.minX,y=b.minY-25*scale;
|
||||
line(anchor,[x-scale,y]);line([x-scale,y],[x+12*scale,y]);
|
||||
const lengths=model.piles.map(p=>p.top-p.bottom),lo=Math.min(...lengths),hi=Math.max(...lengths);
|
||||
text([x,y+3.5*scale],`${input.pileKind??'종류 미확인'} 말뚝 · ${model.piles.length}본`);
|
||||
text([x,y],`Ø${(pile.diameter*1000).toFixed(1)}${input.pileThicknessMm!=null?' × t'+input.pileThicknessMm:''}`);
|
||||
text([x,y-3.5*scale],`L=${lo.toFixed(3)}${hi-lo>.0005?'~'+hi.toFixed(3):''} m`);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Vector3 } from 'three';
|
||||
import type { PierInput } from '../src/core/types';
|
||||
import { entityBounds } from './projection';
|
||||
import type { PierModel } from '../src/core/geometry';
|
||||
import { modelPrimitives, projectPoint, projectModel } from './projection';
|
||||
import { modelBreaks, shortenedY } from './breaks';
|
||||
@@ -61,3 +63,37 @@ export function keyplanEntities(models:readonly PierModel[],selected:PierModel,s
|
||||
{kind:'polyline',points:[[tip[0]-north[0]*2*scale+north[1]*scale,tip[1]-north[1]*2*scale-north[0]*scale],tip,[tip[0]-north[0]*2*scale-north[1]*scale,tip[1]-north[1]*2*scale+north[0]*scale]],closed:false,layer:'TEXT',source});
|
||||
return entities;
|
||||
}
|
||||
|
||||
/** Actual projected model points anchor all isometric dimensions and EL leaders. */
|
||||
export function isometricInfo(model:PierModel,input:PierInput,geometry:Entity[],scale:number,direction=0,dimensions=true):Entity[]{
|
||||
const bounds=entityBounds(geometry),out:Entity[]=[],primitives=modelPrimitives(model);
|
||||
const datum=Math.min(...model.columns.map(c=>c.bottom),...model.piles.map(c=>c.bottom),...model.footings.map(c=>c.bottom))*1000,theta=direction*Math.PI/2;
|
||||
const project=(p:Vector3):Point=>{const [s,n]=projectPoint(model,p,'top'),a=s*Math.cos(theta)-n*Math.sin(theta),b=s*Math.sin(theta)+n*Math.cos(theta);return [(a-b)*Math.sqrt(3)/2,p.y*1000-datum+(a+b)*.5];};
|
||||
const source={pier:model.name,member:'iso-info',index:0};
|
||||
const text=(p:Point,value:string)=>out.push({kind:'text',point:p,text:value,height:2.5*scale,layer:'ELEVATION',source,align:'left'});
|
||||
const line=(a:Point,b:Point)=>out.push({kind:'line',a,b,layer:'ELEVATION',source});
|
||||
const rows:{point:Point;name:string;el:number}[]=[];
|
||||
const column=primitives.cylinders.find(c=>c.source.member==='column');
|
||||
if(column){
|
||||
const bottom=project(column.center.clone().setY(column.bottom)),top=project(column.center.clone().setY(column.top));
|
||||
if(dimensions)out.push({kind:'dimension',a:bottom,b:top,position:bounds.minX-4*scale,axis:'y',height:2.5*scale,value:(column.top-column.bottom)*1000,label:`H=${((column.top-column.bottom)*1000).toFixed(0)}`,source,layer:'DIMENSION'});
|
||||
if(Number.isFinite(input.groundEl))rows.push({point:project(column.center.clone().setY(input.groundEl)),name:'지반고',el:input.groundEl});
|
||||
}
|
||||
const pile=primitives.cylinders.find(c=>c.source.member==='pile');
|
||||
if(pile)rows.push({point:project(pile.center.clone().setY(pile.bottom)),name:'말뚝 하단',el:pile.bottom});
|
||||
const footing=primitives.solids.find(s=>s.source.member==='footing');
|
||||
if(footing){const points=footing.faces.flat(),el=Math.min(...points.map(p=>p.y)),p=points.find(p=>Math.abs(p.y-el)<1e-7)!;rows.push({point:project(p),name:'기초 하단',el});}
|
||||
for(const [member,name,field] of [['block','교좌면','top'],['pad','몰탈 상면','padTop']] as const){
|
||||
const solid=primitives.solids.find(s=>s.source.member===member);if(!solid)continue;
|
||||
const block=model.blocks[solid.source.index];if(!block)continue;const el=block[field],points=solid.faces.flat(),p=points.reduce((a,b)=>Math.abs(a.y-el)<Math.abs(b.y-el)?a:b);
|
||||
rows.push({point:project(p),name,el});
|
||||
}
|
||||
rows.sort((a,b)=>a.point[1]-b.point[1]);let last=-Infinity;
|
||||
for(const row of rows){
|
||||
const y=Math.max(row.point[1],last+8*scale);last=y;const x=bounds.maxX+3*scale;
|
||||
line(row.point,[x-scale,y]);line([x-scale,y],[x+13*scale,y]);
|
||||
out.push({kind:'polyline',points:[[row.point[0]+scale,row.point[1]+.6*scale],row.point,[row.point[0]+scale,row.point[1]-.6*scale]],closed:false,layer:'ELEVATION',source});
|
||||
text([x,y+3.3*scale],row.name);text([x,y],`EL. ${row.el.toFixed(3)}`);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -102,7 +102,7 @@ export function optimiseLayout(doc:DrawingDocument,factory:Factory):void{
|
||||
const left=Math.max(bs[0].maxX-bs[0].minX,bs[2].maxX-bs[2].minX),right=Math.max(bs[1].maxX-bs[1].minX,bs[3].maxX-bs[3].minX);
|
||||
const top=Math.max(bs[0].maxY,bs[1].maxY)-Math.min(bs[0].minY,bs[1].minY),bottom=Math.max(bs[2].maxY-bs[2].minY,bs[3].maxY-bs[3].minY);
|
||||
const overflow=Math.max(0,left+right+gap-major.width)+Math.max(0,top+bottom+gap+12-major.height);
|
||||
const score=overflow*10000+elevation+plan*.45+Math.abs(elevation-plan)*.1;
|
||||
const score=overflow*10000+vs.reduce((n,v)=>n+collisionCount(v),0)*100000+elevation+plan*.45+Math.abs(elevation-plan)*.1;
|
||||
if(!best||score<best.score)best={views:vs,bs,left,right,top,bottom,score};
|
||||
}
|
||||
// The smallest feasible elevation scale is preferred; larger denominators cannot improve clarity.
|
||||
@@ -124,6 +124,11 @@ export function optimiseLayout(doc:DrawingDocument,factory:Factory):void{
|
||||
const centre=column[0].paperOrigin![0]+(column[0].geometryBounds.minX+column[0].geometryBounds.maxX)/(2*column[0].scale);
|
||||
const v=column[1],b=measure(v);v.paperOrigin![0]=centre-(v.geometryBounds.minX+v.geometryBounds.maxX)/(2*v.scale);v.slot.x=v.paperOrigin![0]+b.minX;
|
||||
}
|
||||
// Coordinate callouts may be asymmetric: retain alignment only within the major drawing zone.
|
||||
for(const v of best.views){const b=measure(v),left=v.paperOrigin![0]+b.minX,right=v.paperOrigin![0]+b.maxX;
|
||||
const shift=right>major.x+major.width?major.x+major.width-right:left<major.x?major.x-left:0;
|
||||
v.paperOrigin![0]+=shift;v.slot.x+=shift;
|
||||
}
|
||||
const table=finish(doc.views.find(v=>v.id==='schedule')!),tb=measure(table),tableHeight=tb.maxY-tb.minY;
|
||||
fit(table,{x:538*k,y:527*ky-tableHeight,width:280*k,height:tableHeight});
|
||||
const small:Rect={x:538*k,y:46*ky,width:126*k,height:Math.min(table.slot.y-9,365*ky)-46*ky};
|
||||
|
||||
Reference in new issue
Block a user