Add drawing workspace feature
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
1 parent
ad73ddec2b
commit
a5cc458252
78 files changed
+35740
-6
No files matched your search
+4
-1
@@ -18,7 +18,8 @@
|
||||
<button id="nav-shape" class="step current" type="button" aria-current="step">① 형상 생성 <em id="step1-status">미생성</em></button>
|
||||
<button id="nav-plan" class="step" type="button">② 형상계획 <em id="step2-status">예정</em></button>
|
||||
<button id="nav-rebar" class="step" type="button">③ 철근배근 <em id="step3-status">예정</em></button>
|
||||
<button id="nav-quantity" class="step" type="button">④ 재료표 <em id="step4-status">예정</em></button>
|
||||
<button id="nav-drawing" class="step" type="button">④ 도면작성 <em id="step-drawing-status">형상 필요</em></button>
|
||||
<button id="nav-quantity" class="step" type="button">⑤ 재료표 <em id="step4-status">예정</em></button>
|
||||
</nav>
|
||||
<div class="actions">
|
||||
<span id="save-status" class="savest">저장 전</span>
|
||||
@@ -226,6 +227,8 @@
|
||||
</aside>
|
||||
</main>
|
||||
|
||||
<main id="view-drawing" hidden aria-label="교각 일반도 작성"></main>
|
||||
|
||||
<main class="grid quantity" id="view-quantity" hidden>
|
||||
<aside class="panel piers" aria-labelledby="quantity-pier-title">
|
||||
<p id="quantity-pier-title" class="lbl">교각 목록</p>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
{"schemaVersion":1,"project":{"name":"샘플 01 · 산외교","bridge":"산외교","client":"","author":"","crs":"EPSG:5187","memo":"도면추출 연동 샘플 + 원도면 지반. 설계 지반고 유지. 지도 CRS는 원자료 확인이 필요한 가정(inferred_location_checked)."},"source":{"fileName":"샘플01_산외교_교각입력데이터.xlsx","sheets":["Pier","IP","Bearing"]},"piers":[{"id":"P1","input":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"T형","copingTopEl":71.0632,"footingBottomEl":42.763,"groundEl":52.099,"foundationType":"직접기초","pileLengthMm":0,"copingWidthMm":4000,"copingLengthMm":12500,"columnCount":1,"ips":[{"name":"IP1","x":180993.7987,"y":324479.775,"angleDeg":0},{"name":"IP2","x":180993.0547,"y":324478.3023,"angleDeg":0},{"name":"IP3","x":180988.2522,"y":324468.7966,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":180991.8227,"y":324477.5242,"z":71.4178,"azimuthDeg":296.79913888888893,"row":"전열"},{"no":2,"x":180988.9134,"y":324471.7116,"z":71.2882,"azimuthDeg":296.7994166666667,"row":"전열"},{"no":3,"x":180993.2484,"y":324476.8038,"z":71.4401,"azimuthDeg":296.81624999999997,"row":"후열"},{"no":4,"x":180990.2925,"y":324471.0147,"z":71.3096,"azimuthDeg":296.81597222222223,"row":"후열"}],"bearingAzimuthFrame":"model-xy"},"defaults":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"T형","copingTopEl":71.0632,"footingBottomEl":42.763,"groundEl":52.099,"foundationType":"직접기초","pileLengthMm":0,"copingWidthMm":4000,"copingLengthMm":12500,"columnCount":1,"ips":[{"name":"IP1","x":180993.7987,"y":324479.775,"angleDeg":0},{"name":"IP2","x":180993.0547,"y":324478.3023,"angleDeg":0},{"name":"IP3","x":180988.2522,"y":324468.7966,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":180991.8227,"y":324477.5242,"z":71.4178,"azimuthDeg":296.79913888888893,"row":"전열"},{"no":2,"x":180988.9134,"y":324471.7116,"z":71.2882,"azimuthDeg":296.7994166666667,"row":"전열"},{"no":3,"x":180993.2484,"y":324476.8038,"z":71.4401,"azimuthDeg":296.81624999999997,"row":"후열"},{"no":4,"x":180990.2925,"y":324471.0147,"z":71.3096,"azimuthDeg":296.81597222222223,"row":"후열"}],"bearingAzimuthFrame":"model-xy"},"draft":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"T형","copingTopEl":71.0632,"footingBottomEl":42.763,"groundEl":52.099,"foundationType":"직접기초","pileLengthMm":0,"copingWidthMm":4000,"copingLengthMm":12500,"columnCount":1,"ips":[{"name":"IP1","x":180993.7987,"y":324479.775,"angleDeg":0},{"name":"IP2","x":180993.0547,"y":324478.3023,"angleDeg":0},{"name":"IP3","x":180988.2522,"y":324468.7966,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":180991.8227,"y":324477.5242,"z":71.4178,"azimuthDeg":296.79913888888893,"row":"전열"},{"no":2,"x":180988.9134,"y":324471.7116,"z":71.2882,"azimuthDeg":296.7994166666667,"row":"전열"},{"no":3,"x":180993.2484,"y":324476.8038,"z":71.4401,"azimuthDeg":296.81624999999997,"row":"후열"},{"no":4,"x":180990.2925,"y":324471.0147,"z":71.3096,"azimuthDeg":296.81597222222223,"row":"후열"}],"bearingAzimuthFrame":"model-xy"}},{"id":"P2","input":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P2","sheet":"Pier","type":"T형","copingTopEl":71.9164,"footingBottomEl":38.916,"groundEl":41.319,"foundationType":"말뚝기초","pileLengthMm":7000,"copingWidthMm":4000,"copingLengthMm":12500,"columnCount":1,"ips":[{"name":"IP1","x":181033.8891,"y":324459.3055,"angleDeg":0},{"name":"IP2","x":181033.1327,"y":324457.8391,"angleDeg":0},{"name":"IP3","x":181028.2505,"y":324448.3741,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":181031.8943,"y":324457.0714,"z":72.2629,"azimuthDeg":297.2810833333333,"row":"전열"},{"no":2,"x":181028.9384,"y":324451.2823,"z":72.1334,"azimuthDeg":297.2813611111111,"row":"전열"},{"no":3,"x":181033.3136,"y":324456.3392,"z":72.2932,"azimuthDeg":297.29819444444445,"row":"후열"},{"no":4,"x":181030.3037,"y":324450.578,"z":72.1626,"azimuthDeg":297.29783333333336,Line truncated
|
||||
@@ -0,0 +1 @@
|
||||
{"schemaVersion":1,"project":{"name":"샘플 02 · 운곡교","bridge":"운곡교","client":"","author":"","crs":"EPSG:5186","memo":"도면추출 연동 샘플 + 원도면 지반. 설계 지반고 유지. 지도 CRS는 원자료 확인이 필요한 가정(project_default_not_source_verified)."},"source":{"fileName":"샘플02_운곡교_교각입력데이터.xlsx","sheets":["Pier","IP","Bearing"]},"piers":[{"id":"P1","input":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"T형","copingTopEl":43.411,"footingBottomEl":26.208,"groundEl":28.931,"foundationType":"말뚝기초","pileLengthMm":24400,"copingWidthMm":3500,"copingLengthMm":12500,"columnCount":1,"ips":[{"name":"IP1","x":179132.091213,"y":259866.694501,"angleDeg":0},{"name":"IP2","x":179144.095968,"y":259864.658395,"angleDeg":0},{"name":"IP3","x":179144.086088,"y":259864.66007,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":179132.9863,"y":259865.8348,"z":43.625,"azimuthDeg":14.051138888888886,"row":"전열"},{"no":2,"x":179135.4565,"y":259865.4158,"z":43.697,"azimuthDeg":14.046083333333343,"row":"전열"},{"no":3,"x":179137.9267,"y":259864.9969,"z":43.768,"azimuthDeg":14.041055555555545,"row":"전열"},{"no":4,"x":179140.3968,"y":259864.578,"z":43.84,"azimuthDeg":14.036000000000001,"row":"전열"},{"no":5,"x":179142.867,"y":259864.1591,"z":43.912,"azimuthDeg":14.031000000000006,"row":"전열"},{"no":6,"x":179133.2876,"y":259867.0998,"z":43.6,"azimuthDeg":12.74124999999998,"row":"후열"},{"no":7,"x":179135.7577,"y":259866.6809,"z":43.672,"azimuthDeg":12.737694444444458,"row":"후열"},{"no":8,"x":179138.2278,"y":259866.262,"z":43.744,"azimuthDeg":12.734138888888879,"row":"후열"},{"no":9,"x":179140.6978,"y":259865.8431,"z":43.816,"azimuthDeg":12.730611111111102,"row":"후열"},{"no":10,"x":179143.1679,"y":259865.4242,"z":43.887,"azimuthDeg":12.72705555555558,"row":"후열"}],"bearingAzimuthFrame":"model-xy"},"defaults":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"T형","copingTopEl":43.411,"footingBottomEl":26.208,"groundEl":28.931,"foundationType":"말뚝기초","pileLengthMm":24400,"copingWidthMm":3500,"copingLengthMm":12500,"columnCount":1,"ips":[{"name":"IP1","x":179132.091213,"y":259866.694501,"angleDeg":0},{"name":"IP2","x":179144.095968,"y":259864.658395,"angleDeg":0},{"name":"IP3","x":179144.086088,"y":259864.66007,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":179132.9863,"y":259865.8348,"z":43.625,"azimuthDeg":14.051138888888886,"row":"전열"},{"no":2,"x":179135.4565,"y":259865.4158,"z":43.697,"azimuthDeg":14.046083333333343,"row":"전열"},{"no":3,"x":179137.9267,"y":259864.9969,"z":43.768,"azimuthDeg":14.041055555555545,"row":"전열"},{"no":4,"x":179140.3968,"y":259864.578,"z":43.84,"azimuthDeg":14.036000000000001,"row":"전열"},{"no":5,"x":179142.867,"y":259864.1591,"z":43.912,"azimuthDeg":14.031000000000006,"row":"전열"},{"no":6,"x":179133.2876,"y":259867.0998,"z":43.6,"azimuthDeg":12.74124999999998,"row":"후열"},{"no":7,"x":179135.7577,"y":259866.6809,"z":43.672,"azimuthDeg":12.737694444444458,"row":"후열"},{"no":8,"x":179138.2278,"y":259866.262,"z":43.744,"azimuthDeg":12.734138888888879,"row":"후열"},{"no":9,"x":179140.6978,"y":259865.8431,"z":43.816,"azimuthDeg":12.730611111111102,"row":"후열"},{"no":10,"x":179143.1679,"y":259865.4242,"z":43.887,"azimuthDeg":12.72705555555558,"row":"후열"}],"bearingAzimuthFrame":"model-xy"},"draft":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"T형","copingTopEl":43.411,"footingBottomEl":26.208,"groundEl":28.931,"foundationType":"말뚝기초","pileLengthMm":24400,"copingWidthMm":3500,"copingLengthMm":12500,"columnCount":1,"ips":[{"name":"IP1","x":179132.091213,"y":259866.694501,"angleDeg":0},{"name":"IP2","x":179144.095968,"y":259864.658395,"angleDeg":0},{"name":"IP3","x":179144.086088,"y":259864.66007,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":179132.9863,"y":259865.8348,"z":43.625,"azimuthDeg":14.051138888888886,"row":"전열"},{"no":2,"x":179135.4565,"y":259865.4158,"z":43.697,"azimuthDeg":14.046083333333343,"row":"전열"},{"no":3,"x":179137.9267,"y":259864.9969,"z":43.768,"azimuthDeg":14.041055555555545,"row":"전열"},{"no":4,"x":179140.3968,"y":25986Line truncated
|
||||
@@ -0,0 +1 @@
|
||||
{"schemaVersion":1,"project":{"name":"샘플 03 · 대호지교","bridge":"대호지교","client":"","author":"","crs":"EPSG:5186","memo":"도면추출 연동 샘플 + 원도면 지반. 설계 지반고 유지. 지도 CRS는 원자료 확인이 필요한 가정(project_default_not_source_verified)."},"source":{"fileName":"샘플03_대호지교_교각입력데이터.xlsx","sheets":["Pier","IP","Bearing"]},"piers":[{"id":"P1","input":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"2주형","copingTopEl":7.738,"footingBottomEl":1.4,"groundEl":-0.342,"foundationType":"현장타설말뚝","pileLengthMm":29802,"copingWidthMm":3500,"copingLengthMm":24500,"columnCount":2,"ips":[{"name":"IP1","x":153837.294388,"y":480726.410803,"angleDeg":0},{"name":"IP2","x":153833.31775,"y":480714.93,"angleDeg":0},{"name":"IP3","x":153829.341111,"y":480703.449196,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":153836.266,"y":480725.3912,"z":7.8178,"azimuthDeg":109.10472222222222,"row":"전열"},{"no":2,"x":153835.2514,"y":480722.462,"z":7.8798,"azimuthDeg":109.10472222222222,"row":"전열"},{"no":3,"x":153834.2368,"y":480719.5327,"z":7.9418,"azimuthDeg":109.10472222222222,"row":"전열"},{"no":4,"x":153833.2222,"y":480716.6034,"z":8.0038,"azimuthDeg":109.10472222222222,"row":"전열"},{"no":5,"x":153832.2076,"y":480713.6742,"z":8.0038,"azimuthDeg":109.10472222222222,"row":"전열"},{"no":6,"x":153831.193,"y":480710.7449,"z":7.9418,"azimuthDeg":109.10472222222222,"row":"전열"},{"no":7,"x":153830.1783,"y":480707.8157,"z":7.8798,"azimuthDeg":109.10472222222222,"row":"전열"},{"no":8,"x":153829.1637,"y":480704.8864,"z":7.8178,"azimuthDeg":109.10472222222222,"row":"전열"},{"no":9,"x":153837.4944,"y":480724.9657,"z":7.8243,"azimuthDeg":109.10472222222222,"row":"후열"},{"no":10,"x":153836.4798,"y":480722.0365,"z":7.8863,"azimuthDeg":109.10472222222222,"row":"후열"},{"no":11,"x":153835.4652,"y":480719.1072,"z":7.9483,"azimuthDeg":109.10472222222222,"row":"후열"},{"no":12,"x":153834.4506,"y":480716.178,"z":8.0103,"azimuthDeg":109.10472222222222,"row":"후열"},{"no":13,"x":153833.436,"y":480713.2487,"z":8.0103,"azimuthDeg":109.10472222222222,"row":"후열"},{"no":14,"x":153832.4213,"y":480710.3195,"z":7.9483,"azimuthDeg":109.10472222222222,"row":"후열"},{"no":15,"x":153831.4067,"y":480707.3902,"z":7.8863,"azimuthDeg":109.10472222222222,"row":"후열"},{"no":16,"x":153830.3921,"y":480704.4609,"z":7.8243,"azimuthDeg":109.10472222222222,"row":"후열"}],"bearingAzimuthFrame":"model-xy"},"defaults":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"2주형","copingTopEl":7.738,"footingBottomEl":1.4,"groundEl":-0.342,"foundationType":"현장타설말뚝","pileLengthMm":29802,"copingWidthMm":3500,"copingLengthMm":24500,"columnCount":2,"ips":[{"name":"IP1","x":153837.294388,"y":480726.410803,"angleDeg":0},{"name":"IP2","x":153833.31775,"y":480714.93,"angleDeg":0},{"name":"IP3","x":153829.341111,"y":480703.449196,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":153836.266,"y":480725.3912,"z":7.8178,"azimuthDeg":109.10472222222222,"row":"전열"},{"no":2,"x":153835.2514,"y":480722.462,"z":7.8798,"azimuthDeg":109.10472222222222,"row":"전열"},{"no":3,"x":153834.2368,"y":480719.5327,"z":7.9418,"azimuthDeg":109.10472222222222,"row":"전열"},{"no":4,"x":153833.2222,"y":480716.6034,"z":8.0038,"azimuthDeg":109.10472222222222,"row":"전열"},{"no":5,"x":153832.2076,"y":480713.6742,"z":8.0038,"azimuthDeg":109.10472222222222,"row":"전열"},{"no":6,"x":153831.193,"y":480710.7449,"z":7.9418,"azimuthDeg":109.10472222222222,"row":"전열"},{"no":7,"x":153830.1783,"y":480707.8157,"z":7.8798,"azimuthDeg":109.10472222222222,"row":"전열"},{"no":8,"x":153829.1637,"y":480704.8864,"z":7.8178,"azimuthDeg":109.10472222222222,"row":"전열"},{"no":9,"x":153837.4944,"y":480724.9657,"z":7.8243,"azimuthDeg":109.10472222222222,"row":"후열"},{"no":10,"x":153836.4798,"y":480722.0365,"z":7.8863,"azimuthDeg":109.10472222222222,"row":"후열"},{"no":11,"x":153835.4652,"y":480719.1072,"z":7.9483,"azimuthDeg":109.10472222222222,"row":"후열"},{"no":12,"x":153834.4506,"y":480716.178,"z":8.0103,"azimuthDeg":109.10472222222222,"row":"후열"},{"no":13,"x":153833.436,"y":480713.2487,"z":8.0103,"azimuthDeg":109.10472222222222,"row":"후열"},{"no":14,"x":153832.4213,"y":480710.3195,"z":7.9483,"azimuthDeg":109.10472222222222,"row":"후열"},{"no":15,"x":153831.4067,"y":480707.3902,"z":7.8863,"azimuthDeg":109.10472222222222,"Line truncated
|
||||
@@ -0,0 +1 @@
|
||||
{"schemaVersion":1,"project":{"name":"샘플 04 · 목동교","bridge":"목동교","client":"","author":"","crs":"EPSG:5186","memo":"도면추출 연동 샘플 + 원도면 지반. 설계 지반고 유지. 지도 CRS는 원자료 확인이 필요한 가정(project_default_not_source_verified)."},"source":{"fileName":"샘플04_목동교_교각입력데이터.xlsx","sheets":["Pier","IP","Bearing"]},"piers":[{"id":"P1","input":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"2주형","copingTopEl":133.8541,"footingBottomEl":110.854,"groundEl":118.79,"foundationType":"직접기초","pileLengthMm":0,"copingWidthMm":3500,"copingLengthMm":16500,"columnCount":2,"ips":[{"name":"IP1","x":218127.628104,"y":531065.25932,"angleDeg":0},{"name":"IP2","x":218138.896901,"y":531074.320997,"angleDeg":0},{"name":"IP3","x":218140.182759,"y":531075.355006,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":218129.3712,"y":531065.8269,"z":133.9411,"azimuthDeg":321.06191666666666,"row":"전열"},{"no":2,"x":218131.265,"y":531067.3497,"z":133.9896,"azimuthDeg":321.0619722222222,"row":"전열"},{"no":3,"x":218133.3535,"y":531069.0291,"z":134.043,"azimuthDeg":321.06202777777776,"row":"전열"},{"no":4,"x":218135.4421,"y":531070.7086,"z":134.0965,"azimuthDeg":321.06208333333336,"row":"전열"},{"no":5,"x":218137.5306,"y":531072.3881,"z":134.15,"azimuthDeg":321.06213888888885,"row":"전열"},{"no":6,"x":218139.4243,"y":531073.9109,"z":134.1985,"azimuthDeg":321.06219444444446,"row":"전열"},{"no":7,"x":218128.5503,"y":531066.8348,"z":133.953,"azimuthDeg":320.60875,"row":"후열"},{"no":8,"x":218130.4438,"y":531068.3578,"z":134.0015,"azimuthDeg":320.60897222222223,"row":"후열"},{"no":9,"x":218132.5324,"y":531070.0373,"z":134.055,"azimuthDeg":320.60922222222223,"row":"후열"},{"no":10,"x":218134.6209,"y":531071.7168,"z":134.1084,"azimuthDeg":320.6094444444444,"row":"후열"},{"no":11,"x":218136.7095,"y":531073.3962,"z":134.1619,"azimuthDeg":320.60966666666667,"row":"후열"},{"no":12,"x":218138.6034,"y":531074.9188,"z":134.2103,"azimuthDeg":320.6098888888889,"row":"후열"}],"bearingAzimuthFrame":"model-xy"},"defaults":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"2주형","copingTopEl":133.8541,"footingBottomEl":110.854,"groundEl":118.79,"foundationType":"직접기초","pileLengthMm":0,"copingWidthMm":3500,"copingLengthMm":16500,"columnCount":2,"ips":[{"name":"IP1","x":218127.628104,"y":531065.25932,"angleDeg":0},{"name":"IP2","x":218138.896901,"y":531074.320997,"angleDeg":0},{"name":"IP3","x":218140.182759,"y":531075.355006,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":218129.3712,"y":531065.8269,"z":133.9411,"azimuthDeg":321.06191666666666,"row":"전열"},{"no":2,"x":218131.265,"y":531067.3497,"z":133.9896,"azimuthDeg":321.0619722222222,"row":"전열"},{"no":3,"x":218133.3535,"y":531069.0291,"z":134.043,"azimuthDeg":321.06202777777776,"row":"전열"},{"no":4,"x":218135.4421,"y":531070.7086,"z":134.0965,"azimuthDeg":321.06208333333336,"row":"전열"},{"no":5,"x":218137.5306,"y":531072.3881,"z":134.15,"azimuthDeg":321.06213888888885,"row":"전열"},{"no":6,"x":218139.4243,"y":531073.9109,"z":134.1985,"azimuthDeg":321.06219444444446,"row":"전열"},{"no":7,"x":218128.5503,"y":531066.8348,"z":133.953,"azimuthDeg":320.60875,"row":"후열"},{"no":8,"x":218130.4438,"y":531068.3578,"z":134.0015,"azimuthDeg":320.60897222222223,"row":"후열"},{"no":9,"x":218132.5324,"y":531070.0373,"z":134.055,"azimuthDeg":320.60922222222223,"row":"후열"},{"no":10,"x":218134.6209,"y":531071.7168,"z":134.1084,"azimuthDeg":320.6094444444444,"row":"후열"},{"no":11,"x":218136.7095,"y":531073.3962,"z":134.1619,"azimuthDeg":320.60966666666667,"row":"후열"},{"no":12,"x":218138.6034,"y":531074.9188,"z":134.2103,"azimuthDeg":320.6098888888889,"row":"후열"}],"bearingAzimuthFrame":"model-xy"},"draft":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"2주형","copingTopEl":133.8541,"footingBottomEl":110.854,"groundEl":118.79,"foundationType":"직접기초","pileLengthMm":0,"copingWidthMm":3500,"copingLengthMm":16500,"columnCount":2,"ips":[{"name":"IP1","x":218127.628104,"y":531065.25932,"angleDeg":0},{"name":"IP2","x":218138.896901,"y":531074.320997,"angleDeg":0},{"name":"IP3"Line truncated
|
||||
@@ -0,0 +1 @@
|
||||
{"schemaVersion":1,"project":{"name":"샘플 05 · 승천천교","bridge":"승천천교","client":"","author":"","crs":"EPSG:5186","memo":"도면추출 연동 샘플 + 원도면 지반. 설계 지반고 유지. 지도 CRS는 원자료 확인이 필요한 가정(project_default_not_source_verified)."},"source":{"fileName":"샘플05_승천천교_교각입력데이터.xlsx","sheets":["Pier","IP","Bearing"]},"piers":[],"history":[{"version":"sample-20261102","note":"교각 Excel과 원도면 지반·위성사진·2D 지도 통합. 좌표·스키마·형상 검증.","at":"2026-11-02T14:34:50.364983"}],"bearingBasis":"excel","groundModel":{"schema":2,"generated":"2026-11-02","piers":[{"id":"P1","centerEl":55,"excelEl":null}],"terrainGrid":{"origin":[458809.512,224222.818],"step":5,"nx":41,"ny":41,"z":[null,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55.322,55.896,55.661,55.11,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55.689,56.161,55.677,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55.78,55.857,55.371,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55.009,55.119,55.196,55.273,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,null,null,null,nullLine truncated
|
||||
@@ -0,0 +1 @@
|
||||
{"schemaVersion":1,"project":{"name":"샘플 06 · 계문철교","bridge":"계문철교","client":"","author":"","crs":"EPSG:5186","memo":"도면추출 연동 샘플 + 원도면 지반. 설계 지반고 유지. 지도 CRS는 원자료 확인이 필요한 가정(project_default_not_source_verified)."},"source":{"fileName":"샘플06_계문철교_교각입력데이터.xlsx","sheets":["Pier","IP","Bearing"]},"piers":[{"id":"P1","input":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"2주형","copingTopEl":18.5041,"footingBottomEl":4.804,"groundEl":7.06,"foundationType":"말뚝기초","pileLengthMm":21000,"copingWidthMm":3500,"copingLengthMm":21000,"columnCount":2,"ips":[{"name":"IP1","x":194908.7711,"y":376920.628019,"angleDeg":0},{"name":"IP2","x":194909.621705,"y":376910.212695,"angleDeg":0},{"name":"IP3","x":194910.472309,"y":376899.797371,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":194908.2982,"y":376919.5055,"z":19.6384,"azimuthDeg":83.45638888888891,"row":"전열"},{"no":2,"x":194908.5459,"y":376916.4756,"z":19.4854,"azimuthDeg":83.447,"row":"전열"},{"no":3,"x":194908.7936,"y":376913.4458,"z":19.3333,"azimuthDeg":83.43755555555555,"row":"전열"},{"no":4,"x":194909.0413,"y":376910.4159,"z":19.1822,"azimuthDeg":83.42797222222225,"row":"전열"},{"no":5,"x":194909.2891,"y":376907.386,"z":19.0292,"azimuthDeg":83.41830555555555,"row":"전열"},{"no":6,"x":194909.5368,"y":376904.3561,"z":18.8771,"azimuthDeg":83.40855555555555,"row":"전열"},{"no":7,"x":194909.7845,"y":376901.3262,"z":18.7251,"azimuthDeg":83.39869444444446,"row":"전열"},{"no":8,"x":194909.4601,"y":376919.6004,"z":19.6539,"azimuthDeg":87.6826388888889,"row":"후열"},{"no":9,"x":194909.7075,"y":376916.5705,"z":19.5019,"azimuthDeg":87.69441666666665,"row":"후열"},{"no":10,"x":194909.9548,"y":376913.5406,"z":19.3499,"azimuthDeg":87.7063333333333,"row":"후열"},{"no":11,"x":194910.2022,"y":376910.5106,"z":19.1979,"azimuthDeg":87.71836111111111,"row":"후열"},{"no":12,"x":194910.4496,"y":376907.4807,"z":19.0459,"azimuthDeg":87.7305,"row":"후열"},{"no":13,"x":194910.6969,"y":376904.4508,"z":18.8939,"azimuthDeg":87.74277777777775,"row":"후열"},{"no":14,"x":194910.9443,"y":376901.4209,"z":18.7419,"azimuthDeg":87.7551666666667,"row":"후열"}],"bearingAzimuthFrame":"model-xy"},"defaults":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"2주형","copingTopEl":18.5041,"footingBottomEl":4.804,"groundEl":7.06,"foundationType":"말뚝기초","pileLengthMm":21000,"copingWidthMm":3500,"copingLengthMm":21000,"columnCount":2,"ips":[{"name":"IP1","x":194908.7711,"y":376920.628019,"angleDeg":0},{"name":"IP2","x":194909.621705,"y":376910.212695,"angleDeg":0},{"name":"IP3","x":194910.472309,"y":376899.797371,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":194908.2982,"y":376919.5055,"z":19.6384,"azimuthDeg":83.45638888888891,"row":"전열"},{"no":2,"x":194908.5459,"y":376916.4756,"z":19.4854,"azimuthDeg":83.447,"row":"전열"},{"no":3,"x":194908.7936,"y":376913.4458,"z":19.3333,"azimuthDeg":83.43755555555555,"row":"전열"},{"no":4,"x":194909.0413,"y":376910.4159,"z":19.1822,"azimuthDeg":83.42797222222225,"row":"전열"},{"no":5,"x":194909.2891,"y":376907.386,"z":19.0292,"azimuthDeg":83.41830555555555,"row":"전열"},{"no":6,"x":194909.5368,"y":376904.3561,"z":18.8771,"azimuthDeg":83.40855555555555,"row":"전열"},{"no":7,"x":194909.7845,"y":376901.3262,"z":18.7251,"azimuthDeg":83.39869444444446,"row":"전열"},{"no":8,"x":194909.4601,"y":376919.6004,"z":19.6539,"azimuthDeg":87.6826388888889,"row":"후열"},{"no":9,"x":194909.7075,"y":376916.5705,"z":19.5019,"azimuthDeg":87.69441666666665,"row":"후열"},{"no":10,"x":194909.9548,"y":376913.5406,"z":19.3499,"azimuthDeg":87.7063333333333,"row":"후열"},{"no":11,"x":194910.2022,"y":376910.5106,"z":19.1979,"azimuthDeg":87.71836111111111,"row":"후열"},{"no":12,"x":194910.4496,"y":376907.4807,"z":19.0459,"azimuthDeg":87.7305,"row":"후열"},{"no":13,"x":194910.6969,"y":376904.4508,"z":18.8939,"azimuthDeg":87.74277777777775,"row":"후열"},{"no":14,"x":194910.9443,"y":376901.4209,"z":18.7419,"azimuthDeg":87.7551666666667,"row":"후열"}],"bearingAzimuthFrame":"model-xy"},"draft":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"PieLine truncated
|
||||
@@ -0,0 +1 @@
|
||||
{"schemaVersion":1,"project":{"name":"샘플 07 · 기양천교","bridge":"기양천교","client":"","author":"","crs":"EPSG:5186","memo":"도면추출 연동 샘플 + 원도면 지반. 설계 지반고 유지. 지도 CRS는 원자료 확인이 필요한 가정(project_default_not_source_verified)."},"source":{"fileName":"샘플07_기양천교_교각입력데이터.xlsx","sheets":["Pier","IP","Bearing"]},"piers":[{"id":"P1","input":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"2주형","copingTopEl":15.7665,"footingBottomEl":0.966,"groundEl":5.97,"foundationType":"말뚝기초","pileLengthMm":13600,"copingWidthMm":3500,"copingLengthMm":22500,"columnCount":2,"ips":[{"name":"IP1","x":195901.933872,"y":376676.684353,"angleDeg":0},{"name":"IP2","x":195904.750042,"y":376666.017388,"angleDeg":0},{"name":"IP3","x":195907.564243,"y":376655.357883,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":195901.7932,"y":376675.2306,"z":15.9498,"azimuthDeg":94.7138333333333,"row":"전열"},{"no":2,"x":195902.6065,"y":376672.1472,"z":16.0318,"azimuthDeg":94.67783333333335,"row":"전열"},{"no":3,"x":195903.4195,"y":376669.0643,"z":16.1134,"azimuthDeg":94.642,"row":"전열"},{"no":4,"x":195904.2324,"y":376665.9822,"z":16.1956,"azimuthDeg":94.60630555555554,"row":"전열"},{"no":5,"x":195905.045,"y":376662.9006,"z":16.2764,"azimuthDeg":94.57072222222223,"row":"전열"},{"no":6,"x":195905.8574,"y":376659.8197,"z":16.3568,"azimuthDeg":94.53527777777776,"row":"전열"},{"no":7,"x":195906.6696,"y":376656.7394,"z":16.4367,"azimuthDeg":94.4999722222222,"row":"전열"},{"no":8,"x":195902.9584,"y":376675.1501,"z":15.9811,"azimuthDeg":93.19136111111112,"row":"후열"},{"no":9,"x":195903.7717,"y":376672.0673,"z":16.0627,"azimuthDeg":93.15808333333337,"row":"후열"},{"no":10,"x":195904.5848,"y":376668.9852,"z":16.1438,"azimuthDeg":93.12494444444445,"row":"후열"},{"no":11,"x":195905.3977,"y":376665.9037,"z":16.2256,"azimuthDeg":93.09191666666669,"row":"후열"},{"no":12,"x":195906.2105,"y":376662.8229,"z":16.3059,"azimuthDeg":93.05902777777777,"row":"후열"},{"no":13,"x":195907.0231,"y":376659.7426,"z":16.3859,"azimuthDeg":93.02625,"row":"후열"},{"no":14,"x":195907.8355,"y":376656.663,"z":16.4654,"azimuthDeg":92.99358333333333,"row":"후열"}],"bearingAzimuthFrame":"model-xy"},"defaults":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"2주형","copingTopEl":15.7665,"footingBottomEl":0.966,"groundEl":5.97,"foundationType":"말뚝기초","pileLengthMm":13600,"copingWidthMm":3500,"copingLengthMm":22500,"columnCount":2,"ips":[{"name":"IP1","x":195901.933872,"y":376676.684353,"angleDeg":0},{"name":"IP2","x":195904.750042,"y":376666.017388,"angleDeg":0},{"name":"IP3","x":195907.564243,"y":376655.357883,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":195901.7932,"y":376675.2306,"z":15.9498,"azimuthDeg":94.7138333333333,"row":"전열"},{"no":2,"x":195902.6065,"y":376672.1472,"z":16.0318,"azimuthDeg":94.67783333333335,"row":"전열"},{"no":3,"x":195903.4195,"y":376669.0643,"z":16.1134,"azimuthDeg":94.642,"row":"전열"},{"no":4,"x":195904.2324,"y":376665.9822,"z":16.1956,"azimuthDeg":94.60630555555554,"row":"전열"},{"no":5,"x":195905.045,"y":376662.9006,"z":16.2764,"azimuthDeg":94.57072222222223,"row":"전열"},{"no":6,"x":195905.8574,"y":376659.8197,"z":16.3568,"azimuthDeg":94.53527777777776,"row":"전열"},{"no":7,"x":195906.6696,"y":376656.7394,"z":16.4367,"azimuthDeg":94.4999722222222,"row":"전열"},{"no":8,"x":195902.9584,"y":376675.1501,"z":15.9811,"azimuthDeg":93.19136111111112,"row":"후열"},{"no":9,"x":195903.7717,"y":376672.0673,"z":16.0627,"azimuthDeg":93.15808333333337,"row":"후열"},{"no":10,"x":195904.5848,"y":376668.9852,"z":16.1438,"azimuthDeg":93.12494444444445,"row":"후열"},{"no":11,"x":195905.3977,"y":376665.9037,"z":16.2256,"azimuthDeg":93.09191666666669,"row":"후열"},{"no":12,"x":195906.2105,"y":376662.8229,"z":16.3059,"azimuthDeg":93.05902777777777,"row":"후열"},{"no":13,"x":195907.0231,"y":376659.7426,"z":16.3859,"azimuthDeg":93.02625,"row":"후열"},{"no":14,"x":195907.8355,"y":376656.663,"z":16.4654,"azimuthDeg":92.99358333333333,"row":"후열"}],"bearingAzimuthFrame":"model-xy"},"draft":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheetLine truncated
|
||||
@@ -0,0 +1 @@
|
||||
{"schemaVersion":1,"project":{"name":"샘플 08 · 옥성교","bridge":"옥성교","client":"","author":"","crs":"EPSG:5186","memo":"도면추출 연동 샘플 + 원도면 지반. 설계 지반고 유지. 지도 CRS는 원자료 확인이 필요한 가정(project_default_not_source_verified)."},"source":{"fileName":"샘플08_옥성교_교각입력데이터.xlsx","sheets":["Pier","IP","Bearing"]},"piers":[{"id":"P1","input":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"T형","copingTopEl":21.1279,"footingBottomEl":12.175,"groundEl":18.09,"foundationType":"말뚝기초","pileLengthMm":22000,"copingWidthMm":3500,"copingLengthMm":13500,"columnCount":1,"ips":[{"name":"IP1","x":207605.780013,"y":429978.461366,"angleDeg":0},{"name":"IP2","x":207599.646855,"y":429976.251707,"angleDeg":0},{"name":"IP3","x":207593.513698,"y":429974.042048,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":207604.1324,"y":429978.4314,"z":21.3981,"azimuthDeg":142.18688888888886,"row":"전열"},{"no":2,"x":207601.8028,"y":429977.5921,"z":21.4317,"azimuthDeg":142.18688888888886,"row":"전열"},{"no":3,"x":207599.4732,"y":429976.7528,"z":21.4653,"azimuthDeg":142.18688888888886,"row":"전열"},{"no":4,"x":207597.1436,"y":429975.9134,"z":21.4115,"azimuthDeg":142.18688888888886,"row":"전열"},{"no":5,"x":207594.814,"y":429975.0741,"z":21.3509,"azimuthDeg":142.18688888888886,"row":"전열"},{"no":6,"x":207604.868,"y":429977.4835,"z":21.4193,"azimuthDeg":142.18688888888886,"row":"후열"},{"no":7,"x":207602.5384,"y":429976.6442,"z":21.4529,"azimuthDeg":142.18688888888886,"row":"후열"},{"no":8,"x":207600.2088,"y":429975.8048,"z":21.4865,"azimuthDeg":142.18688888888886,"row":"후열"},{"no":9,"x":207597.8792,"y":429974.9655,"z":21.4326,"azimuthDeg":142.18688888888886,"row":"후열"},{"no":10,"x":207595.5496,"y":429974.1262,"z":21.372,"azimuthDeg":142.18688888888886,"row":"후열"}],"bearingAzimuthFrame":"model-xy"},"defaults":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"T형","copingTopEl":21.1279,"footingBottomEl":12.175,"groundEl":18.09,"foundationType":"말뚝기초","pileLengthMm":22000,"copingWidthMm":3500,"copingLengthMm":13500,"columnCount":1,"ips":[{"name":"IP1","x":207605.780013,"y":429978.461366,"angleDeg":0},{"name":"IP2","x":207599.646855,"y":429976.251707,"angleDeg":0},{"name":"IP3","x":207593.513698,"y":429974.042048,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":207604.1324,"y":429978.4314,"z":21.3981,"azimuthDeg":142.18688888888886,"row":"전열"},{"no":2,"x":207601.8028,"y":429977.5921,"z":21.4317,"azimuthDeg":142.18688888888886,"row":"전열"},{"no":3,"x":207599.4732,"y":429976.7528,"z":21.4653,"azimuthDeg":142.18688888888886,"row":"전열"},{"no":4,"x":207597.1436,"y":429975.9134,"z":21.4115,"azimuthDeg":142.18688888888886,"row":"전열"},{"no":5,"x":207594.814,"y":429975.0741,"z":21.3509,"azimuthDeg":142.18688888888886,"row":"전열"},{"no":6,"x":207604.868,"y":429977.4835,"z":21.4193,"azimuthDeg":142.18688888888886,"row":"후열"},{"no":7,"x":207602.5384,"y":429976.6442,"z":21.4529,"azimuthDeg":142.18688888888886,"row":"후열"},{"no":8,"x":207600.2088,"y":429975.8048,"z":21.4865,"azimuthDeg":142.18688888888886,"row":"후열"},{"no":9,"x":207597.8792,"y":429974.9655,"z":21.4326,"azimuthDeg":142.18688888888886,"row":"후열"},{"no":10,"x":207595.5496,"y":429974.1262,"z":21.372,"azimuthDeg":142.18688888888886,"row":"후열"}],"bearingAzimuthFrame":"model-xy"},"draft":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"T형","copingTopEl":21.1279,"footingBottomEl":12.175,"groundEl":18.09,"foundationType":"말뚝기초","pileLengthMm":22000,"copingWidthMm":3500,"copingLengthMm":13500,"columnCount":1,"ips":[{"name":"IP1","x":207605.780013,"y":429978.461366,"angleDeg":0},{"name":"IP2","x":207599.646855,"y":429976.251707,"angleDeg":0},{"name":"IP3","x":207593.513698,"y":429974.042048,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":207604.1324,"y":429978.4314,"z":21.3981,"azimuthDeg":142.18688888888886,"row":"전열"},{"no":2,"x":207601.8028,"y":429977.5921,"z":21.4317,"azimuthDeg":142.18688888888886,"row":"전열"},{"no":3,"x":207599.4732,"y":429976.7528,"z":21.4653,"azimuthDeg":142.18688888888886,"row":"전열Line truncated
|
||||
@@ -0,0 +1 @@
|
||||
{"schemaVersion":1,"project":{"name":"샘플 09 · 새재1교","bridge":"새재1교","client":"","author":"","crs":"EPSG:5186","memo":"도면추출 연동 샘플 + 원도면 지반. 설계 지반고 유지. 지도 CRS는 원자료 확인이 필요한 가정(project_default_not_source_verified)."},"source":{"fileName":"샘플09_새재1교_교각입력데이터.xlsx","sheets":["Pier","IP","Bearing"]},"piers":[{"id":"P1","input":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"T형","copingTopEl":32.5316,"footingBottomEl":10.032,"groundEl":16.95,"foundationType":"직접기초","pileLengthMm":0,"copingWidthMm":3000,"copingLengthMm":12500,"columnCount":1,"ips":[{"name":"IP1","x":208547.804218,"y":428110.699674,"angleDeg":0},{"name":"IP2","x":208542.956234,"y":428106.806474,"angleDeg":0},{"name":"IP3","x":208538.108532,"y":428102.913503,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":208546.25,"y":428110.0923,"z":32.7901,"azimuthDeg":139.06241666666665,"row":"전열"},{"no":2,"x":208544.2952,"y":428108.5226,"z":32.8911,"azimuthDeg":139.07152777777776,"row":"전열"},{"no":3,"x":208542.3406,"y":428106.9529,"z":32.9921,"azimuthDeg":139.08055555555552,"row":"전열"},{"no":4,"x":208540.3859,"y":428105.3832,"z":33.0931,"azimuthDeg":139.0895277777778,"row":"전열"},{"no":5,"x":208538.4314,"y":428103.8136,"z":33.1941,"azimuthDeg":139.0984166666667,"row":"전열"},{"no":6,"x":208546.9329,"y":428109.3626,"z":32.7757,"azimuthDeg":134.73080555555555,"row":"후열"},{"no":7,"x":208544.978,"y":428107.7926,"z":32.8767,"azimuthDeg":134.75805555555553,"row":"후열"},{"no":8,"x":208543.0231,"y":428106.2227,"z":32.9777,"azimuthDeg":134.78505555555557,"row":"후열"},{"no":9,"x":208541.0682,"y":428104.6528,"z":33.0787,"azimuthDeg":134.8118333333333,"row":"후열"},{"no":10,"x":208539.1134,"y":428103.083,"z":33.1797,"azimuthDeg":134.83838888888891,"row":"후열"}],"bearingAzimuthFrame":"model-xy"},"defaults":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"T형","copingTopEl":32.5316,"footingBottomEl":10.032,"groundEl":16.95,"foundationType":"직접기초","pileLengthMm":0,"copingWidthMm":3000,"copingLengthMm":12500,"columnCount":1,"ips":[{"name":"IP1","x":208547.804218,"y":428110.699674,"angleDeg":0},{"name":"IP2","x":208542.956234,"y":428106.806474,"angleDeg":0},{"name":"IP3","x":208538.108532,"y":428102.913503,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":208546.25,"y":428110.0923,"z":32.7901,"azimuthDeg":139.06241666666665,"row":"전열"},{"no":2,"x":208544.2952,"y":428108.5226,"z":32.8911,"azimuthDeg":139.07152777777776,"row":"전열"},{"no":3,"x":208542.3406,"y":428106.9529,"z":32.9921,"azimuthDeg":139.08055555555552,"row":"전열"},{"no":4,"x":208540.3859,"y":428105.3832,"z":33.0931,"azimuthDeg":139.0895277777778,"row":"전열"},{"no":5,"x":208538.4314,"y":428103.8136,"z":33.1941,"azimuthDeg":139.0984166666667,"row":"전열"},{"no":6,"x":208546.9329,"y":428109.3626,"z":32.7757,"azimuthDeg":134.73080555555555,"row":"후열"},{"no":7,"x":208544.978,"y":428107.7926,"z":32.8767,"azimuthDeg":134.75805555555553,"row":"후열"},{"no":8,"x":208543.0231,"y":428106.2227,"z":32.9777,"azimuthDeg":134.78505555555557,"row":"후열"},{"no":9,"x":208541.0682,"y":428104.6528,"z":33.0787,"azimuthDeg":134.8118333333333,"row":"후열"},{"no":10,"x":208539.1134,"y":428103.083,"z":33.1797,"azimuthDeg":134.83838888888891,"row":"후열"}],"bearingAzimuthFrame":"model-xy"},"draft":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"T형","copingTopEl":32.5316,"footingBottomEl":10.032,"groundEl":16.95,"foundationType":"직접기초","pileLengthMm":0,"copingWidthMm":3000,"copingLengthMm":12500,"columnCount":1,"ips":[{"name":"IP1","x":208547.804218,"y":428110.699674,"angleDeg":0},{"name":"IP2","x":208542.956234,"y":428106.806474,"angleDeg":0},{"name":"IP3","x":208538.108532,"y":428102.913503,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":208546.25,"y":428110.0923,"z":32.7901,"azimuthDeg":139.06241666666665,"row":"전열"},{"no":2,"x":208544.2952,"y":428108.5226,"z":32.8911,"azimuthDeg":139.07152777777776,"row":"전열"},{"no":3,"x":208542.3406,"y":428106.9529,"z":32.9921,"azimuthDeg":139.08055555555552,"row":"전열"},{"no":4,"x":2085Line truncated
|
||||
@@ -0,0 +1 @@
|
||||
{"schemaVersion":1,"project":{"name":"샘플 10 · 도하4교","bridge":"도하4교","client":"","author":"","crs":"EPSG:5186","memo":"도면추출 연동 샘플 + 원도면 지반. 설계 지반고 유지. 지도 CRS는 원자료 확인이 필요한 가정(project_default_not_source_verified)."},"source":{"fileName":"샘플10_도하4교_교각입력데이터.xlsx","sheets":["Pier","IP","Bearing"]},"piers":[{"id":"P1","input":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"T형","copingTopEl":98.552,"footingBottomEl":78.852,"groundEl":85.365,"foundationType":"직접기초","pileLengthMm":0,"copingWidthMm":4000,"copingLengthMm":12500,"columnCount":1,"ips":[{"name":"IP1","x":200987.223238,"y":583704.910196,"angleDeg":0},{"name":"IP2","x":200987.580804,"y":583703.299406,"angleDeg":0},{"name":"IP3","x":200989.888734,"y":583692.902485,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":200987.095,"y":583702.013,"z":98.859,"azimuthDeg":77.47163888888889,"row":"전열"},{"no":2,"x":200988.549,"y":583695.473,"z":98.725,"azimuthDeg":77.47163888888889,"row":"전열"},{"no":3,"x":200988.565,"y":583702.34,"z":98.847,"azimuthDeg":77.49702777777776,"row":"후열"},{"no":4,"x":200990.015,"y":583695.798,"z":98.713,"azimuthDeg":77.49700000000001,"row":"후열"}],"bearingAzimuthFrame":"model-xy"},"defaults":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"T형","copingTopEl":98.552,"footingBottomEl":78.852,"groundEl":85.365,"foundationType":"직접기초","pileLengthMm":0,"copingWidthMm":4000,"copingLengthMm":12500,"columnCount":1,"ips":[{"name":"IP1","x":200987.223238,"y":583704.910196,"angleDeg":0},{"name":"IP2","x":200987.580804,"y":583703.299406,"angleDeg":0},{"name":"IP3","x":200989.888734,"y":583692.902485,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":200987.095,"y":583702.013,"z":98.859,"azimuthDeg":77.47163888888889,"row":"전열"},{"no":2,"x":200988.549,"y":583695.473,"z":98.725,"azimuthDeg":77.47163888888889,"row":"전열"},{"no":3,"x":200988.565,"y":583702.34,"z":98.847,"azimuthDeg":77.49702777777776,"row":"후열"},{"no":4,"x":200990.015,"y":583695.798,"z":98.713,"azimuthDeg":77.49700000000001,"row":"후열"}],"bearingAzimuthFrame":"model-xy"},"draft":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"T형","copingTopEl":98.552,"footingBottomEl":78.852,"groundEl":85.365,"foundationType":"직접기초","pileLengthMm":0,"copingWidthMm":4000,"copingLengthMm":12500,"columnCount":1,"ips":[{"name":"IP1","x":200987.223238,"y":583704.910196,"angleDeg":0},{"name":"IP2","x":200987.580804,"y":583703.299406,"angleDeg":0},{"name":"IP3","x":200989.888734,"y":583692.902485,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":200987.095,"y":583702.013,"z":98.859,"azimuthDeg":77.47163888888889,"row":"전열"},{"no":2,"x":200988.549,"y":583695.473,"z":98.725,"azimuthDeg":77.47163888888889,"row":"전열"},{"no":3,"x":200988.565,"y":583702.34,"z":98.847,"azimuthDeg":77.49702777777776,"row":"후열"},{"no":4,"x":200990.015,"y":583695.798,"z":98.713,"azimuthDeg":77.49700000000001,"row":"후열"}],"bearingAzimuthFrame":"model-xy"}},{"id":"P2","input":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P2","sheet":"Pier","type":"T형","copingTopEl":98.108,"footingBottomEl":78.408,"groundEl":85.754,"foundationType":"직접기초","pileLengthMm":0,"copingWidthMm":4000,"copingLengthMm":12500,"columnCount":1,"ips":[{"name":"IP1","x":201041.036417,"y":583716.399861,"angleDeg":0},{"name":"IP2","x":201041.367881,"y":583714.783498,"angleDeg":0},{"name":"IP3","x":201043.507331,"y":583704.350605,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":201040.861,"y":583713.505,"z":98.415,"azimuthDeg":78.39847222222221,"row":"전열"},{"no":2,"x":201042.209,"y":583706.942,"z":98.281,"azimuthDeg":78.39847222222221,"row":"전열"},{"no":3,"x":201042.336,"y":583713.808,"z":98.402,"azimuthDeg":78.42386111111114,"row":"후열"},{"no":4,"x":201043.68,"y":583707.244,"z":98.268,"azimuthDeg":78.42383333333333,"row"Line truncated
|
||||
@@ -0,0 +1 @@
|
||||
{"schemaVersion":1,"project":{"name":"샘플 11 · 곡천교","bridge":"곡천교","client":"","author":"","crs":"EPSG:5186","memo":"도면추출 연동 샘플 + 원도면 지반. 설계 지반고 유지. 지도 CRS는 원자료 확인이 필요한 가정(project_default_not_source_verified)."},"source":{"fileName":"샘플11_곡천교_교각입력데이터.xlsx","sheets":["Pier","IP","Bearing"]},"piers":[{"id":"P1","input":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"2주형","copingTopEl":86.8393,"footingBottomEl":61.44,"groundEl":64.692,"foundationType":"직접기초","pileLengthMm":0,"copingWidthMm":3500,"copingLengthMm":17000,"columnCount":2,"ips":[{"name":"IP1","x":226998.64978,"y":492646.179517,"angleDeg":0},{"name":"IP2","x":226998.640605,"y":492646.174649,"angleDeg":0},{"name":"IP3","x":227013.515037,"y":492654.066886,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":226999.795,"y":492646.0899,"z":87.3778,"azimuthDeg":347.36733333333336,"row":"전열"},{"no":2,"x":227002.2906,"y":492647.414,"z":87.2989,"azimuthDeg":347.3780277777778,"row":"전열"},{"no":3,"x":227004.7862,"y":492648.7381,"z":87.2201,"azimuthDeg":347.38875,"row":"전열"},{"no":4,"x":227007.282,"y":492650.0623,"z":87.1413,"azimuthDeg":347.39947222222224,"row":"전열"},{"no":5,"x":227009.7779,"y":492651.3866,"z":87.0623,"azimuthDeg":347.4102222222222,"row":"전열"},{"no":6,"x":227012.2739,"y":492652.7109,"z":86.9834,"azimuthDeg":347.421,"row":"전열"},{"no":7,"x":226999.5196,"y":492647.3558,"z":87.3832,"azimuthDeg":348.11983333333336,"row":"후열"},{"no":8,"x":227002.0154,"y":492648.6799,"z":87.3043,"azimuthDeg":348.1310555555556,"row":"후열"},{"no":9,"x":227004.5113,"y":492650.0041,"z":87.2254,"azimuthDeg":348.1422777777778,"row":"후열"},{"no":10,"x":227007.0073,"y":492651.3283,"z":87.1466,"azimuthDeg":348.15352777777775,"row":"후열"},{"no":11,"x":227009.5035,"y":492652.6527,"z":87.0676,"azimuthDeg":348.1647777777778,"row":"후열"},{"no":12,"x":227011.9998,"y":492653.9771,"z":86.9886,"azimuthDeg":348.17605555555554,"row":"후열"}],"bearingAzimuthFrame":"model-xy"},"defaults":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"2주형","copingTopEl":86.8393,"footingBottomEl":61.44,"groundEl":64.692,"foundationType":"직접기초","pileLengthMm":0,"copingWidthMm":3500,"copingLengthMm":17000,"columnCount":2,"ips":[{"name":"IP1","x":226998.64978,"y":492646.179517,"angleDeg":0},{"name":"IP2","x":226998.640605,"y":492646.174649,"angleDeg":0},{"name":"IP3","x":227013.515037,"y":492654.066886,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":226999.795,"y":492646.0899,"z":87.3778,"azimuthDeg":347.36733333333336,"row":"전열"},{"no":2,"x":227002.2906,"y":492647.414,"z":87.2989,"azimuthDeg":347.3780277777778,"row":"전열"},{"no":3,"x":227004.7862,"y":492648.7381,"z":87.2201,"azimuthDeg":347.38875,"row":"전열"},{"no":4,"x":227007.282,"y":492650.0623,"z":87.1413,"azimuthDeg":347.39947222222224,"row":"전열"},{"no":5,"x":227009.7779,"y":492651.3866,"z":87.0623,"azimuthDeg":347.4102222222222,"row":"전열"},{"no":6,"x":227012.2739,"y":492652.7109,"z":86.9834,"azimuthDeg":347.421,"row":"전열"},{"no":7,"x":226999.5196,"y":492647.3558,"z":87.3832,"azimuthDeg":348.11983333333336,"row":"후열"},{"no":8,"x":227002.0154,"y":492648.6799,"z":87.3043,"azimuthDeg":348.1310555555556,"row":"후열"},{"no":9,"x":227004.5113,"y":492650.0041,"z":87.2254,"azimuthDeg":348.1422777777778,"row":"후열"},{"no":10,"x":227007.0073,"y":492651.3283,"z":87.1466,"azimuthDeg":348.15352777777775,"row":"후열"},{"no":11,"x":227009.5035,"y":492652.6527,"z":87.0676,"azimuthDeg":348.1647777777778,"row":"후열"},{"no":12,"x":227011.9998,"y":492653.9771,"z":86.9886,"azimuthDeg":348.17605555555554,"row":"후열"}],"bearingAzimuthFrame":"model-xy"},"draft":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"2주형","copingTopEl":86.8393,"footingBottomEl":61.44,"groundEl":64.692,"foundationType":"직접기초","pileLengthMm":0,"copingWidthMm":3500,"copingLengthMm":17000,"columnCount":2,"ips":[{"name":"IP1","x":226998.64978,"y":492646.179517,"angleDeg":0},{"name":"IP2","x":226998.640605,"y":492646.174649,"angleDeg":0},{"name":"IP3","x":227013.515037,"y":492654.066886,"angleDeg":0},{"Line truncated
|
||||
@@ -0,0 +1 @@
|
||||
{"schemaVersion":1,"project":{"name":"샘플 12 · 감북교","bridge":"감북교","client":"","author":"","crs":"EPSG:5186","memo":"도면추출 연동 샘플 + 원도면 지반. 설계 지반고 유지. 지도 CRS는 원자료 확인이 필요한 가정(inferred_false_northing_convention_not_source_verified)."},"source":{"fileName":"샘플12_감북교_교각입력데이터.xlsx","sheets":["Pier","IP","Bearing"]},"piers":[{"id":"P1","input":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"2주형","copingTopEl":56.5998,"footingBottomEl":43.604,"groundEl":44.64,"foundationType":"현장타설말뚝","pileLengthMm":17566,"copingWidthMm":3000,"copingLengthMm":20000,"columnCount":2,"ips":[{"name":"IP1","x":214529.86606,"y":547194.678176,"angleDeg":0},{"name":"IP2","x":214548.146791,"y":547202.413639,"angleDeg":0},{"name":"IP3","x":214548.137582,"y":547202.409742,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":214531.1568,"y":547194.6814,"z":56.8196,"azimuthDeg":337.06444444444446,"row":"전열"},{"no":2,"x":214533.8276,"y":547195.8116,"z":56.8776,"azimuthDeg":337.06444444444446,"row":"전열"},{"no":3,"x":214536.4983,"y":547196.9417,"z":56.9356,"azimuthDeg":337.06444444444446,"row":"전열"},{"no":4,"x":214539.169,"y":547198.0718,"z":56.9936,"azimuthDeg":337.06444444444446,"row":"전열"},{"no":5,"x":214541.8398,"y":547199.2019,"z":57.0516,"azimuthDeg":337.06444444444446,"row":"전열"},{"no":6,"x":214544.5105,"y":547200.332,"z":57.1096,"azimuthDeg":337.06444444444446,"row":"전열"},{"no":7,"x":214547.1812,"y":547201.4622,"z":57.1676,"azimuthDeg":337.06444444444446,"row":"전열"},{"no":8,"x":214530.7671,"y":547195.6024,"z":56.7998,"azimuthDeg":337.06444444444446,"row":"후열"},{"no":9,"x":214533.4379,"y":547196.7325,"z":56.8578,"azimuthDeg":337.06444444444446,"row":"후열"},{"no":10,"x":214536.1086,"y":547197.8626,"z":56.9158,"azimuthDeg":337.06444444444446,"row":"후열"},{"no":11,"x":214538.7793,"y":547198.9927,"z":56.9738,"azimuthDeg":337.06444444444446,"row":"후열"},{"no":12,"x":214541.4501,"y":547200.1229,"z":57.0318,"azimuthDeg":337.06444444444446,"row":"후열"},{"no":13,"x":214544.1208,"y":547201.253,"z":57.0898,"azimuthDeg":337.06444444444446,"row":"후열"},{"no":14,"x":214546.7916,"y":547202.3831,"z":57.1478,"azimuthDeg":337.06444444444446,"row":"후열"}],"bearingAzimuthFrame":"model-xy"},"defaults":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"2주형","copingTopEl":56.5998,"footingBottomEl":43.604,"groundEl":44.64,"foundationType":"현장타설말뚝","pileLengthMm":17566,"copingWidthMm":3000,"copingLengthMm":20000,"columnCount":2,"ips":[{"name":"IP1","x":214529.86606,"y":547194.678176,"angleDeg":0},{"name":"IP2","x":214548.146791,"y":547202.413639,"angleDeg":0},{"name":"IP3","x":214548.137582,"y":547202.409742,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":214531.1568,"y":547194.6814,"z":56.8196,"azimuthDeg":337.06444444444446,"row":"전열"},{"no":2,"x":214533.8276,"y":547195.8116,"z":56.8776,"azimuthDeg":337.06444444444446,"row":"전열"},{"no":3,"x":214536.4983,"y":547196.9417,"z":56.9356,"azimuthDeg":337.06444444444446,"row":"전열"},{"no":4,"x":214539.169,"y":547198.0718,"z":56.9936,"azimuthDeg":337.06444444444446,"row":"전열"},{"no":5,"x":214541.8398,"y":547199.2019,"z":57.0516,"azimuthDeg":337.06444444444446,"row":"전열"},{"no":6,"x":214544.5105,"y":547200.332,"z":57.1096,"azimuthDeg":337.06444444444446,"row":"전열"},{"no":7,"x":214547.1812,"y":547201.4622,"z":57.1676,"azimuthDeg":337.06444444444446,"row":"전열"},{"no":8,"x":214530.7671,"y":547195.6024,"z":56.7998,"azimuthDeg":337.06444444444446,"row":"후열"},{"no":9,"x":214533.4379,"y":547196.7325,"z":56.8578,"azimuthDeg":337.06444444444446,"row":"후열"},{"no":10,"x":214536.1086,"y":547197.8626,"z":56.9158,"azimuthDeg":337.06444444444446,"row":"후열"},{"no":11,"x":214538.7793,"y":547198.9927,"z":56.9738,"azimuthDeg":337.06444444444446,"row":"후열"},{"no":12,"x":214541.4501,"y":547200.1229,"z":57.0318,"azimuthDeg":337.06444444444446,"row":"후열"},{"no":13,"x":214544.1208,"y":547201.253,"z":57.0898,"azimuthDeg":337.06444444444446,"row":"후열"},{"no":14,"x":214546.7916,"y":547202.3831,"z":57.1478,"azimuthDeg":337.06444444444446,"row":"후열"}],"bearingAzimuthFrame":"model-xy"},"draft":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"Line truncated
|
||||
+14
-4
@@ -1,4 +1,5 @@
|
||||
import './style.css';
|
||||
import { createDrawingWorkspace } from '../도면작성/workspace';
|
||||
import * as THREE from 'three';
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
||||
import { swapXYAzimuth } from './core/coordinate-frame';
|
||||
@@ -76,8 +77,8 @@ interface Pier extends PierValues {
|
||||
}
|
||||
|
||||
/** Part toggles in the order of 시안 ① "부재 표시". Each toggle controls one or more scene parts. */
|
||||
/** The four stages: ① 형상 생성, ② 형상계획, ③ 철근배근, ④ 재료표 (결과 화면). */
|
||||
type Stage = 'shape' | 'plan' | 'rebar' | 'quantity';
|
||||
/** ① 형상 생성, ② 형상계획, ③ 철근배근, ④ 도면작성, ⑤ 재료표. */
|
||||
type Stage = 'shape' | 'plan' | 'rebar' | 'drawing' | 'quantity';
|
||||
|
||||
const PART_GROUPS: { label: string; parts: PartName[] }[] = [
|
||||
{ label: '받침·받침블럭', parts: ['blocks', 'pads', 'bearings'] },
|
||||
@@ -164,6 +165,11 @@ let backupTimer: number | undefined;
|
||||
|
||||
const $ = <T extends HTMLElement>(id: string): T => document.getElementById(id) as T;
|
||||
|
||||
const drawingWorkspace = createDrawingWorkspace($('view-drawing'), () => ({
|
||||
project: state.project, selected: state.active,
|
||||
piers: state.piers.map(p => ({ input: p.input, model: p.model, dirty: pendingKeys(p).length > 0 })),
|
||||
}), selectPier);
|
||||
|
||||
function el<K extends keyof HTMLElementTagNameMap>(tag: K, text?: string, cls?: string): HTMLElementTagNameMap[K] {
|
||||
const node = document.createElement(tag);
|
||||
if (text !== undefined) node.textContent = text;
|
||||
@@ -552,6 +558,7 @@ function renderHeader(): void {
|
||||
parts.push(`교각 ${state.piers.length}기`);
|
||||
$('project-line').textContent = parts.join(' · ');
|
||||
$('step1-status').textContent = pier?.model ? '생성됨' : '미생성';
|
||||
$('step-drawing-status').textContent = pier?.model ? '가능' : '형상 필요';
|
||||
renderSaveStatus();
|
||||
const total = pendingTotal();
|
||||
const targets = generationTargets();
|
||||
@@ -717,6 +724,7 @@ function renderAll(): void {
|
||||
renderRebar();
|
||||
renderRebarColor();
|
||||
if (state.stage === 'quantity') renderQuantityScreen();
|
||||
if (state.stage === 'drawing') drawingWorkspace.update();
|
||||
scheduleBackup();
|
||||
}
|
||||
|
||||
@@ -1177,18 +1185,19 @@ function setStage(stage: Stage): void {
|
||||
if (stage === 'shape' && state.stage !== 'shape') state.allView = true;
|
||||
state.stage = stage;
|
||||
// Switching 전체 보기 on or off in ① needs the scene rebuilt for the new layout.
|
||||
if ((allPiers !== null) !== multiSceneOn()) setupScene();
|
||||
if (stage !== 'drawing' && (allPiers !== null) !== multiSceneOn()) setupScene();
|
||||
if (state.scene) setRebarView(state.scene, stage === 'rebar');
|
||||
$<HTMLElement>('view-shape').hidden = stage !== 'shape';
|
||||
$<HTMLElement>('view-plan').hidden = stage !== 'plan';
|
||||
$<HTMLElement>('view-rebar').hidden = stage !== 'rebar';
|
||||
$<HTMLElement>('view-quantity').hidden = stage !== 'quantity';
|
||||
$<HTMLElement>('view-drawing').hidden = stage !== 'drawing';
|
||||
if (stage === 'quantity') renderQuantityScreen();
|
||||
// Move the viewer after its new parent is visible, so that resize() measures the real size.
|
||||
placeViewer(stage);
|
||||
if (rebar3d) rebar3d.group.visible = stage === 'rebar';
|
||||
// The highlight is the 'current' class (style.css); aria-current is set alongside it for assistive tech.
|
||||
const nav: [string, Stage][] = [['nav-shape', 'shape'], ['nav-plan', 'plan'], ['nav-rebar', 'rebar'], ['nav-quantity', 'quantity']];
|
||||
const nav: [string, Stage][] = [['nav-shape', 'shape'], ['nav-plan', 'plan'], ['nav-rebar', 'rebar'], ['nav-drawing', 'drawing'], ['nav-quantity', 'quantity']];
|
||||
for (const [id, key] of nav) {
|
||||
const btn = $<HTMLButtonElement>(id);
|
||||
btn.classList.toggle('current', stage === key);
|
||||
@@ -3279,6 +3288,7 @@ $<HTMLButtonElement>('regen').addEventListener('click', regenerateChanged);
|
||||
$<HTMLButtonElement>('nav-shape').addEventListener('click', () => setStage('shape'));
|
||||
$<HTMLButtonElement>('nav-plan').addEventListener('click', () => setStage('plan'));
|
||||
$<HTMLButtonElement>('nav-rebar').addEventListener('click', () => setStage('rebar'));
|
||||
$<HTMLButtonElement>('nav-drawing').addEventListener('click', () => setStage('drawing'));
|
||||
$<HTMLButtonElement>('nav-quantity').addEventListener('click', () => setStage('quantity'));
|
||||
$<HTMLInputElement>('mat-fck').addEventListener('change', (ev) => setMaterial('fck', (ev.target as HTMLInputElement).value));
|
||||
$<HTMLInputElement>('mat-waste').addEventListener('change', (ev) => setWaste((ev.target as HTMLInputElement).value));
|
||||
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { defineConfig } from 'vite';
|
||||
import { drawingLauncherPlugin } from './랜딩페이지/drawing-launcher.ts';
|
||||
import { generalArrangementDwgPlugin } from './도면작성/dwg-server.ts';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [drawingLauncherPlugin(fileURLToPath(new URL('.', import.meta.url)))],
|
||||
plugins: [drawingLauncherPlugin(fileURLToPath(new URL('.', import.meta.url))),generalArrangementDwgPlugin(fileURLToPath(new URL('.', import.meta.url)))],
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
.cache/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
output/
|
||||
@@ -0,0 +1,67 @@
|
||||
> 최신 원본 양식 보강과 변경된 치수 스타일(문자 3mm, 점 기호)은 [REFERENCE_PARITY.md](REFERENCE_PARITY.md)를 우선 참조하세요. 아래는 이전 품질 개선 단계의 기록입니다.
|
||||
|
||||
# 교각 일반도 품질 개선 보고서 — 2026-11-03
|
||||
|
||||
## 기준 도면과 기존 결과의 차이
|
||||
|
||||
기준은 이 폴더의 `[교각-T 형] 01 일반도_20260619.dwg`이다. 첨부 원본과 SHA-256이 같은 파일이며 원본을 수정하지 않았다. 원본의 CXBLKA1 도곽/로고, 표제란 38개 속성, NOTE 21줄을 유지했다. 정면·측면 상단, 두부보·기초 평면 하단, 우측 상세·제원표·키플랜·NOTE 구성도 유지했다.
|
||||
|
||||
기존 결과는 고정 슬롯과 형상 경계 중심으로 축척을 정해, 문자와 치수선까지 고려한 점유 영역이 맞지 않았다. 전체·세부 치수가 같은 공간을 차지하고 출력용 문자가 작았으며, 긴 기둥 생략 표시와 실치수의 배치 관계도 미흡했다. 개선 순서는 점유 영역 측정, 치수 레인, 공통 축척·정렬, 출력 스타일, CAD 재읽기 검증이다.
|
||||
|
||||
원본 HS_050의 dimtxt=3, 화살표=1을 참고했다. 원본 레이어는 BYDEFAULT 선가중치를 사용하며 CTB가 없으므로 출력 선두께를 원본과 동일하다고 단정하지 않는다. 새 문서는 외형/숨은선/중심선/치수선/해치에 명시적인 선가중치를 지정한다.
|
||||
|
||||
## 구현 내용
|
||||
|
||||
- 실제 3D 모델 `PierModel`의 투영 형상을 유지한다. 기둥·말뚝 생략은 표시 변환이며 실제 길이·EL·모델 공간 치수는 유지한다.
|
||||
- 문자 회전·한글 폭·화살표·보조선·기둥 생략을 포함한 점유 영역을 계산한다. 정면/측면은 공통 축척과 EL 정렬을 사용하고, 형상별 열 너비·행 높이와 평면도 축척을 산정한다.
|
||||
- 짧은 세부 치수를 먼저 배치하고 전체 치수를 외측 레인으로 옮긴다. 짧은 치수의 화살표는 외부로 배치한다. 치수 보조선에는 간격과 연장 길이를 적용한다.
|
||||
- 생성 치수 문자 2.8mm, EL 2.6mm, 표·일반 문자 최소 2.5mm를 용지 기준으로 관리한다. 원본 도곽 속성/NOTE 문자는 별도 편집값을 유지하므로 이 최소값의 대상이 아니다.
|
||||
- 받침 12개 초과 시 표 행 간격을 4.2mm에서 3.5mm로 줄이되 2.5mm 문자와 셀 안쪽 여백을 유지한다. 표제란·NOTE 사이의 제한된 영역을 넘기지 않도록 한다.
|
||||
- 번호가 많은 키플랜은 교각 번호·시종점·북향 문자를 함께 검사하고 필요시 간격과 리더를 조정한다. 상세 EL 문자는 실제 경계 상자로 간섭을 피한다.
|
||||
- 원본 표제란 시작 경계(841×594 기준 y=537)를 보호한다. 치수표 하단 설명까지 포함해 y=527 이내에 배치한다. 원본 셀보다 긴 편집 속성과 NOTE 초과도 검사한다.
|
||||
- `품질 검사`에서 문자/외형·문자/문자·도면 간·보호 영역 간섭을 확인하고 해당 위치를 강조하거나 결과 JSON을 저장한다. 의도된 중심 번호/단면 기호는 별도 취급한다. 수동 과대 축척은 임의 축소하지 않고 범위 초과를 보고한다.
|
||||
|
||||
문자 경계는 폰트 폭을 보수적으로 추정한 검사다. 실제 CAD 글꼴 대체까지 보장하는 완전한 간섭 해석은 아니며 선의 의도된 연결·표 격자·치수 끝점은 간섭으로 세지 않는다.
|
||||
|
||||
## DWG 출력 및 정확성
|
||||
|
||||
기존 브라우저 벡터/DXF 경로를 유지하고 로컬 서버의 `DWG 내보내기`를 추가했다. 기존 ground 가상환경과 동봉 LibreDWG를 재사용한다. 새 라이브러리·AI·외부 네트워크는 사용하지 않는다. 정적 파일 호스팅만으로는 Python 변환 경로가 실행되지 않는다.
|
||||
|
||||
DWG 2000(AC1015)으로 실제 변환한 후 다시 읽는다. R2004 변환 실험에서는 객체 유실이 확인되어 해당 경로를 사용하지 않는다. 변환기에서 수직 치수 각도를 잃는 문제는 중간 DXF의 AcDbRotatedDimension 그룹 50을 명시해 해결했다. 유효하지 않은 0.10mm 해치 선두께는 CAD 허용값 0.09mm로 수정했다.
|
||||
|
||||
다운로드 전에 다음 항목을 비교하며 불일치시 실패로 처리한다.
|
||||
|
||||
- Model/Paper별 객체 좌표·문자·해치 패턴과 경계 지문
|
||||
- 원본 부재를 추적하는 PZ_GA XDATA
|
||||
- 네이티브 치수 측정값(허용 오차 0.000001mm)
|
||||
- 레이어 선가중치, 치수 스타일, 문자 폰트, 용지와 출력 설정
|
||||
|
||||
Model Space의 정면/측면/평면은 1:1 mm의 생략 없는 형상이다. Layout1은 화면과 공유하는 편집 가능한 용지 도면 객체이며 연동 뷰포트는 아니다. 생략된 Paper 치수 표시는 선/문자로 구성하고 Model에는 실제 네이티브 DIMENSION을 저장한다. 아이소메트릭은 제목에 표시한 생략 여부를 따른다.
|
||||
|
||||
R2000에서는 RGB가 ACI로 근사된다. 재읽기 도구가 재질 사전의 잘못된 ACDBPLACEHOLDER 3개를 정리하는 내용도 검증 JSON에 기록한다. 형상·문자·치수 일치와 이 메타데이터 정리를 구분한다. **실제 AutoCAD 직접 열기·출력 검증은 미실시**이며 설치된 AutoCAD 실행 환경이 없다.
|
||||
|
||||
## 검증 자료와 사용 방법
|
||||
|
||||
`output/quality-review/comparison.html`에서 보관된 개선 전 SVG, 개선 후 SVG, 기준 DWG 렌더링을 비교한다. 다른 교각인 기준 DWG와 픽셀 일치율을 만들지 않았다. 자동 결과의 `before`는 현재 입력의 새 배치 엔진 적용 직전 검사값이며, 보관된 과거 SVG의 간섭 개수와 동일한 의미가 아니다.
|
||||
|
||||
도면작성 화면에서 교각을 선택하고 자동 축척으로 생성한 뒤 `품질 검사`와 `DWG 내보내기`를 사용한다. 내보낸 파일 옆의 `.validation.json`은 해당 DWG의 SHA-256과 검증 결과를 담는다. 검증 데이터는 `.cache/`에, 검토 산출물은 `output/quality-review/`에 보존한다.
|
||||
|
||||
Chrome 1366×768와 1920×1080에서 페이지 오류와 가로 넘침 없이 도면 선택·팬/줌·레이어·용지 변경, 도곽 편집/JSON 저장·불러오기, DWG 다운로드를 확인했다. 기존 철근/재료표/형상 탭 전환과 500mm 코핑 길이 변경 후 모델 재생성에 따른 도면 갱신도 확인했다.
|
||||
|
||||
전체 테스트 538개 중 534개 통과, 기존 실패 4건은 `axis.test.ts`, `bearing-coordinate-frame.test.ts`, `integrated-block.test.ts`, `validate-and-foundation.test.ts`이다. 이들 코어 기능은 변경하지 않았으며 커밋하지 않았다. 타입 검사와 프로덕션 빌드는 통과했다(기존 번들 크기 경고 유지).
|
||||
|
||||
도면작성 테스트 14개는 모두 통과했다. 전체 프로젝트 키플랜을 포함한 실제 샘플 교각 63개에서 배치 엔진 적용 직전 검사 339건이 최종 0건으로 줄었고, 투영 형상 변경은 0건이다. 반복 입력의 배치 일치, 강제 과대 축척의 범위 초과 검출, 의도적으로 겹친 문자 검출, 치수표 하단의 실제 표제란 경계 내 배치도 확인했다. 이 0건은 정의된 보수적 간섭 검사의 결과이며 모든 CAD 글꼴·인쇄 조건에서의 무간섭 보증은 아니다.
|
||||
|
||||
최종 운곡교 P1 및 산외교 π형 DWG는 `output/quality-review/final/`에 있다. 각각 49개/54개의 네이티브 치수를 포함하며 재읽기 최대 치수 오차는 약 7.3×10⁻¹²mm, 감사 오류는 0건이다. 객체·출처·스타일·용지 설정 비교도 통과했다. 각 파일의 `.validation.json`에 개별 결과를 보존한다.
|
||||
|
||||
## 변경 파일과 범위
|
||||
|
||||
신규 핵심 모듈: `cad-style.ts`, `dimension-graphics.ts`, `occupancy.ts`, `quality-layout.ts`, `frame-quality.ts`, `quality-panel.ts`, `dwg_export.py`, `dwg-server.ts`.
|
||||
|
||||
연결/출력/표현 변경: `drawing.ts`, `types.ts`, `presentation.ts`, `project-views.ts`, `bearing-detail.ts`, `sheet.ts`, `svg.ts`, `dxf.ts`, `workspace.ts`, `workspace.css`, `drawing.test.ts`, `.gitignore`, `README.md`.
|
||||
|
||||
이번 품질 개선의 폴더 외 수정은 `vite.config.ts`의 DWG 경로 플러그인 등록이다. 기존 `index.html`/`src/main.ts` 탭 연결 변경은 이전 도면작성 구현이며 이번에는 수정하지 않았다. 기존 철근·재료표·형상 코어를 바꾸지 않았다.
|
||||
|
||||
## 남은 한계
|
||||
|
||||
현재 63개 실제 샘플은 받침 본체/솔플레이트 상세 제원이 없어 A/B/S 등은 미확인으로 남는다. 모델에 없는 변단면 기초·모따기·도로 선형·교대와 시공 조건은 복제하지 않는다. 완전한 부재 가림 판정, 다주 교각의 모든 개별 높이 연쇄 치수, 실제 CAD 폰트·CTB 출력 검토는 추가 확인이 필요하다. 이 결과를 설계 검토 없이 완성 납품 도면으로 판정하지 않는다.
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
# 교각 일반도 자동작성
|
||||
|
||||
2026-11-04 기준 양식 보강: [REFERENCE_PARITY.md](REFERENCE_PARITY.md). 도곽 BLOCK/38개 ATTRIB, 점 치수, 원본 절단 기호, 말뚝 중심 및 제원 지시선, 도곽 개별 CAD 색상 보존을 추가했다.
|
||||
|
||||
현재 프로젝트에서 생성된 `PierModel`을 읽어 정면·측면·두부보 평면·기초 평면·기둥 A-A 단면을 벡터 도면으로 작성한다. 화면 캡처나 별도 교각 모델은 사용하지 않는다. 현재 단계는 **기존 형상 기반의 검토용 일반도**이며 최종 납품 도면의 모든 상세 표현을 완성한 단계는 아니다.
|
||||
|
||||
## 품질 개선 및 실제 DWG 출력
|
||||
|
||||
최신 변경과 검증 범위는 [QUALITY_REPORT.md](QUALITY_REPORT.md)를 참고한다. 실제 문자·치수 점유 영역 기반 자동 배치, 치수 레인, 간섭 검사 패널, DWG 2000 변환 및 재읽기 검증을 추가했다. 개선 전후/원본 비교와 검증된 DWG는 `output/quality-review/`에 있다. 생성 표/일반 문자는 최소 2.5mm, 치수 2.8mm, EL 2.6mm이며 원본 도곽/NOTE 문자는 별도 설정을 유지한다. 이번 외부 변경은 DWG 서버 경로를 등록하는 `vite.config.ts`에 한정한다.
|
||||
|
||||
## 첨부 DWG 도곽·노트·키플랜·등각도 반영 (2026-11-03)
|
||||
|
||||
첨부된 Downloads DWG와 폴더의 참고 DWG는 SHA-256이 동일함을 확인했다. 첨부가 CAD에서 열려 있어 읽기 공유 모드로 해시를 확인했으며 원본은 변경하지 않았다. 원본의 **CXBLKA1 도곽 블록**을 분해해 로고·경계·표제란의 실제 벡터 261개, 편집 가능한 ATTRIB 38개, NOTE 원문 21줄을 추출했다. 곡선은 0.05mm(A1 기준) 오차로 폴리라인화했고 로고의 해치 내부 구멍과 회색 표제란도 보존했다. 실제 데이터는 `reference-template.ts`, 재추출 도구는 `extract_template.py`이다.
|
||||
|
||||
원본의 841×594 기준 배치 체계를 A1/A2/A3에 맞춰 적용했다. 횡단면/측면은 좌측 상단, 두부보/기초 평면은 하단, ISOMETRIC VIEW와 상세 A는 우측 중간, KEY PLAN/NOTE/LEGEND/REFERENCE는 우측 끝, 받침 변화 치수표는 우측 하단, 원본 표제란은 최하단이다. 받침 높이·표고표는 도면 종류의 별도 보기와 DXF 모델 공간에서 유지한다.
|
||||
|
||||
**도곽·노트 편집 / 불러오기** 버튼에서 표제란 38개 값, 노트, 참조 도면, 문자 크기, 원본 로고/도곽 표시, 키플랜 시종점 표기를 바꿀 수 있다. `{{project}}`, `{{bridge}}`, `{{pier}}`, `{{scale}}`, `{{paper}}`를 현재 도면 값으로 치환한다. 설정은 프로젝트 이름/교량별 브라우저 저장소에 보관하며, 전체 도곽의 CAD 객체/속성 위치가 포함된 JSON으로 저장·불러올 수도 있다. 사용자 도곽은 내보낸 JSON의 프리미티브/좌표를 수정해 교체할 수 있다. **임의 DWG를 브라우저에서 직접 해석하는 기능은 아니다.** 일반 프로젝트 `.pzr` 저장 파일에 도곽 설정을 추가하는 코어 변경은 하지 않았다.
|
||||
|
||||
NOTE는 원본의 강도/시공 문구를 복사해 편집 가능한 참고 문서로 제공한다. 원본 fck=35MPa를 현 프로젝트의 재료값으로 자동 지정하거나 검증된 설계조건으로 간주하지 않는다. 편집창과 도면에 참고 원문임을 표시하며 현 프로젝트 조건에 맞게 수정한다. 노트 영역 초과 시 경고한다.
|
||||
|
||||
- **키플랜:** 현재 프로젝트에 실제 생성된 교각들의 E/N 좌표와 코핑 외곽을 사용한다. 교각 연결 방향으로 배치하고 북향 화살표도 함께 회전한다. 선택 교각을 구별하고 이름/시종점 표기를 넣는다. 연결선은 교각 중심 연결선이며, 미제공 도로 선형·교대를 추정하지 않는다.
|
||||
- **아이소메트릭:** 기존 3D 부재 프리즘과 원기둥을 벡터 등각 투영한다. 네 방향 전환과 부재별 면 음영을 지원한다. 생략 옵션을 켜면 일정 단면의 긴 구간을 잘라 표시하고 ‘생략’으로 명시한다. 원래 3D 모델은 변하지 않는다.
|
||||
- **해치:** 실제 외곽선으로 콘크리트 면을 채우고 버림/말뚝 등에 ANSI31 표현을 적용한다. 생략 구간의 해치는 실제로 잘라 빈 구간을 채우지 않는다. DXF에 네이티브 HATCH를 쓰고 로고 등 다중 경계는 내부 구멍을 유지한다. 레이어 및 해치 옵션으로 표시를 전환한다.
|
||||
- **추가 치수:** 코핑 폭/높이와 실제 끝단 높이, 기둥 지름/다주별 높이, 기초 개별 폭/길이, 말뚝 배치 간격, 받침 끝단 여유/열간 간격을 추가했다. 기존 전고·EL·받침 간격도 유지한다. 각 값은 실제 모델의 좌표/투영점으로 계산한다. 모델에 없는 모따기/변단면을 원본처럼 보이게 만들지는 않는다.
|
||||
|
||||
추가 모듈은 `template.ts`, `sheet-editor.ts`, `project-views.ts`, `hatches.ts`, `component-dimensions.ts`이며 이번 변경은 전부 `도면작성` 내부이다. 원본 도곽의 편집 값과 표시 객체는 화면/SVG/출력/DXF Layout1에서 공유한다. DXF의 정면/측면/평면 모델 공간은 생략하지 않은 실치수이며, 아이소메트릭은 제목에 명시한 표시 방식의 투영 그림이다.
|
||||
|
||||
검증: 신규 도면 테스트 14개 통과, 샘플 교각 63개 생성/원본 불변성 확인. 타입 검사·프로덕션 빌드 통과. 1366×768/1920×1080에서 도곽/노트 수정, 프로젝트별 브라우저 저장, JSON 내보내기/불러오기, 해치 전환, 아이소메트릭 방향, 교각 변경·형상 갱신·기존 탭 전환을 확인했다. 전체 테스트는 538개 중 534개 통과하며 기존 실패 4건은 유지된다. DXF 감사 오류/수정 0건이며 네이티브 치수값과 실제 도면 객체를 재비교했다. 실제 AutoCAD 열기는 미검증이다.
|
||||
|
||||
## 원본 일반도 체계 보완 (2026-11-03)
|
||||
|
||||
기둥과 말뚝의 긴 일정 단면 구간을 생략선으로 잘라 표시한다. 기본값은 생략 표현이며 왼쪽 체크박스에서 전체 길이로 전환할 수 있다. 다주 교각은 모든 기둥에 공통되는 일정 단면 구간만 생략하므로 두부보/기초 접합부를 자르지 않는다. 실제 `PierModel`·원본 도면 객체·치수값·EL은 변경하지 않는다. 정면·측면은 자동 설정에서 동일 축척을 사용하며 기둥 생략부에는 첫 기둥의 실제 지름 A-A 단면을 함께 표시한다.
|
||||
|
||||
원본 일반도의 실제 표 제목과 항목을 확인했다: **받침 및 교좌면 상세 변화 치수표 / NO., A, B, C, D, S(%), 솔플레이트 크기(교축/교직), 두께 T/T1/T2**. 이 체계를 신규 일반도의 우측에 적용하고, EL.X와 h1/h2/h3/T/E는 별도 높이·표고표 보기로 제공한다. 모든 받침을 전·후열 및 실제 번호로 구분한다. 평면도에는 원형 번호와 리더를 붙인다. 상세 A는 선택한 받침과 기존 프로그램의 순서별 짝을 그리며, 좌측 받침 선택에서 다른 번호로 전환할 수 있다. 열별 개수가 달라도 짝이 없는 받침을 임의로 연결하지 않는다.
|
||||
|
||||
- A/B/S는 `BlockModel.plan`, 솔플 크기·두께는 기존 `solplateGeometry`에서 가져온다. T1/T2는 같은 모델의 경사 규칙을 사용한다.
|
||||
- C는 전열 교좌면 축에 투영한 전·후열 중심 간격이며 전열 행에 표기한다. 꺾임과 회전은 기존 stretch와 블럭 회전을 사용한다.
|
||||
- D는 **실제 일체 교좌면이 있는 경우에만** 모델의 일체 길이를 표기한다. 분리 교좌면 및 미대응 받침에 일체 길이를 만들어 넣지 않는다.
|
||||
- h1=블럭 높이, h2=몰탈 높이, h3=받침 높이, E=h1+h2+h3+T, EL.X=몰탈 상면이다.
|
||||
- 현재 63개 샘플에는 본체·솔플레이트 계획 제원 A/B/S가 저장되어 있지 않다. 해당 값과 파생값은 **미확인**, 비해당 C/D는 **—**로 명확히 구분한다. 원본 일반도의 700/640/1298/2500 같은 특정 교각의 값을 다른 교각에 복사하지 않는다. 모델에 실제 제원이 있을 때는 자동으로 상세와 표에 반영한다.
|
||||
- 화면과 출력, DXF `Layout1`은 같은 벡터 도면 객체를 사용한다. DXF 모델 공간은 생략하지 않은 실치수 mm를 유지하고 `Layout1`은 생략 표현·상세·치수표를 용지 mm로 담는다. Layout1 치수는 실값이 적힌 표시 객체이며 모델 공간에는 네이티브 DIMENSION을 유지한다.
|
||||
|
||||
이번 보완 코드는 전부 `도면작성` 내부에 한정했다. 기존 탭 연결 코드에는 추가 변경을 하지 않았다.
|
||||
|
||||
## 실행
|
||||
|
||||
기존 프로그램을 실행하고 프로젝트/샘플을 연 다음 **철근배근 → 도면작성 → 재료표** 사이의 도면작성 탭을 선택한다. 교각을 고르면 생성된 형상을 즉시 표시한다. 기존 형상 입력을 수정했지만 재생성하지 않았다면 도면은 생성본을 유지하고 미반영 안내를 표시한다. 기존 화면에서 형상을 재생성한 뒤 돌아오면 새 모델을 자동 반영한다.
|
||||
|
||||
왼쪽에서 교각·표시 도면·용지·축척·치수/중심선/EL·레이어를 설정한다. 중앙 화면은 휠 확대·축소, 드래그 이동, 전체 맞춤, 객체 클릭 시 원본 부재 확인을 지원한다. 개별 도면 선택 시 다른 도면은 숨긴다. 상단에서 품질 검사, DXF/DWG 내보내기 및 출력 미리보기, 하단에서 SVG 저장을 실행한다. 출력은 브라우저의 인쇄/PDF 기능을 사용한다.
|
||||
|
||||
도면 종류에서 **상세 A**, **받침 및 교좌면 상세 변화 치수표**를 고르면 해당 부분만 크게 확인할 수 있다. 용지/축척 설정은 일반 투영도에 적용하며 상세는 자체 맞춤 축척, 치수표는 일정한 용지 글자 크기를 유지한다.
|
||||
|
||||
## 실제 참고 도면 분석
|
||||
|
||||
분석 대상은 이 폴더의 `[교각-T 형] 01 일반도_20260619.dwg`와 `02 배근도`이다. 기존 LibreDWG 변환기를 읽기 전용으로 사용해 `.cache`에 DXF를 만들고, ezdxf로 객체·레이어·치수 스타일·배치·문자를 분석했다. 원본 일반도를 렌더링해 실제 배치도 확인했다. 상세 결과와 원본 해시는 `reference-analysis.json`, 재실행 도구는 `analyze_reference.py`이다. 원본 DWG는 변경하지 않았다.
|
||||
|
||||
- 일반도 원본 단위는 mm(`INSUNITS=4`). 모델 공간에 치수가 다른 T형 교각 도면과 설명이 여러 개 배치되어 있다. 특정 한 교각의 치수를 템플릿으로 복제하지 않았다.
|
||||
- 상단에는 횡단 정면과 측면 단면, 하단에는 코핑·기초 평면, 우측에는 상세/주기, 최하단에는 표제란을 배치하는 체계를 사용한다.
|
||||
- 주요 레이어는 `CS-DIML`, `CS-DIMT`, `CS-CONC-REFR-HIDD`, `CS-XXXXCS-CENT`, `CX-BORD-*`이며 외형·숨은선·중심선·치수·문자를 구분한다. 신규 문서는 대응되는 의미별 레이어와 도곽/해치 레이어를 구분한다.
|
||||
- 일반도에는 DIMENSION 150개, TEXT 575개가 있다. 주요 원본 문자 높이 300/350/360, 제목 500/600은 원본 모델 공간 값이다. 신규 문자는 용지 기준 최소 2.5mm (치수 2.8mm, EL 2.6mm)를 선택한 축척에 따라 모델 공간으로 환산한다. 특정 원본의 확대 배치 치수를 그대로 적용하지 않는다.
|
||||
- 치수 스타일에 `HS_050`(dimscale=50, dimtxt=3, dimlfac=1), `1_1(typ1)` 등이 있다. 모델 형상은 실치수, 도면별 표시 축척은 별도라는 원칙을 적용한다.
|
||||
- 원본에는 기둥 절단 기호, 변단면 기초 및 모따기 등 현재 생성 모델에 없는 상세가 있다. 이를 임의로 만들지 않는다.
|
||||
- 초기 배치 분석에서는 Model의 A3와 Layout1의 Letter 메타데이터가 서로 달랐다. 후속 도곽 블록 분석에서 `CXBLKA1`의 841×594 비율과 `TD_HSCAL=S=1:200@A3` 속성을 직접 확인했다. 기본 A3 가로를 유지하되 새 모델의 배치 축척은 형상에 따라 자동 산정하고 사용자가 조정할 수 있다.
|
||||
|
||||
## 데이터 흐름과 모듈
|
||||
|
||||
`기존 state.project / state.active / state.piers[].input·model` → `DrawingSnapshot` → `projection.ts` → `drawing.ts` → `svg.ts / dxf.ts` → `workspace.ts`
|
||||
|
||||
| 파일 | 역할 |
|
||||
|---|---|
|
||||
| `types.ts` | mm 단위 도면 객체, 의미 레이어, 원본 부재 연결, 용지/축척 형식 |
|
||||
| `projection.ts` | 기존 로컬 프레임/mitre 클리핑 재사용, 부재 프리즘·원기둥의 실제 좌표 투영과 외곽선 계산 |
|
||||
| `breaks.ts` | 기둥·말뚝 공통 일정 단면 구간의 생략, 표시 좌표 변환과 생략선 |
|
||||
| `bearing-detail.ts` | 실제 받침별 변화 치수표·높이 표, 선택한 교좌면의 단면·평면 상세 |
|
||||
| `presentation.ts`, `sheet.ts` | 실치수 객체를 생략 표현으로 배치하고 화면/출력/DXF Layout1에서 공유 |
|
||||
| `drawing.ts` | 도면 배치, 중심선, 실치수 치수선, 표고, 받침 번호/간격, 기둥 단면 |
|
||||
| `svg.ts` | 동일 도면 객체의 벡터 미리보기·출력, 용지 좌표 환산 |
|
||||
| `dxf.ts` | 실치수 DXF LINE/LWPOLYLINE/CIRCLE/TEXT/DIMENSION, 레이어·선종·부재 XDATA |
|
||||
| `workspace.ts`, `workspace.css` | 독립 작업 화면, 설정, 선택, 줌/팬/맞춤, 내보내기/출력 |
|
||||
| `drawing.test.ts` | 실제 샘플과 꺾임 형상의 기하·단위·축척·원본 불변성 검증 |
|
||||
| `verify_dxf.py` | DXF 재읽기, 감사, 실치수 치수값·부재 연결 검증 |
|
||||
|
||||
외부 수정은 `index.html`의 탭/화면 컨테이너 추가와 `src/main.ts`의 상태 전달·탭 라우팅에 한정한다. 코어 형상/철근/재료표 계산은 수정하지 않았다.
|
||||
|
||||
기존 `stretchMatrix`, `mitrePlane`, `blockFramePoint`, `solplateGeometry`를 그대로 사용한다. 기존 3D 장면의 `(E-pivot.E, EL, -(N-pivot.N))` 좌표를 첫 로컬 축으로 정투영한다. 정면은 축선 방향 거리/EL, 측면은 법선 방향 거리/EL, 평면은 축선/법선 거리이다. 화면 북방향으로 임의 재회전하거나 지도 좌표를 보정하지 않는다. 기존 m 단위를 도면 mm로 환산한다. 각 객체의 교각명·부재명·인덱스는 화면 선택과 DXF `PZ_GA` XDATA로 추적한다.
|
||||
|
||||
T형·2주형(π형)·3주형, 꺾임/단차와 비대칭 받침은 생성된 모델의 실제 부재 배열에서 읽는다. 정투영 치수는 꺾인 축선의 펼친 길이와 다르므로 경고한다. 전체 외곽 치수에는 표시된 버림/말뚝 등도 포함된다. 주요 수직 치수는 첫 기둥 순높이, 첫 기초 두께, 코핑 전체 높이 및 전체 외곽 높이이며 다주 교각의 개별 높이는 형상과 EL을 함께 검토해야 한다.
|
||||
|
||||
## CAD 및 출력 기준
|
||||
|
||||
DXF 모델 공간은 **1:1 mm**이다. 도면끼리 겹치지 않도록 뷰별 평행이동만 수행하며 구조물 좌표를 용지 축척으로 줄이지 않는다. DIMENSION은 네이티브 치수 객체와 익명 표시 블록을 함께 저장한다. 치수값은 투영한 실제 두 점으로 산정한다. EL 문자는 원래 m 표고를 유지한다. A3/A2/A1 용지 배치는 화면/인쇄와 DXF `Layout1`에 공유한다. Layout1은 네이티브 선·폴리라인·원·문자로 작성한 용지 도면이며 모델 공간을 참조하는 연동 뷰포트는 아니다.
|
||||
|
||||
레이어: STRUCTURE, HIDDEN, CENTER, DIMENSION, TEXT, ELEVATION, BEARING, SECTION, HATCH, FRAME. HIDDEN은 기본 숨김이며 켜면 부재별 투영 내부 형상선을 표시한다. 완전한 부재 간 은선 제거 엔진은 아직 없다.
|
||||
|
||||
## 검증 (2026-11-03, 상세 수치·한계는 QUALITY_REPORT.md 참조)
|
||||
|
||||
- 타입 검사 통과. 프로덕션 번들 생성 통과(기존 대형 번들 경고는 유지).
|
||||
- 신규 14개 테스트 통과. 샘플의 63개 교각(T형 31, 2주형 32)을 모두 투영했다. 원본 모델 불변성, mm 환산, 치수 산정, 용지/축척 변경 시 구조 좌표 불변성, 생략 전후 실제 치수 불변성, 받침 제원이 있는 경우와 없는 경우의 표/상세를 확인했다. 도곽/노트 편집과 JSON 검증, 키플랜의 실제 교각 목록, 등각 방향, 해치 절단도 검증했다. 63개 자동 배치에서 용지 범위 초과 경고는 없었다.
|
||||
- 명시적인 검증 전용 60° 꺾임 T형/2주형/3주형으로 mitre 클리핑, 평면 기둥 수 및 유한 좌표를 확인했다. 검증 형상을 프로젝트 데이터로 저장하거나 사용하지 않는다.
|
||||
- Chrome 1366×768 / 1920×1080 화면 검증. 도면 생성, 교각 변경, 개별 도면 선택, 확대/축소/팬/맞춤, 레이어, A2/A3 변경, DXF 다운로드, 출력 미리보기, 기존 철근/재료표/형상 탭 전환을 확인했다. 페이지 오류 0건, 가로 넘침 없음.
|
||||
- 기존 코핑 길이 입력을 500mm 변경하는 브라우저 검증에서, 재생성 전에는 도면이 유지되고 미반영 안내를 표시했다. 재생성 후 도면이 자동 갱신됨을 확인했다. 검증 프로젝트는 저장하지 않았다.
|
||||
- DXF 재읽기와 감사에서 오류/수정 0건. 모든 모델 공간 치수의 측정값을 원본 도면 객체와 10⁻⁶mm 허용치로 비교했다. Layout1 객체 및 부재 XDATA, 원본 도곽과 네이티브 해치를 확인하고 실제 DXF Layout1도 별도 렌더링해 검토했다. 세로 치수의 회전각 저장 위치 오류를 재읽기 검사로 찾아 수정했다. 최신 수치는 `.cache/dxf-validation-generated-general.json`에 기록한다.
|
||||
- 전체 테스트 538개 중 534개 통과, 기존 실패 4개 유지: `axis.test.ts`, `bearing-coordinate-frame.test.ts`, `integrated-block.test.ts`, `validate-and-foundation.test.ts`. 다른 기능의 좌표/검증 규칙은 이번 범위에서 변경하지 않았다. 전체 통과가 아니므로 자동 커밋하지 않았다.
|
||||
- 실제 AutoCAD 실행 환경이 없어 **AutoCAD에서 직접 열기 검증은 미실시**. ezdxf 감사 통과와 AutoCAD 실검증을 구분한다.
|
||||
|
||||
재실행(프로젝트 루트):
|
||||
|
||||
```powershell
|
||||
npm run typecheck
|
||||
npx vitest run 도면작성/drawing.test.ts
|
||||
& ./pierzainer_ground/.venv/Scripts/python.exe 도면작성/verify_dxf.py
|
||||
npx vite build --outDir 도면작성/.cache/build
|
||||
```
|
||||
|
||||
분석·검증 캐시, 다운로드한 DXF와 미리보기는 `.cache/`에 모으고 Git에서 제외한다. 실행 소스에서 캐시를 읽지는 않는다.
|
||||
|
||||
## 후속 개발 및 한계
|
||||
|
||||
- 원본 도면의 정확한 선가중치/표제란/모든 문자 충돌 회피까지 재현하는 최종 납품용 정리는 추가 작업이다. 기둥·말뚝 생략선과 원형 받침 번호, 받침 상세/치수표는 구현되어 있다.
|
||||
- 모델에 없는 변단면 기초·모따기·철근·받침 실형상을 추정하지 않는다. 받침 계획 제원이 없는 경우 블럭/몰탈과 중심 위치만 표시하고 누락 안내를 보여 준다.
|
||||
- A-A는 첫 기둥의 일정한 원형 단면이다. 생략부에 함께 표시하고 개별 단면 보기로도 확인한다. 임의 절단 위치, 복잡한 부재 단면, 모든 원본 상세의 자동 인식은 미구현이다.
|
||||
- 부재 간 완전한 가림 판정, CAD 실제 글꼴을 포함한 완전한 간섭 판정, 다주 교각의 모든 개별 높이 연쇄 치수, 모델 공간과 연동되는 DXF 뷰포트, AutoCAD 실검증은 후속 범위이다.
|
||||
- 대호지교/감북교의 기존 현장타설말뚝 후속 화면 오류와 승천천교의 원본 방위각 누락 등 기존 샘플 제약은 그대로 유지한다. 도면작성 때문에 외부 기능을 수정하지 않았다.
|
||||
@@ -0,0 +1,46 @@
|
||||
# 기준 일반도 재현 현황 — 2026-11-04
|
||||
|
||||
기준: `[교각-T 형] 01 일반도_20260619.dwg`. 원본은 보존하고, 기존 읽기 모듈로 변환된 객체를 분석했다. 상세한 객체별 색상 분포/레이어/폰트는 `reference-style-analysis.json`에 기록했다.
|
||||
|
||||
## 이번에 실제 반영한 변경
|
||||
|
||||
- CS-CONC-MAJR: ACI 4(시안), CS-DIML/CS-XXXXCS-CENT/CS-SYMB: ACI 1(빨강), CS-DIMT 및 표 문자: ACI 3(초록). 기존 임의 네이비 색상을 제거했다. 흰 종이 미리보기의 ACI 7은 검정으로 표시한다.
|
||||
- DXF 레이어의 선가중치는 원본의 BYDEFAULT(-3)를 따른다. 원본 CTB가 제공되지 않았으므로 출력 굵기가 같다고 단정하지 않는다. 화면의 구별용 선굵기와 CAD의 원본 선가중치는 구분한다.
|
||||
- A-ISONS01, A-HIDDN01, A-CENTR01, CENTER의 원본 패턴을 등록했다. 구조 외형/치수/기호 및 숨은선에 대응한다.
|
||||
- 원본의 주된 Standard 폰트 gulim.ttc를 생성 문자에 적용했다. TB_TEXT, TB_WBTXT, ST1, 굴림체, 돋음 스타일과 원본 폰트 파일명도 등록했다. 실제 설치되지 않은 글꼴은 CAD 대체가 발생할 수 있다. 글꼴 파일을 임의 복사하지 않았다.
|
||||
- 표 문자/격자는 CS-TABL-TEX2 / CS-TABL-LIN2로 분리했다. 치수 문자와 치수선도 CS-DIMT / CS-DIML로 분리한다.
|
||||
- 콘크리트 면 채움에는 원본에서 다수 확인된 ACI 254의 회색(#cccccc)을 적용했다. 아이소메트릭의 면별 음영은 형상 가독성을 위해 유지했다.
|
||||
- 도곽 CXBLKA1에서 추출한 실제 벡터·로고·38개 속성·NOTE 21줄과 기존 편집 기능을 유지했다. 기존 현재 모델 데이터, 치수, EL, 받침 번호 및 제원표를 사용한다.
|
||||
|
||||
## 아직 동일하지 않은 항목
|
||||
|
||||
추가 구현: 원본 HS_050의 점(_Dot) 끝기호, 치수 문자 3mm, 보조선 간격 5mm, 치수 레인 9mm를 적용했다. 실제 CZSECMU-ARW2의 선·삼각 채움·문자 좌표를 모듈화해 단면 절단선에 적용했다. 기초 평면 말뚝 중심 마크와 실제 지름·본수 지시선을 추가했다. 원본 도곽 개별 객체의 CAD 레이어·색상·선가중치를 추출해 보존한다. 형상 차이에 따른 치수 레인의 실제 위치는 자동 배치되므로 원본 좌표의 복제는 아니다. 일반/표 2.5mm 및 EL 2.6mm는 가독성 기준을 유지한다.
|
||||
|
||||
원본은 여러 교각과 블록/개별 객체 색상 재정의가 섞여 있어 레이어 기본색만으로 모든 객체가 같아지지는 않는다. 도곽은 PZ_CXBLKA1 BLOCK으로 재구성하고 표제란 38개 태그를 ATTDEF/ATTRIB으로 저장한다. 빈 값도 속성으로 보존하므로 CAD 속성 편집이 가능하다. 원본 로고/곡선의 허용오차 폴리라인화는 유지한다. 모델에 없는 모따기/변단면 기초/받침 제원은 미확인으로 유지한다.
|
||||
|
||||
## 지질주상도 확장
|
||||
|
||||
`geology-extension.ts`에 출처가 있는 주상도 입력과 독립 용지 영역을 받는 인터페이스를 추가했다. 데이터가 없으면 패널을 만들지 않는다. 실제 연결 시 별도 영역 또는 추가 장에 배치하고 기존 점유 영역 검사에 포함해야 한다. 현 단계에서 허구의 주상도나 빈 공간을 채우는 그림을 생성하지 않는다.
|
||||
|
||||
## DWG 제한
|
||||
|
||||
이전 내보낸 DWG가 사용자 CAD에서 열리지 않았다는 보고가 있다. LibreDWG 재읽기 통과는 실제 AutoCAD 호환성 보증이 아니다. 이번 산출물도 내부 왕복 검증과 CAD 직접 열기 검증을 구분해야 한다. 실제 사용 CAD 이름/버전/오류 문구 또는 정상 DWG writer가 확인되기 전까지 **AutoCAD에서 열림·편집 가능 검증 완료라고 판정하지 않는다.** 원본 DWG를 변환 템플릿으로 덮어쓰지 않는다.
|
||||
|
||||
## 검증
|
||||
|
||||
도면작성 테스트 14개 통과, 타입 검사 통과. 마지막 스타일 변경 후 원시 CAD 생성 테스트를 다시 통과했다. 실제 DWG 직접 열기와 CTB를 적용한 색상·굵기 인쇄 비교는 미완료다. 기존 3D/철근/재료표 코어는 수정하지 않았다.
|
||||
|
||||
|
||||
## 2차 구현 변경 파일
|
||||
|
||||
`reference-symbols.ts`: 원본 단면 절단 기호. `dimension-graphics.ts`, `cad-style.ts`: 원본 점 치수 및 여백. `drawing.ts`: 실제 말뚝 중심·지름/본수 리더. `dxf.ts`: 도곽 BLOCK/ATTDEF/ATTRIB, _Dot 블록 및 네이티브 치수 참조. `extract_template.py`, `reference-template.ts`, `types.ts`, `svg.ts`: 원본 도곽 개별 객체의 CAD 색상/레이어 보존. `template.ts`: 빈 표제란 필드 보존. `quality-layout.ts`: 간섭 검사 캐시. `dwg_export.py`, `verify_dxf.py`: BLOCK/ATTRIB 내용과 치수 점 기호 재읽기 검사.
|
||||
|
||||
표제란 38개 속성은 CAD 원시 객체 재읽기로 확인한다. DWG 변환 후에도 블록 정의·속성값·삽입 좌표가 유지되는지 검사한다. 원본 값이 없는 시공 상세·받침 제원 및 지질주상도는 생성하지 않는다. 사용 CAD에서의 직접 열기 검증은 별도 미완료 항목이다.
|
||||
|
||||
|
||||
최종 검사에서 PZ_CXBLKA1의 ATTDEF/ATTRIB 38개, 빈 속성 유지, Paper Space 속성 소유 관계, 네이티브 DIMENSION의 _Dot 참조를 추가 확인했다. 원본 블록 좌표와 별도로 실제 치수와 표고는 현재 모델을 유지한다. 치수 문자 확대/절단 기호 때문에 달라진 점유 영역은 자동 축척·배치에 반영한다. 전체 일괄 투영 검사는 BSP 면 가림 처리와 병행 테스트 시간을 고려해 120초 한도로 실행한다.
|
||||
|
||||
실제 출력: `output/reference-parity-attributes/운곡교_P1_일반도.dwg`. 원본 모델 49개 네이티브 치수와 블록/속성의 왕복 일치, DXF 감사 오류/수정 0건을 확인했다. DWG 재읽기에서는 기존 재질 사전 placeholder 3개 정리 경고가 유지되므로 무결한 AutoCAD 출력으로 단정하지 않는다.
|
||||
|
||||
|
||||
최종 회귀 검사: 도면작성 14개 통과, 샘플 교각 63개 간섭 0건/원본 투영 형상 변경 0건. 전체 538개 중 534개 통과, 기존 코어 실패 4건(axis, bearing-coordinate-frame, integrated-block, validate-and-foundation)은 유지한다. 타입 검사·빌드 통과. 커밋하지 않았다. 실제 AutoCAD 열기/CTB 출력 검사는 미실시다.
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,46 @@
|
||||
"""Read-only DWG template analysis. Every conversion and report stays in 도면작성."""
|
||||
from collections import Counter
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
PROJECT = ROOT.parent
|
||||
sys.path.insert(0, str(PROJECT / 'pierzainer_ground'))
|
||||
from pierzainer_ground.dxf_terrain import read_source_document
|
||||
from ezdxf import bbox
|
||||
|
||||
CACHE = ROOT / '.cache'
|
||||
CACHE.mkdir(exist_ok=True)
|
||||
exe = PROJECT / 'pierzainer_ground/tools/libredwg/dwgread.exe'
|
||||
results = []
|
||||
for source in sorted(ROOT.glob('*.dwg')):
|
||||
out = CACHE / (source.stem + '.dxf')
|
||||
if not out.exists():
|
||||
process = subprocess.run([str(exe), '-O', 'DXF', '-o', str(out), str(source)], capture_output=True)
|
||||
(CACHE / (source.stem + '.conversion.log')).write_bytes(process.stdout + process.stderr)
|
||||
if process.returncode or not out.exists():
|
||||
raise RuntimeError('DWG conversion failed: ' + source.name)
|
||||
doc, _ = read_source_document(out, CACHE)
|
||||
texts = []
|
||||
dimensions = []
|
||||
for entity in doc.modelspace():
|
||||
if entity.dxftype() in ('TEXT', 'MTEXT'):
|
||||
text = entity.plain_text() if entity.dxftype() == 'MTEXT' else entity.dxf.text
|
||||
texts.append(dict(text=text, layer=entity.dxf.layer, height=float(entity.dxf.char_height if entity.dxftype() == 'MTEXT' else entity.dxf.height),
|
||||
point=list(entity.dxf.insert), rotation=float(entity.dxf.get('rotation', 0))))
|
||||
if entity.dxftype() == 'DIMENSION':
|
||||
dimensions.append(dict(style=entity.dxf.dimstyle, text=entity.dxf.get('text', ''), type=int(entity.dxf.dimtype)))
|
||||
extents = bbox.extents(doc.modelspace(), fast=True)
|
||||
result = dict(file=source.name, sha256=hashlib.sha256(source.read_bytes()).hexdigest(),
|
||||
units=doc.header.get('$INSUNITS'), entity_types=dict(Counter(e.dxftype() for e in doc.modelspace())),
|
||||
extent=[list(extents.extmin), list(extents.extmax)], texts=texts, dimensions=dimensions,
|
||||
layers=[dict(name=l.dxf.name, color=l.dxf.color, linetype=l.dxf.linetype, lineweight=l.dxf.lineweight) for l in doc.layers],
|
||||
dimstyles=[dict(name=d.dxf.name, text_height=d.dxf.get('dimtxt'), arrow_size=d.dxf.get('dimasz'), scale=d.dxf.get('dimscale'), factor=d.dxf.get('dimlfac')) for d in doc.dimstyles],
|
||||
layouts=[dict(name=l.name, attributes=l.dxf.all_existing_dxf_attribs()) for l in doc.layouts])
|
||||
results.append(result)
|
||||
print(source.name, result['entity_types'], 'texts', len(texts), 'dims', len(dimensions))
|
||||
print('HEADINGS', [t for t in texts if any(w in t['text'] for w in ('정면', '측면', '평면', '단면', '일반', '축척', 'S=', 'S =', 'EL.', 'A1', 'A3'))][:35])
|
||||
(ROOT / 'reference-analysis.json').write_text(json.dumps(results, ensure_ascii=False, indent=2, default=str), encoding='utf-8')
|
||||
@@ -0,0 +1,132 @@
|
||||
import { DIM, type PierModel } from '../src/core/geometry';
|
||||
import { pairEccentricities } from '../src/core/bearing-layout';
|
||||
import { INTEGRATED_BLOCK } from '../src/core/integrated-block';
|
||||
import { solplateGeometry } from '../src/core/solplate';
|
||||
import { stretchMatrix } from '../src/view/scene';
|
||||
import { localPoint, modelPrimitives, silhouette } from './projection';
|
||||
import type { Entity, Point, Source } from './types';
|
||||
|
||||
export type BearingScheduleRow={index:number;no:number;row:'전열'|'후열';A:number|null;B:number|null;C:number|null;D:number|null;S:number|null;L:number|null;W:number|null;T:number|null;T1:number|null;T2:number|null;elx:number;h1:number;h2:number;h3:number|null;E:number|null};
|
||||
const value=(n:number|null,digits=1)=>n===null?'미확인':n.toLocaleString('en-US',{maximumFractionDigits:digits});
|
||||
|
||||
/** All cells refer to generated model solids. No reference drawing's sample values are copied. */
|
||||
export function bearingSchedule(model:PierModel):BearingScheduleRow[] {
|
||||
const pairs=pairEccentricities(model.blocks);
|
||||
return model.blocks.map((b,index)=>{
|
||||
const p=b.plan,ib=model.integratedBlocks.find(x=>(b.row==='전열'?x.frontNo:x.backNo)===b.no);
|
||||
const pair=pairs.find(x=>b.row==='전열'&&x.frontNo===b.no);
|
||||
let C:number|null=null;
|
||||
if(pair){
|
||||
const back=model.blocks.find(x=>x.row==='후열'&&x.no===pair.backNo)!;
|
||||
const backPoint=localPoint(model,back.k,back.s,0,back.n).applyMatrix4(stretchMatrix(model,b.k).invert());
|
||||
const angle=b.rowSkewDeg*Math.PI/180;
|
||||
C=Math.abs(-(backPoint.x-b.s)*Math.sin(angle)+(backPoint.z-b.n)*Math.cos(angle))*1000;
|
||||
}
|
||||
const plate=p?solplateGeometry(b,p.h3,{A:p.A,B:p.B,margin:p.margin,T:p.T,slopePct:p.S}):undefined;
|
||||
const h1=(b.top-b.bottom)*1000,h2=(b.padTop-b.top)*1000;
|
||||
return {index,no:b.no,row:b.row,A:p?.A??null,B:p?.B??null,C,D:b.row==='전열'&&ib?INTEGRATED_BLOCK.length:null,
|
||||
S:p?.S??null,L:plate?plate.L*1000:null,W:plate?plate.W*1000:null,T:p?.T??null,T1:plate?plate.T1*1000:null,T2:plate?plate.T2*1000:null,
|
||||
elx:b.padTop,h1,h2,h3:p?.h3??null,E:p?h1+h2+p.h3+p.T:null};
|
||||
}).sort((a,b)=>(a.row===b.row?a.no-b.no:a.row==='전열'?-1:1));
|
||||
}
|
||||
|
||||
export function scheduleEntities(model:PierModel,width:number,mode:'all'|'variation'|'heights'='all'):Entity[] {
|
||||
const entities:Entity[]=[],factor=100;
|
||||
const source:Source={pier:model.name,member:'bearing-table',index:0};
|
||||
const line=(a:Point,b:Point)=>entities.push({kind:'line',a:[a[0]*factor,-a[1]*factor],b:[b[0]*factor,-b[1]*factor],layer:'TEXT',source});
|
||||
const text=(x:number,y:number,t:string,height=1.7,s=source,align:'left'|'center'='center')=>entities.push({kind:'text',point:[x*factor,-y*factor],text:t,height:height*factor,layer:'TEXT',source:s,align});
|
||||
const rows=bearingSchedule(model),rowH=rows.length>12?3.5:4.2,baseline=rowH===3.5?2.7:3.2,headH=8;
|
||||
const widths=[12,10,10,11,10,12,13,13,8,9,9],total=widths.reduce((a,b)=>a+b,0),xs=[0];
|
||||
widths.forEach(w=>xs.push(xs[xs.length-1]+w/total*width));
|
||||
// Original template: A/B/C/D/S + grouped solplate size and thickness columns.
|
||||
text(0,-3.2,'받침 및 교좌면 상세 변화 치수표',2.5,source,'left');
|
||||
const bottom=headH+rows.length*rowH;
|
||||
line([0,0],[width,0]);line([0,headH],[width,headH]);line([0,bottom],[width,bottom]);
|
||||
for(let i=0;i<xs.length;i++)line([xs[i],i===7||i===9||i===10?4:0],[xs[i],bottom]);
|
||||
line([xs[6],4],[width,4]);
|
||||
['NO./열','A','B','C','D','S(%)'].forEach((h,i)=>text((xs[i]+xs[i+1])/2,4.9,h));
|
||||
text((xs[6]+xs[8])/2,2.9,'솔플레이트 크기');text((xs[8]+xs[11])/2,2.9,'솔플레이트 두께');
|
||||
['교축 L','교직 W','T','T1','T2'].forEach((h,i)=>text((xs[i+6]+xs[i+7])/2,6.9,h,1.55));
|
||||
rows.forEach((r,i)=>{
|
||||
const y=headH+i*rowH,s={...source,index:r.index};
|
||||
if(i)line([0,y],[width,y]);
|
||||
const cells=[`${r.row==='전열'?'전':'후'} ${r.no}`,value(r.A),value(r.B),r.C===null?'—':value(r.C),r.D===null?'—':value(r.D),r.S===null?'미확인':r.S.toFixed(3),value(r.L),value(r.W),value(r.T),value(r.T1),value(r.T2)];
|
||||
cells.forEach((c,j)=>text((xs[j]+xs[j+1])/2,y+baseline,c,2.5,s));
|
||||
});
|
||||
if(mode==='variation'){
|
||||
['단위 mm · 미확인: 제원 없음 · —: 비해당 / C: 열간격 · D: 일체 길이','L=A+여유, W=B+여유, T1·T2=T±L×|S|/200'].forEach((t,i)=>text(0,bottom+4+i*4,t,2.5,source,'left'));
|
||||
return entities;
|
||||
}
|
||||
if(mode==='heights')entities.length=0;
|
||||
let y=mode==='heights'?0:bottom+4.5;
|
||||
text(0,y,'상세 A 높이·표고 (EL.X: 몰탈 상면)',2.1,source,'left');y+=2;
|
||||
const secondWidths=[.13,.19,.14,.12,.14,.12,.16],sx=[0];secondWidths.forEach(w=>sx.push(sx[sx.length-1]+w*width));
|
||||
const start=y,h=5,end=y+h+rows.length*rowH;
|
||||
for(const x of sx)line([x,start],[x,end]);line([0,start],[width,start]);line([0,start+h],[width,start+h]);
|
||||
['NO./열','EL.X(m)','h1','h2','h3','T','E'].forEach((v,j)=>text((sx[j]+sx[j+1])/2,start+3.4,v));
|
||||
rows.forEach((r,i)=>{
|
||||
const yy=start+h+i*rowH,s={...source,index:r.index};
|
||||
const cells=[`${r.row==='전열'?'전':'후'} ${r.no}`,r.elx.toFixed(3),value(r.h1),value(r.h2),value(r.h3),value(r.T),value(r.E)];
|
||||
cells.forEach((v,j)=>text((sx[j]+sx[j+1])/2,yy+baseline,v,2.5,s));line([0,yy+rowH],[width,yy+rowH]);
|
||||
});
|
||||
const notes=['단위 mm (EL만 m) · 미확인: 모델 제원 없음 · —: 비해당',
|
||||
'C: 전·후열 중심 간격(전열 교좌면 축 투영), 전열에 표기',
|
||||
'D: 일체 교좌면의 모델 길이 · 분리/미대응 받침은 —',
|
||||
'L=A+여유, W=B+여유, T1·T2=T±L×|S|/200',
|
||||
'h1 블럭 / h2 몰탈 / h3 받침 / E=h1+h2+h3+T'];
|
||||
notes.forEach((n,i)=>text(0,end+4+i*4,n,2.5,source,'left'));
|
||||
return entities;
|
||||
}
|
||||
|
||||
/** Detail A: actual selected bearing pair in its stretch frame, with a plan below. */
|
||||
export function bearingDetail(model:PierModel,scale:number,key?:string,dimensions=true,centers=true):{entities:Entity[];title:string} {
|
||||
const selected=model.blocks.find(b=>`${b.row}:${b.no}`===key)??model.blocks[0];
|
||||
if(!selected)return{entities:[],title:'상세 A · 받침 없음'};
|
||||
const pair=pairEccentricities(model.blocks).find(p=>selected.row==='전열'?p.frontNo===selected.no:p.backNo===selected.no);
|
||||
const blocks=pair?model.blocks.filter(b=>b.row==='전열'?b.no===pair.frontNo:b.no===pair.backNo):[selected];
|
||||
const indices=blocks.map(b=>model.blocks.indexOf(b));
|
||||
const integratedIndices=model.integratedBlocks.map((b,i)=>blocks.some(x=>x.row==='전열'&&x.no===b.frontNo)?i:-1).filter(i=>i>=0);
|
||||
const solids=modelPrimitives(model).solids.filter(s=>s.source.member==='integrated-block'?integratedIndices.includes(s.source.index):['block','pad','bearing','solplate'].includes(s.source.member)&&indices.includes(s.source.index));
|
||||
const anchor=blocks.find(b=>b.row==='전열')??selected,inverse=stretchMatrix(model,anchor.k).invert(),angle=anchor.rowSkewDeg*Math.PI/180;
|
||||
const frame=(p:ReturnType<typeof localPoint>)=>{const q=p.clone().applyMatrix4(inverse),s=q.x,n=q.z;return q.set(s*Math.cos(angle)+n*Math.sin(angle),q.y,-s*Math.sin(angle)+n*Math.cos(angle));};
|
||||
const local=blocks.map(b=>frame(localPoint(model,b.k,b.s,0,b.n)));
|
||||
const cx=local.reduce((sum,p)=>sum+p.z,0)/blocks.length,cy=local.reduce((sum,p)=>sum+p.x,0)/blocks.length,datum=Math.min(...blocks.map(b=>b.bottom));
|
||||
const planY=-1900,entities:Entity[]=[],source:Source={member:'bearing-detail',pier:model.name,index:model.blocks.indexOf(selected)};
|
||||
for(const solid of solids)for(const view of ['section','plan']) {
|
||||
const polygons=solid.faces.map(face=>face.map(p=>{const q=frame(p);return view==='section'?[(q.z-cx)*1000,(q.y-datum)*1000] as Point:[(q.z-cx)*1000,(q.x-cy)*1000+planY] as Point;}));
|
||||
for(const [a,b] of silhouette(polygons))entities.push({kind:'line',a,b,layer:solid.layer,source:solid.source});
|
||||
}
|
||||
const line=(a:Point,b:Point,layer:Entity['layer']='CENTER')=>entities.push({kind:'line',a,b,layer,source});
|
||||
const text=(point:Point,t:string,height=1.8*scale,align:'left'|'center'|'right'='left')=>entities.push({kind:'text',point,text:t,height,align,layer:'TEXT',source});
|
||||
const dim=(a:Point,b:Point,position:number,axis:'x'|'y')=>entities.push({kind:'dimension',a,b,position,axis,value:Math.abs(axis==='x'?a[0]-b[0]:a[1]-b[1]),height:1.7*scale,layer:'DIMENSION',source});
|
||||
blocks.forEach((block,i)=>{
|
||||
const x=(local[i].z-cx)*1000,z=(block.padTop-datum)*1000,idx=model.blocks.indexOf(block),s={member:'bearing-number',index:idx,pier:model.name};
|
||||
entities.push({kind:'circle',center:[x,z+6*scale],radius:1.7*scale,layer:'TEXT',source:s});
|
||||
entities.push({kind:'text',point:[x,z+5.2*scale],text:String(block.no),height:1.8*scale,align:'center',layer:'TEXT',source:s});
|
||||
line([x,z],[x,z+4.3*scale],'TEXT');
|
||||
text([x,z+9*scale],`${block.row} · EL.X ${block.padTop.toFixed(3)}`,1.5*scale,'center');
|
||||
if(centers)line([x,-2*scale],[x,z+4*scale]);
|
||||
text([x,planY+2*scale],`${block.row} ${block.no}`,1.65*scale,'center');
|
||||
if(dimensions){
|
||||
const p=block.plan,sign=x<0?-1:1,near=x+sign*(700+4*scale);
|
||||
const zs=[block.bottom,block.top,block.padTop,...(p?[block.padTop+p.h3/1000,block.padTop+(p.h3+p.T)/1000]:[])].map(v=>(v-datum)*1000);
|
||||
// Short height labels are led out with spacing; actual dimension values remain in the table.
|
||||
['h1','h2','h3','T'].slice(0,zs.length-1).forEach((name,j)=>{
|
||||
const yy=(zs[j]+zs[j+1])/2,target=(j*3-2)*scale;
|
||||
line([x+sign*DIM.padSize*500,yy],[near,target],'TEXT');
|
||||
text([near+sign*scale,target],`${name}=${value(zs[j+1]-zs[j])}`,1.55*scale,sign<0?'right':'left');
|
||||
});
|
||||
}
|
||||
});
|
||||
if(pair&&dimensions){const a=(local[0].z-cx)*1000,b=(local[1].z-cx)*1000;dim([a,planY],[b,planY],planY-1000,'x');text([0,planY-1000-3*scale],'C · 전/후열 중심 간격',1.6*scale,'center');}
|
||||
const ib=model.integratedBlocks.find(b=>blocks.some(x=>x.row==='전열'&&x.no===b.frontNo));
|
||||
if(ib)text([0,planY-1000-6*scale],`D=${value(INTEGRATED_BLOCK.length)} (일체 교좌면 길이)`,1.6*scale,'center');
|
||||
const plan=selected.plan;
|
||||
if(plan){
|
||||
const p=solplateGeometry(selected,plan.h3,{A:plan.A,B:plan.B,margin:plan.margin,T:plan.T,slopePct:plan.S});
|
||||
text([0,-650],`솔플 L=${value(p.L*1000)} / W=${value(p.W*1000)} · S=${plan.S.toFixed(3)}%`,1.6*scale,'center');
|
||||
text([0,-800],`T=${value(plan.T)} / T1=${value(p.T1*1000)} / T2=${value(p.T2*1000)}`,1.6*scale,'center');
|
||||
}else text([0,-650],'받침 본체·솔플레이트 제원 미확인',1.65*scale,'center');
|
||||
text([0,-1040],'교좌면 평면 · 가로 교축(n) / 세로 교직(s)',1.65*scale,'center');
|
||||
return{entities,title:`상세 A · ${blocks.map(b=>`${b.no}(${b.row==='전열'?'전':'후'})`).join(' / ')}`};
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import type { PierModel } from '../src/core/geometry';
|
||||
import type { Bounds, Entity, Point, ViewBreak } from './types';
|
||||
import { cutHatch } from './hatches';
|
||||
|
||||
/** Break only a shared, constant-section interior: never the cap or foundation interfaces. */
|
||||
export function modelBreaks(model:PierModel,scale:number): ViewBreak[] {
|
||||
const result:ViewBreak[]=[];
|
||||
for(const [member,items] of [['column',model.columns],['pile',model.piles]] as const){
|
||||
if(!items.length)continue;
|
||||
const diameter=Math.max(...items.map(c=>c.diameter))*1000;
|
||||
const bottom=Math.max(...items.map(c=>c.bottom))*1000,top=Math.min(...items.map(c=>c.top))*1000;
|
||||
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;
|
||||
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);
|
||||
}
|
||||
export function shortenedY(y:number,breaks:readonly ViewBreak[]=[]):number {
|
||||
let removed=0;
|
||||
for(const b of breaks){
|
||||
if(y<=b.from)break;
|
||||
if(y<b.to)return b.from-removed+(y-b.from)/(b.to-b.from)*b.gap;
|
||||
removed+=b.to-b.from-b.gap;
|
||||
}
|
||||
return y-removed;
|
||||
}
|
||||
export const shortenedBounds=(b:Bounds,breaks:readonly ViewBreak[]=[]):Bounds=>({...b,minY:shortenedY(b.minY,breaks),maxY:shortenedY(b.maxY,breaks)});
|
||||
|
||||
/** Ordinary entities keep full source coordinates. Only their presentation is shortened. */
|
||||
export function shortenEntity(entity:Entity,breaks:readonly ViewBreak[],clip=true):Entity[] {
|
||||
const point=(p:Point):Point=>[p[0],shortenedY(p[1],breaks)];
|
||||
if(!breaks.length)return[entity];
|
||||
if(entity.kind==='text')return[{...entity,point:point(entity.point)}];
|
||||
if(entity.kind==='circle')return[{...entity,center:point(entity.center)}];
|
||||
if(entity.kind==='dimension')throw new Error('Expand dimension graphics before presentation.');
|
||||
if(entity.kind==='hatch')return cutHatch(entity.points,breaks).map(points=>({...entity,points:points.map(point)}));
|
||||
const line=(a:Point,b:Point):Entity[]=>{
|
||||
if(!clip)return[{...entity,kind:'line',a:point(a),b:point(b)}];
|
||||
const dy=b[1]-a[1],ts=[0,1];
|
||||
if(Math.abs(dy)>1e-9)for(const cut of breaks)for(const y of [cut.from,cut.to]){const t=(y-a[1])/dy;if(t>0&&t<1)ts.push(t);}
|
||||
ts.sort((x,y)=>x-y);
|
||||
const at=(t:number):Point=>[a[0]+(b[0]-a[0])*t,a[1]+dy*t];
|
||||
return ts.slice(1).flatMap((t,i)=>{
|
||||
const middle=at((ts[i]+t)/2)[1];
|
||||
if(breaks.some(c=>middle>c.from+1e-7&&middle<c.to-1e-7))return[];
|
||||
return[{...entity,kind:'line' as const,a:point(at(ts[i])),b:point(at(t))}];
|
||||
});
|
||||
};
|
||||
if(entity.kind==='line')return line(entity.a,entity.b);
|
||||
return entity.points.slice(1).flatMap((p,i)=>line(entity.points[i],p)).concat(entity.closed?line(entity.points[entity.points.length-1],entity.points[0]):[]);
|
||||
}
|
||||
|
||||
export function breakSymbols(entities:Entity[],breaks:readonly ViewBreak[],scale:number):Entity[] {
|
||||
return entities.filter(e=>e.kind==='polyline'&&['column','pile'].includes(e.source.member)).flatMap(e=>{
|
||||
if(e.kind!=='polyline')return[];
|
||||
const xs=e.points.map(p=>p[0]),ys=e.points.map(p=>p[1]),left=Math.min(...xs),right=Math.max(...xs),width=right-left;
|
||||
return breaks.filter(b=>e.source.member===b.member&&Math.min(...ys)<b.from&&Math.max(...ys)>b.to).flatMap(b=>[b.from,b.to].map(y=>({
|
||||
kind:'polyline' as const,closed:false,source:e.source,layer:'STRUCTURE' as const,
|
||||
points:[[left-.5*scale,shortenedY(y,breaks)],[left+width*.37,shortenedY(y,breaks)],[left+width*.48,shortenedY(y,breaks)+.65*scale],
|
||||
[left+width*.58,shortenedY(y,breaks)-.65*scale],[left+width*.7,shortenedY(y,breaks)],[right+.5*scale,shortenedY(y,breaks)]] as Point[],
|
||||
})));
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/** HS_050 / 1_1(typ1): 3mm text and 1mm arrow in the reference. Values below are paper mm. */
|
||||
export const CAD_STYLE={dimensionText:3.0,elevationText:2.6,text:2.5,tableText:2.5,title:3.0,arrow:1.0,extensionGap:5,extensionOverrun:1,laneGap:9,clearance:1.2,viewGap:7};
|
||||
export const CAD_LAYERS:Record<string,string>={STRUCTURE:'CS-CONC-MAJR',HIDDEN:'CS-CONC-REFR-HIDD',CENTER:'CS-XXXXCS-CENT',DIMENSION:'CS-DIML',TEXT:'CS-DIMT',ELEVATION:'CS-DIMT',BEARING:'CS-CONC-MAJR',SECTION:'CS-SYMB',HATCH:'CS-PATT253',FRAME:'CX-BORD-LIN1'};
|
||||
|
||||
/** Verified from reference-style-analysis.json; ACI 7 is black on white paper. */
|
||||
export const REFERENCE_ACI:Record<string,number>={'CS-CONC-MAJR':4,'CS-CONC-REFR-HIDD':8,'CS-XXXXCS-CENT':1,'CS-DIML':1,'CS-DIMT':3,'CS-SYMB':1,'CS-PATT253':253,'CX-BORD-LIN1':4,'CX-BORD-TEX3':7,'CS-TABL-LIN1':4,'CS-TABL-LIN2':1,'CS-TABL-TEX2':3,'CR-BORE-TEXT':3,'CR-BORE-MISC':1};
|
||||
export const referenceLayer=(e:{layer:string;kind:string;source:{member:string};cad?:{layer:string}}):string=>{
|
||||
if(e.cad)return e.cad.layer;
|
||||
if(e.source.member==='bearing-table')return e.kind==='text'?'CS-TABL-TEX2':'CS-TABL-LIN2';
|
||||
if(e.layer==='FRAME'&&e.kind==='text')return 'CX-BORD-TEX3';
|
||||
if(e.layer==='DIMENSION'&&e.kind==='text')return 'CS-DIMT';
|
||||
return CAD_LAYERS[e.layer]??e.layer;
|
||||
};
|
||||
export const aciPaper=(aci:number):string=>({1:'#ff0000',2:'#ffff00',3:'#00ff00',4:'#00ffff',5:'#0000ff',6:'#ff00ff',7:'#000000',8:'#808080',252:'#666666',253:'#999999',254:'#cccccc'}[aci]??'#000000');
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { PierModel } from '../src/core/geometry';
|
||||
import { entityBounds, localPoint, projectPoint } from './projection';
|
||||
import type { Entity, Point, ViewId } from './types';
|
||||
|
||||
export function componentDimensions(model:PierModel,id:ViewId,geometry:Entity[],scale:number):Entity[]{
|
||||
const out:Entity[]=[],source={member:'annotation',index:0,pier:model.name};
|
||||
const dim=(a:Point,b:Point,position:number,axis:'x'|'y')=>{const value=Math.abs(axis==='x'?a[0]-b[0]:a[1]-b[1]);if(value>.001)out.push({kind:'dimension',a,b,position,axis,value,height:2*scale,layer:'DIMENSION',source});};
|
||||
const coping=geometry.filter(e=>e.source.member==='coping');
|
||||
if(coping.length&&['front','side'].includes(id)){
|
||||
const c=entityBounds(coping);dim([c.minX,c.maxY],[c.maxX,c.maxY],c.maxY+5*scale,'x');
|
||||
const points=coping.flatMap(e=>e.kind==='line'?[e.a,e.b]:e.kind==='polyline'?e.points:[]);
|
||||
const end=points.filter(p=>Math.abs(p[1]-model.endBottom*1000)<.01).sort((a,b)=>a[0]-b[0])[0];
|
||||
const level=points.filter(p=>Math.abs(p[1]-model.levelBottom*1000)<.01).sort((a,b)=>a[0]-b[0])[0];
|
||||
if(end&&level)dim(level,end,c.minX-9*scale,'y');
|
||||
const top=end?points.filter(p=>Math.abs(p[0]-end[0])<.01&&p[1]>end[1]).sort((a,b)=>b[1]-a[1])[0]:undefined;
|
||||
if(end&&top)dim(end,top,c.minX-9*scale,'y');
|
||||
model.columns.forEach((col,i)=>{const p=projectPoint(model,localPoint(model,col.k,col.s,col.bottom,0),id),r=col.diameter*500;
|
||||
dim([p[0]-r,p[1]+col.diameter*450],[p[0]+r,p[1]+col.diameter*450],p[1]+col.diameter*450,'x');
|
||||
if(i)dim([p[0]+r,col.bottom*1000],[p[0]+r,col.top*1000],p[0]+r+3*scale,'y');
|
||||
});
|
||||
}
|
||||
if(['front','side','footing'].includes(id))for(let i=0;i<model.footings.length;i++){
|
||||
const f=geometry.filter(e=>e.source.member==='footing'&&e.source.index===i);if(!f.length)continue;const b=entityBounds(f);
|
||||
dim([b.minX,b.maxY],[b.maxX,b.maxY],b.maxY+(id==='footing'?4:1)*scale,'x');
|
||||
if(id==='footing')dim([b.maxX,b.minY],[b.maxX,b.maxY],b.maxX+5*scale,'y');
|
||||
}
|
||||
if(id==='top'){
|
||||
const b=entityBounds(coping);
|
||||
for(const row of ['전열','후열']){
|
||||
const ps=model.bearings.filter(p=>p.row===row).map(p=>projectPoint(model,localPoint(model,p.k,p.s,p.z,p.n),'top')).sort((a,b)=>a[0]-b[0]);if(!ps.length)continue;
|
||||
const above=ps.reduce((s,p)=>s+p[1],0)/ps.length>(b.minY+b.maxY)/2,position=above?b.maxY+12*scale:b.minY-12*scale;
|
||||
dim([b.minX,ps[0][1]],ps[0],position,'x');dim(ps[ps.length-1],[b.maxX,ps[ps.length-1][1]],position,'x');
|
||||
}
|
||||
const front=model.bearings.find(p=>p.row==='전열'),back=model.bearings.find(p=>p.row==='후열');
|
||||
if(front&&back){const a=projectPoint(model,localPoint(model,front.k,front.s,front.z,front.n),'top'),c=projectPoint(model,localPoint(model,back.k,back.s,back.z,back.n),'top');dim(a,c,b.maxX+5*scale,'y');}
|
||||
}
|
||||
if(id==='footing'&&model.piles.length){
|
||||
const ps=model.piles.map(p=>projectPoint(model,localPoint(model,p.k,p.s,0,p.n),'footing')),b=entityBounds(geometry);
|
||||
const unique=(values:number[])=>[...new Set(values.map(n=>Math.round(n*1000)/1000))].sort((a,b)=>a-b);
|
||||
const xs=unique(ps.map(p=>p[0])),ys=unique(ps.map(p=>p[1]));
|
||||
xs.slice(1).forEach((x,i)=>dim([xs[i],b.minY],[x,b.minY],b.minY-14*scale,'x'));
|
||||
ys.slice(1).forEach((y,i)=>dim([b.minX,ys[i]],[b.minX,y],b.minX-11*scale,'y'));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { Entity,Point } from './types';
|
||||
import { CAD_STYLE } from './cad-style';
|
||||
export const formatMm=(v:number)=>v.toLocaleString('en-US',{maximumFractionDigits:1});
|
||||
export function dimensionGraphics(entity:Extract<Entity,{kind:'dimension'}>):Entity[]{
|
||||
const {a,b,axis,position,height,source,layer}=entity,s=height/CAD_STYLE.dimensionText,arrow=CAD_STYLE.arrow*s;
|
||||
const p:Point=axis==='x'?[a[0],position]:[position,a[1]],q:Point=axis==='x'?[b[0],position]:[position,b[1]],u:Point=axis==='x'?[Math.sign(q[0]-p[0])||1,0]:[0,Math.sign(q[1]-p[1])||1];
|
||||
const n:Point=[-u[1],u[0]],line=(a:Point,b:Point):Entity=>({kind:'line',a,b,source,layer});
|
||||
const ext=(a:Point,b:Point):Entity=>{const d=Math.hypot(b[0]-a[0],b[1]-a[1]),k=d?1/d:0,dx=(b[0]-a[0])*k,dy=(b[1]-a[1])*k;return line([a[0]+dx*Math.min(CAD_STYLE.extensionGap*s,d),a[1]+dy*Math.min(CAD_STYLE.extensionGap*s,d)],[b[0]+dx*s,b[1]+dy*s]);};
|
||||
const length=Math.hypot(q[0]-p[0],q[1]-p[1]),textWidth=formatMm(entity.value).length*height*.6,external=length<textWidth+2*arrow+2*s;
|
||||
const out:Entity[]=[ext(a,p),ext(b,q),line(external?[p[0]-u[0]*3*arrow,p[1]-u[1]*3*arrow]:p,external?[q[0]+u[0]*3*arrow,q[1]+u[1]*3*arrow]:q)];
|
||||
for(const point of [p,q])out.push({kind:'hatch',points:Array.from({length:16},(_,i)=>[point[0]+arrow*.5*Math.cos(i*Math.PI/8),point[1]+arrow*.5*Math.sin(i*Math.PI/8)] as Point),pattern:'SOLID',spacing:s,angle:0,color:'#ff0000',source,layer});
|
||||
out.push({kind:'text',point:[(p[0]+q[0])/2+(axis==='y'?-1.2*s:0),(p[1]+q[1])/2+(axis==='x'?1.2*s:0)],text:formatMm(entity.value),height,rotation:axis==='y'?90:0,align:'center',source,layer});
|
||||
return out;
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { readFileSync, readdirSync, mkdirSync, writeFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import { Vector3 } from 'three';
|
||||
import { buildPier, solveIp4 } from '../src/core/geometry';
|
||||
import { buildAxis, copingSpan, fromAxis, kinkArc } from '../src/core/axis';
|
||||
import type { PierInput } from '../src/core/types';
|
||||
import { generateDrawing, DEFAULT_SETTINGS } from './drawing';
|
||||
import { localPoint, projectPoint, modelPrimitives, silhouette } from './projection';
|
||||
import { drawingSvg } from './svg';
|
||||
import { exportDxf } from './dxf';
|
||||
import { bearingSchedule } from './bearing-detail';
|
||||
import { shortenedBounds } from './breaks';
|
||||
import { presentationEntities } from './presentation';
|
||||
import { defaultSheet, REFERENCE_TEMPLATE, parseSheetConfig } from './template';
|
||||
import { sheetFrame } from './sheet';
|
||||
import { cutHatch } from './hatches';
|
||||
import { viewCollisions,entityBox } from './occupancy';
|
||||
import { LAYERS, type Layer } from './types';
|
||||
|
||||
const root=resolve(import.meta.dirname,'..');
|
||||
const samples=readdirSync(resolve(root,'랜딩페이지/assets/samples')).filter(f=>/^sample\d+\.pzr\.json$/.test(f)).map(file=>({file,...JSON.parse(readFileSync(resolve(root,'랜딩페이지/assets/samples',file),'utf8'))}));
|
||||
const input: PierInput=samples.find(s=>s.file==='sample02.pzr.json').piers[0].input;
|
||||
const project={name:'검증 프로젝트',bridge:'운곡교'};
|
||||
const layers=new Set(Object.keys(LAYERS) as Layer[]);
|
||||
|
||||
describe('실제 모델 → 일반도',()=>{
|
||||
it('전체 샘플의 생성 가능한 모델을 투영하며 원본을 변경하지 않는다',()=>{
|
||||
let count=0;let piWritten=false;const checks:unknown[]=[];
|
||||
for(const sample of samples){const models=sample.piers.filter((p:{input?:PierInput})=>p.input).map((p:{input:PierInput})=>buildPier(p.input));for(const pier of sample.piers){
|
||||
if(!pier.input)continue;
|
||||
const model=buildPier(pier.input),before=JSON.stringify(model);
|
||||
const doc=generateDrawing(model,pier.input,sample.project,DEFAULT_SETTINGS,{models});
|
||||
expect(doc.quality?.geometryUnchanged).toBe(true);expect(doc.quality?.after,`${sample.file}/${model.name}: ${JSON.stringify({issues:doc.quality?.issues,table:doc.quality?.occupied.schedule})}`).toBe(0);
|
||||
if(!piWritten&&pier.input.type==='2주형'){mkdirSync(resolve(import.meta.dirname,'.cache'),{recursive:true});writeFileSync(resolve(import.meta.dirname,'.cache/generated-pi.dxf'),exportDxf(doc,layers));writeFileSync(resolve(import.meta.dirname,'.cache/generated-pi.svg'),drawingSvg(doc,layers));piWritten=true;}
|
||||
checks.push({sample:sample.file,pier:model.name,warnings:doc.warnings,quality:doc.quality,views:doc.views.map(v=>({id:v.id,scale:v.scale,breaks:v.breaks?.length??0,display:shortenedBounds(v.bounds,v.breaks),slot:v.slot}))});
|
||||
expect(doc.views.map(v=>v.id)).toEqual(['front','side','top','footing','section','detail','schedule','heights','iso','keyplan']);
|
||||
for(const view of doc.views){
|
||||
expect(view.entities.length).toBeGreaterThan(0);
|
||||
expect(Object.values(view.geometryBounds).every(Number.isFinite)).toBe(true);
|
||||
for(const e of view.entities)if(e.kind==='dimension')expect(e.value).toBeCloseTo(Math.abs(e.axis==='x'?e.b[0]-e.a[0]:e.b[1]-e.a[1]),7);
|
||||
}
|
||||
expect(JSON.stringify(model)).toBe(before);count++;
|
||||
}}
|
||||
expect(count).toBeGreaterThanOrEqual(63);
|
||||
mkdirSync(resolve(import.meta.dirname,'.cache'),{recursive:true});writeFileSync(resolve(import.meta.dirname,'.cache/sample-drawing-checks.json'),JSON.stringify(checks,null,2));
|
||||
},120000);
|
||||
|
||||
it('EL과 실제 기둥 지름을 mm로 투영하고 로컬 축과 일치한다',()=>{
|
||||
const model=buildPier(input),c=model.columns[0];
|
||||
const p=projectPoint(model,localPoint(model,c.k,c.s,c.top,0),'front');
|
||||
expect(p[1]).toBeCloseTo(c.top*1000,7);
|
||||
const base=projectPoint(model,new Vector3(model.bp1.e-model.pivot.e,0,-(model.bp1.n-model.pivot.n)),'top');
|
||||
expect(base[0]).toBeCloseTo(0,7);expect(base[1]).toBeCloseTo(0,7);
|
||||
const doc=generateDrawing(model,input,project,DEFAULT_SETTINGS);
|
||||
const section=doc.views.find(v=>v.id==='section')!;
|
||||
expect(section.entities.some(e=>e.kind==='circle'&&e.radius===c.diameter*500)).toBe(true);
|
||||
});
|
||||
|
||||
it('용지·축척 변경은 외형 좌표와 치수값을 바꾸지 않는다',()=>{
|
||||
const model=buildPier(input);
|
||||
const docs=[generateDrawing(model,input,project,DEFAULT_SETTINGS),generateDrawing(model,input,project,{...DEFAULT_SETTINGS,paper:'A1',scale:200})];
|
||||
expect(docs[0].views.slice(0,5).map(v=>v.entities.filter(e=>e.kind!=='hatch'&&e.source.member!=='annotation'&&e.source.member!=='bearing-location'))).toEqual(docs[1].views.slice(0,5).map(v=>v.entities.filter(e=>e.kind!=='hatch'&&e.source.member!=='annotation'&&e.source.member!=='bearing-location')));
|
||||
expect(docs[0].views.map(v=>v.entities.filter(e=>e.kind==='dimension').map(e=>e.value))).toEqual(docs[1].views.map(v=>v.entities.filter(e=>e.kind==='dimension').map(e=>e.value)));
|
||||
});
|
||||
|
||||
it('꺾임 교각도 기존 mitre 평면의 허용 영역만 사용한다',()=>{
|
||||
// Same explicit 60-degree test geometry as the existing bent-coping suite.
|
||||
// Verification fixture only; never used as application/project data.
|
||||
const ip1={e:0,n:0},ip3={e:9,n:-3.4641016};
|
||||
const axis=buildAxis(ip1,ip3,solveIp4(ip1,{e:1,n:0},ip3,60));
|
||||
const start=copingSpan(axis,6).sStart;
|
||||
const bearings=(['전열','후열'] as const).flatMap(row=>[.6,1.9,3.2].map((g,i)=>{
|
||||
const arc=start+g,k=arc>kinkArc(axis)!?1:0,p=fromAxis(axis,{k,s:arc,n:row==='전열'?-.7:.6});
|
||||
return {no:i+1,row,x:p.e,y:p.n,z:input.copingTopEl+.15,azimuthDeg:0};
|
||||
}));
|
||||
const bent: PierInput={...input,type:'T형',columnCount:1,copingLengthMm:6000,copingWidthMm:3100,bearings,
|
||||
ips:[{name:'IP1',x:0,y:0,angleDeg:0},{name:'IP2',x:1,y:0,angleDeg:0},{name:'IP3',x:9,y:-3.4641016,angleDeg:0},{name:'IP4',x:0,y:0,angleDeg:60}]};
|
||||
const model=buildPier(bent),primitives=modelPrimitives(model);
|
||||
expect(model.stretches.length).toBe(2);
|
||||
for(const solid of primitives.solids.filter(s=>s.source.member==='coping'))for(const face of solid.faces)for(const p of face){
|
||||
const east=p.x+model.pivot.e-model.kink!.e,north=-p.z+model.pivot.n-model.kink!.n;
|
||||
const signed=east*model.mitre!.e+north*model.mitre!.n;
|
||||
const k=model.copingParts[solid.source.index].k;
|
||||
expect(k===0?signed:-signed).toBeLessThanOrEqual(1e-7);
|
||||
}
|
||||
expect(generateDrawing(model,bent,project,DEFAULT_SETTINGS).warnings.some(w=>w.includes('정투영'))).toBe(true);
|
||||
for(const type of ['2주형','3주형'] as const){
|
||||
const multi={...bent,type,columnCount:type==='2주형'?2:3,copingLengthMm:12000};
|
||||
const m=buildPier(multi),drawing=generateDrawing(m,multi,project,DEFAULT_SETTINGS);
|
||||
expect(m.stretches).toHaveLength(2);
|
||||
expect(drawing.views.find(v=>v.id==='top')!.entities.filter(e=>e.kind==='circle'&&e.source.member==='column')).toHaveLength(m.columns.length);
|
||||
expect(drawingSvg(drawing,layers)).not.toMatch(/NaN|Infinity/);
|
||||
}
|
||||
});
|
||||
|
||||
it('프리즘의 내부 면 경계를 외곽선으로 잘못 표시하지 않는다',()=>{
|
||||
const edges=silhouette([[[0,0],[10,0],[10,10],[0,10]],[[5,0],[15,0],[15,10],[5,10]]]);
|
||||
expect(edges.length).toBeGreaterThan(3);
|
||||
expect(edges.some(([a,b])=>a[0]===b[0]&&(a[0]===5||a[0]===10))).toBe(false);
|
||||
});
|
||||
|
||||
it('생략 표현은 기둥·말뚝의 화면 길이만 줄이고 실제 좌표·치수·EL은 유지한다',()=>{
|
||||
const model=buildPier(input),before=JSON.stringify(model);
|
||||
const full=generateDrawing(model,input,project,{...DEFAULT_SETTINGS,shorten:false});
|
||||
const short=generateDrawing(model,input,project,DEFAULT_SETTINGS);
|
||||
const a=full.views[0],b=short.views[0],display=shortenedBounds(b.geometryBounds,b.breaks);
|
||||
expect(b.breaks?.some(c=>c.member==='column')).toBe(true);
|
||||
expect(b.breaks?.some(c=>c.member==='pile')).toBe(true);
|
||||
expect(display.maxY-display.minY).toBeLessThan((a.geometryBounds.maxY-a.geometryBounds.minY)*.6);
|
||||
expect(a.geometryBounds).toEqual(b.geometryBounds);
|
||||
expect(a.entities.filter(e=>e.kind==='dimension').map(e=>e.value)).toEqual(b.entities.filter(e=>e.kind==='dimension').map(e=>e.value));
|
||||
expect(presentationEntities(b).some(e=>e.entity.source.member==='column-section-inline')).toBe(true);
|
||||
expect(JSON.stringify(model)).toBe(before);
|
||||
});
|
||||
|
||||
it('참고 치수표 항목은 모델의 받침 제원·실제 높이를 사용하고 미확인 값을 만들지 않는다',()=>{
|
||||
const m=buildPier(input),rows=bearingSchedule(m);
|
||||
expect(rows).toHaveLength(m.blocks.length);expect(rows[0].A).toBeNull();expect(rows[0].T1).toBeNull();
|
||||
const b=m.blocks[0];expect(rows[0].h1).toBeCloseTo((b.top-b.bottom)*1000,7);expect(rows[0].elx).toBe(b.padTop);
|
||||
// Explicit test parameters, not application defaults or source drawing dimensions.
|
||||
const actual=buildPier({...input,bearingPlans:input.bearings.map(b=>({no:b.no,A:720,B:650,S:-2.5})),solplate:{T:35,margin:120,h3:190}});
|
||||
const r=bearingSchedule(actual)[0];
|
||||
expect(r.L).toBeCloseTo(840,8);expect(r.W).toBeCloseTo(770,8);expect(r.T1).toBeCloseTo(45.5,8);expect(r.T2).toBeCloseTo(24.5,8);
|
||||
expect(r.E).toBeCloseTo(r.h1+r.h2+190+35,8);
|
||||
expect(r.C).not.toBeNull();expect(bearingSchedule(actual).find(x=>x.row==='후열')!.C).toBeNull();
|
||||
const drawing=generateDrawing(actual,input,project,DEFAULT_SETTINGS),table=drawing.views.find(v=>v.id==='schedule')!;
|
||||
expect(table.entities.filter(e=>e.kind==='text').some(e=>e.text==='45.5')).toBe(true);
|
||||
expect(drawing.views.find(v=>v.id==='detail')!.entities.some(e=>e.source.member==='solplate')).toBe(true);
|
||||
const cd=drawing.views.find(v=>v.id==='detail')!.entities.find(e=>e.kind==='dimension');
|
||||
expect(cd?.kind==='dimension'?cd.value:null).toBeCloseTo(r.C!,6);
|
||||
const uneven={...input,bearings:input.bearings.filter(b=>!(b.row==='전열'&&b.no===5))},um=buildPier(uneven);
|
||||
expect(bearingSchedule(um)).toHaveLength(uneven.bearings.length);
|
||||
const ud=generateDrawing(um,uneven,project,{...DEFAULT_SETTINGS,detailBearing:'후열:10'});
|
||||
expect(ud.views.find(v=>v.id==='detail')!.title).toContain('10(후)');
|
||||
const ub=bearingSchedule(um).find(r=>r.no===10&&r.row==='후열')!;expect(ub.C).toBeNull();expect(ub.D).toBeNull();
|
||||
});
|
||||
|
||||
it('원시 CAD 객체와 DXF 검증 산출물을 생성한다',()=>{
|
||||
const doc=generateDrawing(buildPier(input),input,project,DEFAULT_SETTINGS);
|
||||
const svg=drawingSvg(doc,layers,true),dxf=exportDxf(doc,layers);
|
||||
expect(svg).not.toMatch(/NaN|Infinity/);expect(dxf).not.toMatch(/NaN|Infinity/);
|
||||
expect(dxf).toContain('DIMENSION');expect(dxf).toContain('PZ_GA');
|
||||
expect(drawingSvg(doc,new Set(['STRUCTURE']))).not.toContain('EL. ');
|
||||
mkdirSync(resolve(import.meta.dirname,'.cache'),{recursive:true});
|
||||
writeFileSync(resolve(import.meta.dirname,'.cache/generated-general.svg'),svg);
|
||||
writeFileSync(resolve(import.meta.dirname,'.cache/generated-general.dxf'),dxf);
|
||||
writeFileSync(resolve(import.meta.dirname,'.cache/generated-quality.json'),JSON.stringify(doc.quality,null,2));
|
||||
writeFileSync(resolve(import.meta.dirname,'.cache/generated-issues.json'),JSON.stringify(doc.quality?.issues.map(q=>({...q,a:doc.views.find(v=>v.id===q.view)?.entities[q.first],b:doc.views.find(v=>v.id===q.view)?.entities[q.second??-1]})),null,2));
|
||||
writeFileSync(resolve(import.meta.dirname,'.cache/expected-dimensions.json'),JSON.stringify(doc.views.flatMap(v=>v.entities.filter(e=>e.kind==='dimension').map(e=>e.value))));
|
||||
});
|
||||
|
||||
it('첨부 DWG의 실제 도곽/노트와 편집 값을 출력에 공유한다',()=>{
|
||||
expect(REFERENCE_TEMPLATE.sha256).toBe('156570dc5389650e20840e91c3d9a974a92c40687669f8d00aafb62073fb2b6b');
|
||||
expect(REFERENCE_TEMPLATE.fields).toHaveLength(38);expect(REFERENCE_TEMPLATE.entities.length).toBeGreaterThan(200);
|
||||
expect(REFERENCE_TEMPLATE.notes).toContain('콘크리트 기준 압축강도');
|
||||
const sheet={...defaultSheet(),notes:'현 프로젝트 검토 노트',fields:{TD_DNAME_MAIN:'일반도 · {{pier}}',TD_CCOMP:'검증 시공자'}};
|
||||
const doc=generateDrawing(buildPier(input),input,project,{...DEFAULT_SETTINGS,sheet}),frame=sheetFrame(doc);
|
||||
expect(frame.some(e=>e.kind==='text'&&e.text==='일반도 · P1')).toBe(true);
|
||||
expect(frame.some(e=>e.kind==='text'&&e.text==='현 프로젝트 검토 노트')).toBe(true);
|
||||
expect(drawingSvg(doc,layers)).toContain('검증 시공자');expect(exportDxf(doc,layers)).toContain('검증 시공자');
|
||||
expect(parseSheetConfig(JSON.parse(JSON.stringify({...sheet,template:REFERENCE_TEMPLATE})))).toMatchObject(sheet);
|
||||
expect(()=>parseSheetConfig({...sheet,noteHeight:NaN})).toThrow();
|
||||
expect(()=>parseSheetConfig({...sheet,template:{...REFERENCE_TEMPLATE,entities:[{kind:'line',a:[NaN,0],b:[0,1],layer:'FRAME',source:{}}]}})).toThrow();
|
||||
});
|
||||
|
||||
it('키플랜은 현재 프로젝트의 실제 교각만 표시하고 등각도는 모델에서 투영한다',()=>{
|
||||
const s=samples.find(s=>s.file==='sample02.pzr.json'),models=s.piers.map((p:{input:PierInput})=>buildPier(p.input));
|
||||
const before=JSON.stringify(models),doc=generateDrawing(models[0],input,s.project,DEFAULT_SETTINGS,{models});
|
||||
const key=doc.views.find(v=>v.id==='keyplan')!,iso=doc.views.find(v=>v.id==='iso')!;
|
||||
expect(key.entities.filter(e=>e.source.member==='keyplan-label')).toHaveLength(models.length);
|
||||
expect(new Set(key.entities.filter(e=>e.source.member==='keyplan-label').map(e=>e.source.pier))).toEqual(new Set(models.map((m:ReturnType<typeof buildPier>)=>m.name)));
|
||||
expect(iso.entities.some(e=>e.kind==='hatch')).toBe(true);
|
||||
const other=generateDrawing(models[0],input,s.project,{...DEFAULT_SETTINGS,isoDirection:1},{models}).views.find(v=>v.id==='iso')!;
|
||||
expect(iso.entities).not.toEqual(other.entities);expect(JSON.stringify(models)).toBe(before);
|
||||
});
|
||||
|
||||
it('콘크리트 해치는 생략부를 메우지 않고 실치수 모델은 유지한다',()=>{
|
||||
const doc=generateDrawing(buildPier(input),input,project,DEFAULT_SETTINGS),front=doc.views[0];
|
||||
expect(front.entities.some(e=>e.kind==='hatch')).toBe(true);
|
||||
const cut=front.breaks!.find(c=>c.member==='column')!;
|
||||
const column=front.entities.find(e=>e.kind==='hatch'&&e.source.member==='column');
|
||||
expect(column?.kind).toBe('hatch');
|
||||
if(column?.kind==='hatch')for(const p of cutHatch(column.points,[cut]))expect(p.every(q=>q[1]<=cut.from+1e-6)||p.every(q=>q[1]>=cut.to-1e-6)).toBe(true);
|
||||
const none=generateDrawing(buildPier(input),input,project,{...DEFAULT_SETTINGS,hatches:false});
|
||||
expect(none.views.flatMap(v=>v.entities).filter(e=>e.kind==='hatch')).toHaveLength(0);
|
||||
expect(front.entities.filter(e=>e.kind==='dimension').map(e=>e.kind==='dimension'?e.value:0)).toEqual(none.views[0].entities.filter(e=>e.kind==='dimension').map(e=>e.kind==='dimension'?e.value:0));
|
||||
});
|
||||
|
||||
it('전체 치수는 외측 레인에 배치하고 출력 문자 크기와 실제 점유 범위를 검증한다',()=>{
|
||||
const doc=generateDrawing(buildPier(input),input,project,DEFAULT_SETTINGS),front=doc.views[0];
|
||||
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);
|
||||
expect(overall.position).toBe(Math.min(...vertical.map(d=>d.position)));
|
||||
expect(doc.views.slice(0,4).every(v=>v.paperOrigin&&v.slot.width>0&&v.slot.height>0)).toBe(true);
|
||||
expect(doc.views[0].scale).toBe(doc.views[1].scale);
|
||||
const table=doc.views.find(v=>v.id==='schedule')!;
|
||||
expect(table.slot.y+table.slot.height).toBeLessThan(537*doc.paper.height/594);
|
||||
});
|
||||
|
||||
it('형상에 따라 배치가 변하고 같은 입력의 배치는 반복 가능하다',()=>{
|
||||
const model=buildPier(input),a=generateDrawing(model,input,project,DEFAULT_SETTINGS),b=generateDrawing(model,input,project,DEFAULT_SETTINGS);
|
||||
expect(a.quality).toEqual(b.quality);
|
||||
const otherInput=samples.find(s=>s.file==='sample01.pzr.json').piers.find((p:{input:PierInput})=>p.input.type==='2주형').input;
|
||||
const c=generateDrawing(buildPier(otherInput),otherInput,project,DEFAULT_SETTINGS);
|
||||
expect(c.views.slice(0,4).map(v=>v.slot)).not.toEqual(a.views.slice(0,4).map(v=>v.slot));
|
||||
const crowded=generateDrawing(model,input,project,{...DEFAULT_SETTINGS,scale:25});
|
||||
expect(crowded.views[0].scale).toBe(25);expect(crowded.quality!.issues.some(i=>i.kind==='outside'||i.kind==='view-overlap')).toBe(true);
|
||||
});
|
||||
|
||||
it('문자 회전과 실제 간섭을 검사하며 검사를 무조건 통과시키지 않는다',()=>{
|
||||
const doc=generateDrawing(buildPier(input),input,project,DEFAULT_SETTINGS),v=doc.views[0];
|
||||
const text={kind:'text' as const,point:[0,0] as [number,number],text:'123456',height:3,rotation:90,layer:'TEXT' as const,source:{pier:'P1',member:'annotation',index:0}};
|
||||
const bounds=entityBox(text);expect(bounds.maxY-bounds.minY).toBeGreaterThan(bounds.maxX-bounds.minX);
|
||||
expect(viewCollisions({...v,scale:1,entities:[text,{...text}],breaks:[]})).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
import { sectionCut } from './reference-symbols';
|
||||
import { Vector3 } from 'three';
|
||||
import type { PierInput } from '../src/core/types';
|
||||
import type { PierModel } from '../src/core/geometry';
|
||||
import { entityBounds, localPoint, modelPrimitives, projectModel, projectPoint } from './projection';
|
||||
import { modelBreaks, shortenedBounds } from './breaks';
|
||||
import { bearingDetail, scheduleEntities } from './bearing-detail';
|
||||
import { templateSlots, defaultSheet, wrappedLines } from './template';
|
||||
import { isometricEntities, keyplanEntities } from './project-views';
|
||||
import { hatchGeometry } from './hatches';
|
||||
import { componentDimensions } from './component-dimensions';
|
||||
import { optimiseLayout } from './quality-layout';
|
||||
import { formatMm } from './dimension-graphics';
|
||||
import type { Bounds, DrawingDocument, DrawingSettings, DrawingView, Entity, Point, Rect, Source, ViewId } from './types';
|
||||
|
||||
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'};
|
||||
|
||||
function slots(width: number,height: number): Record<ViewId,Rect> {
|
||||
return templateSlots(width,height);
|
||||
}
|
||||
function recommend(bounds: Bounds,slot: Rect,id: ViewId): number {
|
||||
const side=id==='front'||id==='side';
|
||||
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[] {
|
||||
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});
|
||||
const dim=(a:Point,c:Point,position:number,axis:'x'|'y')=>out.push({kind:'dimension',a,b:c,position,axis,height:h,value:Math.abs(axis==='x'?c[0]-a[0]:c[1]-a[1]),source,layer:'DIMENSION'});
|
||||
if(settings.dimensions) {
|
||||
dim([b.minX,b.minY],[b.maxX,b.minY],b.minY-(id==='top'?22:7)*scale,'x');
|
||||
dim([b.minX,b.minY],[b.minX,b.maxY],b.minX-5*scale,'y');
|
||||
}
|
||||
if(id==='section') {
|
||||
if(settings.centers) { line([b.minX-3*scale,0],[b.maxX+3*scale,0]);line([0,b.minY-3*scale],[0,b.maxY+3*scale]); }
|
||||
text([0,b.maxY+4*scale],`Ø${formatMm(model.columns[0].diameter*1000)}`,'SECTION','center');
|
||||
return settings.hatches===false?out.filter(e=>e.kind!=='hatch'):out;
|
||||
}
|
||||
if(settings.centers) {
|
||||
const seen=new Set<string>();
|
||||
model.columns.forEach((c,i)=>{
|
||||
const p=projectPoint(model,localPoint(model,c.k,c.s,0,0),id),r=c.diameter*500;
|
||||
const identity=p[0].toFixed(3);if((id==='front'||id==='side')&&seen.has(identity))return;seen.add(identity);
|
||||
if(id==='front'||id==='side')line([p[0],c.bottom*1000-3*scale],[p[0],c.top*1000+3*scale]);
|
||||
else {line([p[0]-r-3*scale,p[1]],[p[0]+r+3*scale,p[1]]);line([p[0],p[1]-r-3*scale],[p[0],p[1]+r+3*scale]);}
|
||||
});
|
||||
if(id==='top') {
|
||||
const world=[model.bp1,...(model.kink?[model.kink]:[]),model.bp2];
|
||||
world.slice(1).forEach((p,i)=>line(projectPoint(model,new Vector3(world[i].e-model.pivot.e,0,-(world[i].n-model.pivot.n)),id),projectPoint(model,new Vector3(p.e-model.pivot.e,0,-(p.n-model.pivot.n)),id)));
|
||||
}
|
||||
}
|
||||
if(id==='front'||id==='side') {
|
||||
if(settings.dimensions) {
|
||||
const c=model.columns[0],p=projectPoint(model,localPoint(model,c.k,c.s,0,0),id);
|
||||
dim([p[0],c.bottom*1000],[p[0],c.top*1000],b.minX-9*scale,'y');
|
||||
const f=model.footings[0];
|
||||
if(f)dim([b.minX,f.bottom*1000],[b.minX,f.top*1000],b.minX-9*scale,'y');
|
||||
const elevations=model.copingParts.flatMap(part=>part.profile.map(p=>p[1]*1000));
|
||||
dim([b.minX,Math.min(...elevations)],[b.minX,Math.max(...elevations)],b.minX-13*scale,'y');
|
||||
}
|
||||
if(settings.elevations) {
|
||||
const els=[model.copingRefTop,model.levelBottom,...model.columns.map(c=>c.bottom),...model.footings.map(f=>f.bottom),...model.piles.map(p=>p.bottom)];
|
||||
const unique=[...new Set(els.map(v=>Math.round(v*1e6)/1e6))].sort((a,c)=>a-c);
|
||||
let previous=-Infinity;
|
||||
for(const el of unique) {
|
||||
const y=el*1000,labelY=Math.max(y,previous+3.4*scale);previous=labelY;
|
||||
line([b.maxX+scale,y],[b.maxX+3*scale,labelY],'ELEVATION');
|
||||
out.push({kind:'polyline',points:[[b.maxX,y],[b.maxX+1.3*scale,y+scale],[b.maxX+1.3*scale,y-scale]],closed:true,layer:'ELEVATION',source});
|
||||
text([b.maxX+3.5*scale,labelY+.3*scale],`EL. ${el.toFixed(3)}`,'ELEVATION');
|
||||
}
|
||||
}
|
||||
if(id==='front'&&model.columns.length>1&&settings.dimensions) {
|
||||
const xs=model.columns.map(c=>projectPoint(model,localPoint(model,c.k,c.s,c.bottom,0),id)[0]).sort((a,c)=>a-c);
|
||||
xs.slice(1).forEach((x,i)=>dim([xs[i],b.minY],[x,b.minY],b.minY-16*scale,'x'));
|
||||
}
|
||||
if(id==='front') {
|
||||
const c=model.columns[0],p=projectPoint(model,localPoint(model,c.k,c.s,(c.top+c.bottom)/2,0),id),radius=c.diameter*500;
|
||||
const cut=settings.shorten===false?undefined:modelBreaks(model,scale).find(b=>b.member==='column');
|
||||
if(cut&&p[1]>cut.from&&p[1]<cut.to)p[1]=cut.to+.3*c.diameter*1000;
|
||||
line([p[0]-radius-3*scale,p[1]],[p[0]+radius+3*scale,p[1]],'SECTION');
|
||||
out.push(...sectionCut([p[0]-radius-3*scale,p[1]],scale,'A',false,source),...sectionCut([p[0]+radius+3*scale,p[1]],scale,'A',true,source));
|
||||
}
|
||||
}
|
||||
if(id==='top') {
|
||||
model.bearings.forEach((bearing,i)=>{
|
||||
const p=projectPoint(model,localPoint(model,bearing.k,bearing.s,bearing.z,bearing.n),id);
|
||||
const s={member:'bearing-location',index:i,pier:model.name};
|
||||
const rowMean=model.bearings.filter(x=>x.row===bearing.row).reduce((sum,x)=>sum+projectPoint(model,localPoint(model,x.k,x.s,x.z,x.n),id)[1],0)/model.bearings.filter(x=>x.row===bearing.row).length;
|
||||
const upper=rowMean>(b.minY+b.maxY)/2,labelY=upper?b.maxY+5*scale:b.minY-5*scale;
|
||||
out.push({kind:'line',a:p,b:[p[0],labelY+(upper?-1.7:1.7)*scale],layer:'TEXT',source:s});
|
||||
out.push({kind:'circle',center:[p[0],labelY],radius:1.7*scale,layer:'TEXT',source:s});
|
||||
out.push({kind:'text',point:[p[0],labelY-.8*scale],text:String(bearing.no),height:1.8*scale,align:'center',layer:'TEXT',source:s});
|
||||
});
|
||||
if(settings.dimensions) for(const row of ['전열','후열']) {
|
||||
const points=model.bearings.filter(p=>p.row===row).map(p=>projectPoint(model,localPoint(model,p.k,p.s,p.z,p.n),id)).sort((a,c)=>a[0]-c[0]);
|
||||
const mean=points.reduce((sum,p)=>sum+p[1],0)/Math.max(1,points.length),above=mean>(b.minY+b.maxY)/2;
|
||||
points.slice(1).forEach((p,i)=>dim(points[i],p,(above?b.maxY+12*scale:b.minY-12*scale),'x'));
|
||||
}
|
||||
}
|
||||
if(id==='footing'&&model.piles.length){
|
||||
if(settings.centers)for(const pile of model.piles){
|
||||
const p=projectPoint(model,localPoint(model,pile.k,pile.s,0,pile.n),id),r=pile.diameter*500+1.5*scale;
|
||||
line([p[0]-r,p[1]],[p[0]+r,p[1]]);line([p[0],p[1]-r],[p[0],p[1]+r]);
|
||||
}
|
||||
const pile=model.piles[0],p=projectPoint(model,localPoint(model,pile.k,pile.s,0,pile.n),id),elbow:Point=[b.minX,b.minY-6*scale],end:Point=[elbow[0]+3*scale,elbow[1]];
|
||||
line(p,elbow,'SECTION');line(elbow,end,'SECTION');
|
||||
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}본`);
|
||||
}
|
||||
if(settings.dimensions)out.push(...componentDimensions(model,id,geometry??projectModel(model,id),scale));
|
||||
return settings.hatches===false?out.filter(e=>e.kind!=='hatch'):out;
|
||||
}
|
||||
|
||||
export function generateDrawing(model: PierModel,input: PierInput,project: {name:string;bridge:string},settings: DrawingSettings,context?:{models:readonly PierModel[]}): DrawingDocument {
|
||||
const size={A3:[420,297],A2:[594,420],A1:[841,594]}[settings.paper];
|
||||
const paper={name:settings.paper,width:size[0],height:size[1]},positions=slots(...size as [number,number]);
|
||||
const primitives=modelPrimitives(model),warnings: string[]=[];
|
||||
if(!model.columns.length)throw new Error('모델에 기둥 형상이 없습니다.');
|
||||
const missing=model.blocks.filter(b=>!b.plan).length;
|
||||
if(missing)warnings.push(`받침 ${missing}/${model.blocks.length}개의 본체·솔플레이트 제원이 없습니다. 해당 치수표는 미확인으로, 형상은 모델의 블럭·몰탈·중심으로 표시합니다.`);
|
||||
if(model.kink)warnings.push('꺾임 교각의 전체폭·간격 치수는 정투영 거리입니다. 축선 실장은 별도 확인하세요.');
|
||||
const views: DrawingView[]=(['front','side','top','footing','section'] as ViewId[]).map(id=>{
|
||||
const source={member:'column',index:0,pier:model.name};
|
||||
const geometry: Entity[]=id==='section'?[{kind:'circle',center:[0,0],radius:model.columns[0].diameter*500,layer:'STRUCTURE',source}]:projectModel(model,id,primitives);
|
||||
const geometryBounds=entityBounds(geometry),slot=positions[id],wide=id==='front'||id==='side';
|
||||
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 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};
|
||||
});
|
||||
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)];
|
||||
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};
|
||||
}
|
||||
}
|
||||
let detailScale=50,detail=bearingDetail(model,detailScale,settings.detailBearing,settings.dimensions,settings.centers);
|
||||
for(let i=0;i<3;i++){
|
||||
const b=entityBounds(detail.entities),s=positions.detail,need=Math.max((b.maxX-b.minX)/(s.width-6),(b.maxY-b.minY)/(s.height-12));
|
||||
detailScale=SCALES.find(x=>x>=need)??200;detail=bearingDetail(model,detailScale,settings.detailBearing,settings.dimensions,settings.centers);
|
||||
}
|
||||
const db=entityBounds(detail.entities);
|
||||
views.push({id:'detail',title:detail.title,entities:detail.entities,geometryBounds:db,bounds:{minX:db.minX-50,maxX:db.maxX+50,minY:db.minY-100,maxY:db.maxY+100},scale:detailScale,slot:positions.detail});
|
||||
const table=scheduleEntities(model,positions.schedule.width,'variation'),tb=entityBounds(table);
|
||||
// Table text is paper-sized; increase its allotted height or sheet size rather than silently omit rows.
|
||||
views.push({id:'schedule',title:VIEW_TITLES.schedule,entities:table,geometryBounds:tb,bounds:tb,scale:100,slot:positions.schedule});
|
||||
const heights=scheduleEntities(model,positions.heights.width,'heights'),hb=entityBounds(heights);
|
||||
views.push({id:'heights',title:VIEW_TITLES.heights,entities:heights,geometryBounds:hb,bounds:hb,scale:100,slot:positions.heights});
|
||||
const iso=isometricEntities(model,false,settings.isoDirection??0),ib=entityBounds(iso),islot=positions.iso;
|
||||
const iscale=SCALES.find(s=>s>=Math.max((ib.maxX-ib.minX)/(islot.width-4),(ib.maxY-ib.minY)/(islot.height-8)))??2000;
|
||||
views.push({id:'iso',title:'ISOMETRIC VIEW',entities:settings.hatches===false?iso.filter(e=>e.kind!=='hatch'):iso,geometryBounds:ib,bounds:ib,scale:iscale,slot:islot});
|
||||
const cfg=settings.sheet??defaultSheet(),models=context?.models??[model];let keyscale=1000,key=keyplanEntities(models,model,keyscale,cfg.startLabel,cfg.endLabel);
|
||||
for(let i=0;i<3;i++){const b=entityBounds(key),s=positions.keyplan;keyscale=Math.max(100,Math.ceil(Math.max((b.maxX-b.minX)/(s.width-8),(b.maxY-b.minY)/(s.height-8))/100)*100);key=keyplanEntities(models,model,keyscale,cfg.startLabel,cfg.endLabel);}
|
||||
const kb=entityBounds(key);views.push({id:'keyplan',title:'KEY PLAN',entities:key,geometryBounds:kb,bounds:kb,scale:keyscale,slot:positions.keyplan});
|
||||
if(settings.hatches!==false)for(const v of views.filter(v=>['front','side','top','footing','section','detail'].includes(v.id)))v.entities=[...hatchGeometry(v.entities,v.scale),...v.entities];
|
||||
if(wrappedLines(cfg.notes,133,cfg.noteHeight).length*cfg.noteHeight*1.65>155)warnings.push('NOTE 원문이 지정 영역을 넘습니다. 노트 편집에서 문자 높이/내용을 조정하세요.');
|
||||
const doc:DrawingDocument={project:project.name,bridge:project.bridge,pier:model.name,units:'mm',views,paper,warnings,settings:{...settings},notes:[
|
||||
`${input.type} · 기둥 ${model.columns.length}개 · ${input.foundationType}`,
|
||||
`받침 전열 ${model.bearings.filter(b=>b.row==='전열').length} / 후열 ${model.bearings.filter(b=>b.row==='후열').length}`,
|
||||
`기준 E ${model.pivot.e.toFixed(3)}`,`기준 N ${model.pivot.n.toFixed(3)}`,
|
||||
'치수 mm / 표고 EL m','정면 기준: 첫 교각 로컬 축','A-A: 첫 기둥의 동일 원형 단면',
|
||||
settings.shorten===false?'기둥·말뚝 전체 길이 표시':'기둥·말뚝 생략 표현 / 치수·EL은 실값',
|
||||
'모델에 없는 변단면·모따기 생략','숨은선은 개별 부재의 형상선',
|
||||
]};
|
||||
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))];
|
||||
else if(id==='detail')entities=bearingDetail(model,scale,settings.detailBearing,settings.dimensions,settings.centers).entities;
|
||||
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];
|
||||
return{...original,entities:entities.map(e=>({...e})),scale,breaks,paperOrigin:undefined};
|
||||
});
|
||||
return doc;
|
||||
}
|
||||
|
||||
export { dimensionGraphics, formatMm } from './dimension-graphics';
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { Plugin } from 'vite';
|
||||
import type { IncomingMessage,ServerResponse } from 'node:http';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { mkdir,readFile,writeFile } from 'node:fs/promises';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import path from 'node:path';
|
||||
export const DWG_API='/__pierzainer/general-arrangement/dwg';
|
||||
|
||||
/** A narrow local export route; no project state or existing drawing-extractor endpoints are changed. */
|
||||
export function generalArrangementDwgPlugin(root:string):Plugin{
|
||||
let active=0;
|
||||
const handler=async(req:IncomingMessage,res:ServerResponse)=>{
|
||||
const error=(status:number,message:string)=>{res.statusCode=status;res.setHeader('Content-Type','application/json; charset=utf-8');res.end(JSON.stringify({error:message}));};
|
||||
if(req.method!=='POST'){error(405,'DWG 출력은 POST 요청만 지원합니다.');return;}
|
||||
try{if(req.headers.origin&&new URL(req.headers.origin).host!==req.headers.host){error(403,'현재 프로그램에서만 요청할 수 있습니다.');return;}}catch{error(403,'잘못된 요청 출처입니다.');return;}
|
||||
if(active>=2){error(429,'다른 도면을 변환 중입니다. 완료 후 다시 실행하세요.');return;}
|
||||
active++;
|
||||
try{
|
||||
const chunks:Buffer[]=[];let size=0;
|
||||
for await(const chunk of req){const b=Buffer.from(chunk);size+=b.length;if(size>20*1024*1024)throw new Error('도면 크기가 20MB를 넘습니다.');chunks.push(b);}
|
||||
const body=JSON.parse(Buffer.concat(chunks).toString('utf8')) as {dxf?:unknown};
|
||||
if(typeof body.dxf!=='string'||!body.dxf.includes('SECTION')||!body.dxf.trim().endsWith('EOF'))throw new Error('유효한 DXF 도면 데이터가 필요합니다.');
|
||||
const python=path.join(root,'pierzainer_ground','.venv','Scripts','python.exe'),script=path.join(root,'도면작성','dwg_export.py');
|
||||
if(!existsSync(python))throw new Error('DWG 출력용 기존 Python 실행 환경을 찾을 수 없습니다.');
|
||||
const folder=path.join(root,'도면작성','output',randomUUID());await mkdir(folder,{recursive:true});
|
||||
const source=path.join(folder,'source.dxf'),output=path.join(folder,'general.dwg');await writeFile(source,body.dxf,'utf8');
|
||||
const result=await new Promise<{code:number;stdout:string;stderr:string}>((resolve,reject)=>{
|
||||
const child=spawn(python,['-X','utf8',script,source,output],{cwd:root,windowsHide:true,stdio:['ignore','pipe','pipe']});
|
||||
let stdout='',stderr='';child.stdout.on('data',b=>stdout+=b);child.stderr.on('data',b=>stderr+=b);child.on('error',reject);
|
||||
const timeout=setTimeout(()=>{child.kill();reject(new Error('DWG 변환 제한 시간(120초)을 넘었습니다.'));},120000);
|
||||
child.on('exit',code=>{clearTimeout(timeout);resolve({code:code??-1,stdout,stderr});});
|
||||
});
|
||||
const report=JSON.parse(result.stdout.trim().split(/\r?\n/).at(-1)??'{}');
|
||||
if(result.code!==0||report.status!=='validated')throw new Error(report.message||'DWG 왕복 검증을 통과하지 못했습니다.');
|
||||
const bytes=await readFile(output);res.statusCode=200;res.setHeader('Content-Type','application/octet-stream');res.setHeader('Content-Disposition','attachment; filename="general.dwg"');res.setHeader('X-Pierzainer-Validation',encodeURIComponent(JSON.stringify(report)));res.end(bytes);
|
||||
}catch(e){error(422,(e as Error).message);}finally{active--;}
|
||||
};
|
||||
return{name:'pierzainer-general-arrangement-dwg',configureServer(server){server.middlewares.use(DWG_API,(req,res)=>void handler(req,res));},configurePreviewServer(server){server.middlewares.use(DWG_API,(req,res)=>void handler(req,res));}};
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
"""Validated DWG 2000 writer using the project's existing LibreDWG tools.
|
||||
|
||||
No source/project files are modified. Conversion intermediates and reports stay beside the requested output.
|
||||
"""
|
||||
from collections import Counter
|
||||
import contextlib
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import ezdxf
|
||||
from ezdxf import recover, colors
|
||||
|
||||
ROOT=Path(__file__).resolve().parent
|
||||
TOOLS=ROOT.parent/'pierzainer_ground/tools/libredwg'
|
||||
|
||||
def compatibility_angles(path):
|
||||
# LibreDWG imports the linear angle from AcDbRotatedDimension whereas standard
|
||||
# DXF places it in AcDbAlignedDimension. Mirror the value in the converter input only.
|
||||
lines=path.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)
|
||||
output=[]
|
||||
for group in groups:
|
||||
angle=next((v for c,v in group if c.strip()=='50'),'0')
|
||||
for c,v in group:
|
||||
output.extend([c,v])
|
||||
if group[0][1]=='DIMENSION' and c.strip()=='100' and v=='AcDbRotatedDimension':output.extend([' 50',angle])
|
||||
path.write_text('\n'.join(output)+'\n',encoding='cp949')
|
||||
|
||||
def fingerprint(entity):
|
||||
def point(p):return tuple(round(float(v),5) for v in p)
|
||||
d=entity.dxf;t=entity.dxftype();link=tuple((tag.code,tag.value) for tag in entity.get_xdata('PZ_GA')) if entity.has_xdata('PZ_GA') else ();base=(t,d.layer,link)
|
||||
if t=='LINE':return base+(point(d.start),point(d.end))
|
||||
if t=='CIRCLE':return base+(point(d.center),round(d.radius,5))
|
||||
if t=='LWPOLYLINE':return base+(bool(entity.closed),tuple(point(p[:2]) for p in entity.get_points()))
|
||||
if t=='TEXT':return base+(d.text,point(d.insert),round(d.height,5),round(d.get('rotation',0),5),d.get('style','STANDARD').casefold())
|
||||
if t=='DIMENSION':return base+(round(float(entity.get_measurement()),5),point(d.defpoint2),point(d.defpoint3))
|
||||
if t in ('ATTDEF','ATTRIB'):return base+(d.tag,d.text,point(d.insert),round(d.height,5))
|
||||
if t=='INSERT':return base+(d.name,point(d.insert),tuple(fingerprint(a) for a in entity.attribs))
|
||||
if t=='HATCH':return base+(d.pattern_name,tuple(tuple(point(p[:2]) for p in loop.vertices) for loop in entity.paths if hasattr(loop,'vertices')),
|
||||
tuple((round(line.angle,5),point(line.base_point),point(line.offset),tuple(round(v,5) for v in line.dash_length_items)) for line in entity.pattern.lines) if entity.pattern else ())
|
||||
return base
|
||||
|
||||
def convert(source,output):
|
||||
source=Path(source).resolve();output=Path(output).resolve()
|
||||
if output.suffix.lower()!='.dwg' or not output.is_relative_to(ROOT):raise ValueError('DWG output must stay inside 도면작성')
|
||||
if output.exists():raise ValueError('Existing DWG is preserved; choose another output file')
|
||||
output.parent.mkdir(parents=True,exist_ok=True)
|
||||
original=ezdxf.readfile(source);audit=original.audit()
|
||||
if audit.errors:raise ValueError('Input DXF audit failed')
|
||||
doc=original
|
||||
# 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
|
||||
for entity in doc.entitydb.values():
|
||||
if entity.is_alive and getattr(entity,'rgb',None):
|
||||
rgb=entity.rgb
|
||||
nearest=min(range(1,256),key=lambda i:sum((a-b)**2 for a,b in zip(rgb,colors.aci2rgb(i))))
|
||||
entity.dxf.color=nearest;entity.dxf.discard('true_color');colour_count+=1
|
||||
doc.dxfversion='AC1015';doc.encoding='cp949';doc.header['$DWGCODEPAGE']='ANSI_949'
|
||||
intermediate=output.with_suffix('.conversion.dxf');doc.saveas(intermediate)
|
||||
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)
|
||||
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)]]:
|
||||
result=subprocess.run(args,capture_output=True,timeout=90)
|
||||
logs.append(result.stderr.decode('utf-8','replace')[-12000:])
|
||||
if not output.exists() or output.stat().st_size<100:raise RuntimeError('DWG writer did not create a valid file')
|
||||
if output.read_bytes()[:6]!=b'AC1015':raise ValueError('Invalid DWG 2000 signature')
|
||||
roundtrip,reader_audit=recover.readfile(output.with_suffix('.roundtrip.dxf'))
|
||||
audit=roundtrip.audit()
|
||||
if audit.errors:raise ValueError('DWG round-trip audit failed')
|
||||
actual={layout.name:Counter(fingerprint(e) for e in layout) for layout in roundtrip.layouts}
|
||||
differences=[]
|
||||
for name,wanted in expected.items():
|
||||
got=actual.get(name,Counter())
|
||||
if wanted!=got:differences.append(dict(layout=name,missing=sum((wanted-got).values()),extra=sum((got-wanted).values())))
|
||||
if differences:raise ValueError('DWG round-trip changed objects: '+json.dumps(differences))
|
||||
for name,wanted in expected_blocks.items():
|
||||
if name not in roundtrip.blocks or Counter(fingerprint(e) for e in roundtrip.blocks[name])!=wanted:raise ValueError('DWG block/attribute definition changed: '+name)
|
||||
source_dimensions=sorted(float(e.get_measurement()) for e in doc.modelspace().query('DIMENSION'))
|
||||
target_dimensions=sorted(float(e.get_measurement()) for e in roundtrip.modelspace().query('DIMENSION'))
|
||||
max_error=max((abs(a-b) for a,b in zip(source_dimensions,target_dimensions)),default=0)
|
||||
if max_error>1e-6:raise ValueError('DWG dimension measurement differs by more than 0.000001mm')
|
||||
for layer in doc.layers:
|
||||
target=roundtrip.layers.get(layer.dxf.name)
|
||||
if layer.dxf.lineweight!=target.dxf.lineweight:raise ValueError('DWG layer lineweight changed')
|
||||
for style in doc.dimstyles:
|
||||
target=roundtrip.dimstyles.get(style.dxf.name)
|
||||
for key in ['dimtxt','dimasz','dimlfac','dimscale','dimexe','dimexo']:
|
||||
if abs(float(style.dxf.get(key,0))-float(target.dxf.get(key,0)))>1e-6:raise ValueError('DWG dimension style changed: '+style.dxf.name+'/'+key)
|
||||
for style in doc.styles:
|
||||
if style.dxf.font.casefold()!=roundtrip.styles.get(style.dxf.name).dxf.font.casefold():raise ValueError('DWG text font changed')
|
||||
for layout in doc.layouts:
|
||||
target=roundtrip.layouts.get(layout.name)
|
||||
for key in ['paper_width','paper_height','scale_numerator','scale_denominator','plot_layout_flags']:
|
||||
if layout.dxf.get(key)!=target.dxf.get(key):raise ValueError('DWG paper/plot setting changed: '+key)
|
||||
report=dict(status='validated',format='DWG 2000 / AC1015',sha256=hashlib.sha256(output.read_bytes()).hexdigest(),
|
||||
layouts={l.name:len(l) for l in roundtrip.layouts},dimensions=len(roundtrip.modelspace().query('DIMENSION')),
|
||||
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)
|
||||
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
|
||||
|
||||
if __name__=='__main__':
|
||||
try:print(json.dumps(convert(sys.argv[1],sys.argv[2]),ensure_ascii=False))
|
||||
except Exception as error:
|
||||
print(json.dumps(dict(status='error',message=str(error)),ensure_ascii=False));sys.exit(1)
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
import { dimensionGraphics } from './dimension-graphics';
|
||||
import { LAYERS, type DrawingDocument, type Entity, type Layer, type Point } from './types';
|
||||
import { sheetFrame, viewPaperEntities } from './sheet';
|
||||
import { CAD_LAYERS, CAD_STYLE, REFERENCE_ACI, referenceLayer } from './cad-style';
|
||||
|
||||
/** Native CAD entities in millimetres. Sheet scale affects annotations, never structural coordinates or measurements. */
|
||||
export function exportDxf(doc: DrawingDocument,visible: ReadonlySet<Layer>): string {
|
||||
let next=256;
|
||||
const handle=()=> (next++).toString(16).toUpperCase();
|
||||
const root=handle(),groups=handle(),layoutDictionary=handle(),modelLayout=handle(),paperLayout=handle(),modelRecord=handle(),paperRecord=handle(),textStyle=handle(),dimTextStyle=handle();
|
||||
const pairs=(...values:(string|number)[])=>values.map((v,i)=>i%2?String(v).replace(/[\r\n]/g,' '):String(v)).join('\r\n')+'\r\n';
|
||||
const section=(name:string,value:string)=>pairs(0,'SECTION',2,name)+value+pairs(0,'ENDSEC');
|
||||
const table=(name:string,entries:string[],extra='')=>pairs(0,'TABLE',2,name,5,handle(),100,'AcDbSymbolTable',70,entries.length)+extra+entries.join('')+pairs(0,'ENDTAB');
|
||||
const xdata=(e: Entity,view:string)=>pairs(1001,'PZ_GA',1000,e.source.pier.slice(0,100),1000,e.source.member,1071,e.source.index,1000,view);
|
||||
const scales=[...new Set(doc.views.map(v=>v.scale))];
|
||||
// Explicit model-unit text/arrow sizes; automatic DIMSCALE avoids a second viewport multiplier.
|
||||
const dimstyle=(scale:number,name:string)=>pairs(0,'DIMSTYLE',105,handle(),100,'AcDbSymbolTableRecord',100,'AcDbDimStyleTableRecord',2,name,70,0,40,0,41,CAD_STYLE.arrow*scale,42,CAD_STYLE.extensionGap*scale,44,CAD_STYLE.extensionOverrun*scale,140,CAD_STYLE.dimensionText*scale,144,1,77,1,271,1,278,46,176,1,177,1,178,3,147,1.2*scale,342,dotRecord,340,dimTextStyle);
|
||||
const dotRecord=handle();
|
||||
const ltypes=[['CONTINUOUS',[]],['HIDDEN',[2,-1]],['CENTER',[1.25,-.25,.25,-.25]],['A-CENTR01',[1.25,-.25,.25,-.25]],['A-HIDDN01',[.25,-.125]],['A-ISONS01',[1,1]]] as [string,number[]][];
|
||||
let tables=table('LTYPE',ltypes.map(([name,pattern])=>pairs(0,'LTYPE',5,handle(),100,'AcDbSymbolTableRecord',100,'AcDbLinetypeTableRecord',2,name,70,0,3,name,72,65,73,pattern.length,40,pattern.reduce((a,b)=>a+Math.abs(b),0))+pattern.map(v=>pairs(49,v,74,0)).join('')));
|
||||
const layerNames=[...new Set(['0',...Object.keys(LAYERS),...Object.values(CAD_LAYERS),...Object.keys(REFERENCE_ACI),...sheetFrame(doc).map(e=>referenceLayer(e)),'CX-BORD-TEX3'])];
|
||||
tables+=table('LAYER',layerNames.map(name=>{const semantic=Object.entries(CAD_LAYERS).find(([,v])=>v===name)?.[0]??name;return pairs(0,'LAYER',5,handle(),100,'AcDbSymbolTableRecord',100,'AcDbLayerTableRecord',2,name,70,0,62,REFERENCE_ACI[name]??7,6,semantic==='CENTER'?'CENTER':semantic==='HIDDEN'?'A-HIDDN01':name==='CS-CONC-MAJR'||name==='CS-DIML'||name==='CS-SYMB'?'A-ISONS01':'CONTINUOUS',370,-3);}));
|
||||
const styleNames=['STANDARD','PZ_DIM','PZ_TEXT','PZ_EL','TB_TEXT','TB_WBTXT','ST1','굴림체','돋음'];
|
||||
tables+=table('STYLE',styleNames.map((name,i)=>pairs(0,'STYLE',5,i===0?textStyle:i===1?dimTextStyle:handle(),100,'AcDbSymbolTableRecord',100,'AcDbTextStyleTableRecord',2,name,70,0,40,0,41,1,50,0,71,0,42,2.8,3,name==='TB_TEXT'?'BareunDotumPro1.ttf':name==='TB_WBTXT'?'BareunDotumPro3.ttf':['ST1','굴림체','돋음'].includes(name)?'Dotum.ttf':'gulim.ttc',4,'')));
|
||||
tables+=table('APPID',['ACAD','PZ_GA'].map(name=>pairs(0,'APPID',5,handle(),100,'AcDbSymbolTableRecord',100,'AcDbRegAppTableRecord',2,name,70,0)));
|
||||
tables+=table('DIMSTYLE',[dimstyle(1,'STANDARD'),dimstyle(50,'HS_050'),dimstyle(1,'1_1(typ1)'),...scales.map(s=>dimstyle(s,`PZ_${s}`))]);
|
||||
let entities='',blocks='';
|
||||
const blockRecords=[{name:'_Dot',id:dotRecord,layout:''},{name:'*Model_Space',id:modelRecord,layout:modelLayout},{name:'*Paper_Space',id:paperRecord,layout:paperLayout}];
|
||||
const common=(e:Entity,owner:string,scale:number)=>pairs(5,handle(),330,owner,100,'AcDbEntity',8,referenceLayer(e),48,scale)+(e.cad?pairs(62,e.cad.color,370,e.cad.lineweight):'')+(owner===paperRecord?pairs(67,1,410,'Layout1'):'');
|
||||
const translate=(p: Point,offset: Point): Point=>[p[0]+offset[0],p[1]+offset[1]];
|
||||
const primitive=(e:Entity,offset:Point,owner:string,scale:number,view:string):string=>{
|
||||
const base=common(e,owner,scale),data=xdata(e,view);
|
||||
if(e.kind==='line'){const a=translate(e.a,offset),b=translate(e.b,offset);return pairs(0,'LINE')+base+pairs(100,'AcDbLine',10,a[0],20,a[1],30,0,11,b[0],21,b[1],31,0)+data;}
|
||||
if(e.kind==='polyline')return pairs(0,'LWPOLYLINE')+base+pairs(100,'AcDbPolyline',90,e.points.length,70,e.closed?1:0)+e.points.map(p=>{const q=translate(p,offset);return pairs(10,q[0],20,q[1]);}).join('')+data;
|
||||
if(e.kind==='circle'){const p=translate(e.center,offset);return pairs(0,'CIRCLE')+base+pairs(100,'AcDbCircle',10,p[0],20,p[1],30,0,40,e.radius)+data;}
|
||||
if(e.kind==='hatch'){
|
||||
const solid=e.pattern==='SOLID',angle=e.angle*Math.PI/180,color=e.color&&/^#[0-9a-f]{6}$/i.test(e.color)?parseInt(e.color.slice(1),16):0xcbd1d6;
|
||||
const loops=[e.points,...e.holes??[]];
|
||||
return pairs(0,'HATCH')+base+pairs(420,color,100,'AcDbHatch',10,0,20,0,30,0,210,0,220,0,230,1,2,e.pattern,70,solid?1:0,71,0,91,loops.length)+loops.map((loop,i)=>pairs(92,i?2:3,72,0,73,1,93,loop.length)+loop.map(p=>{const q=translate(p,offset);return pairs(10,q[0],20,q[1]);}).join('')+pairs(97,0)).join('')+pairs(75,0,76,1)+
|
||||
(solid?'':pairs(52,0,41,1,77,0,78,1,53,e.angle,43,0,44,0,45,-Math.sin(angle)*e.spacing,46,Math.cos(angle)*e.spacing,79,0))+pairs(98,0)+data;
|
||||
}
|
||||
if(e.kind==='text'){
|
||||
const p=translate(e.point,offset),align=e.align==='center'?1:e.align==='right'?2:0;
|
||||
return pairs(0,'TEXT')+base+pairs(100,'AcDbText',10,p[0],20,p[1],30,0,40,e.height,1,e.text,50,e.rotation??0,7,e.cad?.textStyle&&styleNames.includes(e.cad.textStyle)?e.cad.textStyle:e.layer==='ELEVATION'?'PZ_EL':e.layer==='DIMENSION'?'PZ_DIM':'PZ_TEXT',72,align,11,p[0],21,p[1],31,0,100,'AcDbText',73,0)+data;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
const startBlock=(name:string,id:string)=>pairs(0,'BLOCK',5,handle(),330,id,100,'AcDbEntity',8,'0',100,'AcDbBlockBegin',2,name,70,name.startsWith('*D')?1:0,10,0,20,0,30,0,3,name,1,'');
|
||||
const endBlock=(id:string)=>pairs(0,'ENDBLK',5,handle(),330,id,100,'AcDbEntity',8,'0',100,'AcDbBlockEnd');
|
||||
for(const record of blockRecords)blocks+=startBlock(record.name,record.id)+(record.name==='_Dot'?primitive({kind:'hatch',points:Array.from({length:16},(_,i)=>[.5*Math.cos(i*Math.PI/8),.5*Math.sin(i*Math.PI/8)] as Point),pattern:'SOLID',spacing:1,angle:0,color:'#ff0000',layer:'DIMENSION',source:{member:'dimension-dot',pier:doc.pier,index:0}},[0,0],record.id,1,'dimension-dot'):'')+endBlock(record.id);
|
||||
const front=doc.views[0],side=doc.views[1];
|
||||
const fw=front.bounds.maxX-front.bounds.minX,sw=side.bounds.maxX-side.bounds.minX;
|
||||
let dimensionIndex=0;
|
||||
for(const view of doc.views) {
|
||||
const b=view.bounds;
|
||||
const sectionWidth=doc.views.find(v=>v.id==='section')!.bounds.maxX-doc.views.find(v=>v.id==='section')!.bounds.minX;
|
||||
const extraIndex=['heights','iso','keyplan'].indexOf(view.id);
|
||||
const x=extraIndex>=0?(fw+sw+sectionWidth+20000)*(extraIndex+2):view.id==='side'||view.id==='footing'?fw+5000:view.id==='section'||view.id==='detail'?fw+sw+10000:view.id==='schedule'?fw+sw+sectionWidth+20000:0;
|
||||
const y=['top','footing','detail','schedule'].includes(view.id)?-(b.maxY-b.minY)-5000:0;
|
||||
const offset:Point=[x-b.minX,y-b.minY];
|
||||
for(const e of view.entities) {
|
||||
if(!visible.has(e.layer))continue;
|
||||
if(e.kind!=='dimension'){entities+=primitive(e,offset,modelRecord,view.scale,view.id);continue;}
|
||||
const id=handle(),name=`*D${++dimensionIndex}`;blockRecords.push({name,id,layout:''});
|
||||
blocks+=startBlock(name,id)+dimensionGraphics(e).map(p=>primitive(p,offset,id,view.scale,view.id)).join('')+endBlock(id);
|
||||
const a=translate(e.a,offset),b2=translate(e.b,offset);
|
||||
const definition:Point=e.axis==='x'?[b2[0],e.position+offset[1]]:[e.position+offset[0],b2[1]];
|
||||
const mid:Point=e.axis==='x'?[(a[0]+b2[0])/2,definition[1]+e.height]:[definition[0]-e.height,(a[1]+b2[1])/2];
|
||||
entities+=pairs(0,'DIMENSION')+common(e,modelRecord,view.scale)+pairs(100,'AcDbDimension',2,name,10,definition[0],20,definition[1],30,0,11,mid[0],21,mid[1],31,0,70,32,1,'<>',3,`PZ_${view.scale}`,42,e.value,
|
||||
100,'AcDbAlignedDimension',13,a[0],23,a[1],33,0,14,b2[0],24,b2[1],34,0,50,e.axis==='x'?0:90,100,'AcDbRotatedDimension')+xdata(e,view.id);
|
||||
}
|
||||
const source={member:'view-title',index:0,pier:doc.pier};
|
||||
if(visible.has('TEXT'))entities+=primitive({kind:'text',point:[b.minX,b.maxY+4*view.scale],text:`${view.title} / model 1:1 mm`,height:3*view.scale,source,layer:'TEXT'},offset,modelRecord,view.scale,view.id);
|
||||
}
|
||||
// Layout1 matches the shortened general arrangement, while model space remains true-size.
|
||||
const frame=sheetFrame(doc).filter(e=>visible.has(e.layer));
|
||||
const border=frame.filter(e=>e.source.member==='reference-frame'||e.source.member==='reference-logo');
|
||||
const fields=frame.filter((e):e is Extract<Entity,{kind:'text'}>=>e.kind==='text'&&e.source.member.startsWith('frame-field:'));
|
||||
if(border.length){
|
||||
const id=handle(),name='PZ_CXBLKA1';blockRecords.push({name,id,layout:''});
|
||||
const attribute=(e:Extract<Entity,{kind:'text'}>,owner:string,definition:boolean)=>{
|
||||
const text=primitive(e,[0,0],owner,1,'sheet');
|
||||
const at=text.indexOf(pairs(100,'AcDbText',73,0));
|
||||
return text.slice(0,at).replace(pairs(0,'TEXT'),pairs(0,definition?'ATTDEF':'ATTRIB'))+
|
||||
pairs(100,definition?'AcDbAttributeDefinition':'AcDbAttribute',...(definition?[3,e.source.member.slice(12)]:[]),2,e.source.member.slice(12),70,0,73,0,74,0)+xdata(e,'sheet');
|
||||
};
|
||||
blocks+=startBlock(name,id)+border.map(e=>primitive(e,[0,0],id,1,'sheet')).join('')+fields.map(e=>attribute(e,id,true)).join('')+endBlock(id);
|
||||
const insert=handle();entities+=pairs(0,'INSERT',5,insert,330,paperRecord,100,'AcDbEntity',8,'0',67,1,410,'Layout1',100,'AcDbBlockReference',66,fields.length?1:0,2,name,10,0,20,0,30,0,41,1,42,1,43,1,50,0)+pairs(1001,'PZ_GA',1000,doc.pier,1000,'reference-frame',1071,0,1000,'sheet');
|
||||
if(fields.length)entities+=fields.map(e=>attribute(e,paperRecord,false)).join('')+pairs(0,'SEQEND',5,handle(),330,insert,100,'AcDbEntity',8,'0',67,1,410,'Layout1');
|
||||
}
|
||||
for(const e of frame)if(!border.includes(e)&&!fields.includes(e as Extract<Entity,{kind:'text'}>))entities+=primitive(e,[0,0],paperRecord,1,'sheet');
|
||||
for(const view of doc.views)if(!['section','heights'].includes(view.id))for(const {entity} of viewPaperEntities(doc,view))
|
||||
if(visible.has(entity.layer))entities+=primitive(entity,[0,0],paperRecord,1,view.id);
|
||||
tables+=table('BLOCK_RECORD',blockRecords.map(r=>pairs(0,'BLOCK_RECORD',5,r.id,100,'AcDbSymbolTableRecord',100,'AcDbBlockTableRecord',2,r.name,70,4)+ (r.layout?pairs(340,r.layout):'')));
|
||||
const dictionary=(id:string,owner:string,entries:[string,string][])=>pairs(0,'DICTIONARY',5,id,330,owner,100,'AcDbDictionary',281,1)+entries.map(([name,value])=>pairs(3,name,350,value)).join('');
|
||||
const layout=(id:string,name:string,record:string,order:number)=>pairs(0,'LAYOUT',5,id,330,layoutDictionary,100,'AcDbPlotSettings',1,'',2,'',4,doc.paper.name,6,'',40,0,41,0,42,0,43,0,44,doc.paper.width,45,doc.paper.height,46,0,47,0,70,128,72,1,73,0,74,0,75,16,142,1,143,1,76,0,77,0,
|
||||
100,'AcDbLayout',1,name,70,name==='Model'?1:0,71,order,10,0,20,0,11,doc.paper.width,21,doc.paper.height,12,0,22,0,32,0,14,0,24,0,34,0,15,100000,25,100000,35,0,146,0,13,0,23,0,33,0,16,1,26,0,36,0,17,0,27,1,37,0,76,0,330,record);
|
||||
const objects=dictionary(root,'0',[['ACAD_GROUP',groups],['ACAD_LAYOUT',layoutDictionary]])+dictionary(groups,root,[])+dictionary(layoutDictionary,root,[['Model',modelLayout],['Layout1',paperLayout]])+layout(modelLayout,'Model',modelRecord,0)+layout(paperLayout,'Layout1',paperRecord,1);
|
||||
const header=pairs(9,'$ACADVER',1,'AC1027',9,'$DWGCODEPAGE',3,'ANSI_949',9,'$INSUNITS',70,4,9,'$MEASUREMENT',70,1,9,'$DIMASSOC',280,1,9,'$LWDISPLAY',290,1,9,'$LTSCALE',40,1,9,'$HANDSEED',5,(next+20).toString(16).toUpperCase());
|
||||
return section('HEADER',header)+section('TABLES',tables)+section('BLOCKS',blocks)+section('ENTITIES',entities)+section('OBJECTS',objects)+pairs(0,'EOF');
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
"""Extract the supplied DWG's actual frame and note text; never change the source."""
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
import sys
|
||||
sys.path.insert(0,str(Path(__file__).resolve().parent.parent/'pierzainer_ground'))
|
||||
from pierzainer_ground.dxf_terrain import read_source_document
|
||||
from ezdxf.disassemble import recursive_decompose
|
||||
from ezdxf import path as ezpath
|
||||
from ezdxf import bbox, colors
|
||||
|
||||
ROOT=Path(__file__).resolve().parent
|
||||
doc,_=read_source_document(ROOT/'.cache/[교각-T 형] 01 일반도_20260619.dxf',ROOT/'.cache')
|
||||
frame=next(e for e in doc.modelspace().query('INSERT') if e.dxf.name=='CXBLKA1')
|
||||
entities=[]
|
||||
def p(q): return [float(q[0])/100,float(q[1])/100]
|
||||
for e in recursive_decompose([frame]):
|
||||
typ=e.dxftype()
|
||||
bb=bbox.extents([e],fast=True)
|
||||
logo=bb.has_data and bb.extmin.x>5000 and bb.extmax.x<18000 and bb.extmin.y>1000 and bb.extmax.y<6000
|
||||
base=dict(layer='FRAME',source=dict(member='reference-logo' if logo else 'reference-frame',index=len(entities),pier=''))
|
||||
layer=doc.layers.get(e.dxf.layer)
|
||||
aci=e.dxf.get('color',256)
|
||||
if aci in (0,256):aci=layer.dxf.color
|
||||
base['cad']=dict(layer=e.dxf.layer,color=abs(aci),linetype=e.dxf.get('linetype','BYLAYER'),lineweight=e.dxf.get('lineweight',-1),textStyle=e.dxf.all_existing_dxf_attribs().get('style','Standard'))
|
||||
if typ=='ATTRIB': continue
|
||||
if typ=='LINE': entities.append(dict(**base,kind='line',a=p(e.dxf.start),b=p(e.dxf.end)))
|
||||
elif typ in ('ARC','ELLIPSE','LWPOLYLINE'):
|
||||
try:
|
||||
pts=[p(q) for q in ezpath.make_path(e).flattening(5)]
|
||||
if len(pts)>1: entities.append(dict(**base,kind='polyline',points=pts,closed=bool(getattr(e,'is_closed',False))))
|
||||
except (TypeError,ValueError): pass
|
||||
elif typ=='TEXT':
|
||||
pt=e.dxf.align_point if e.dxf.get('valign',0) else e.dxf.insert
|
||||
entities.append(dict(**base,kind='text',point=p(pt),text=e.dxf.text,height=e.dxf.height/100,rotation=e.dxf.get('rotation',0),align='left'))
|
||||
elif typ=='HATCH':
|
||||
loops=[[p(q) for q in path.flattening(5)] for path in ezpath.from_hatch(e)]
|
||||
loops=[pts for pts in loops if len(pts)>2]
|
||||
aci=e.dxf.get('color',256)
|
||||
rgb=e.rgb or (colors.aci2rgb(aci) if 0<aci<256 and aci!=7 else (0,0,0))
|
||||
color='#'+''.join(f'{v:02x}' for v in rgb)
|
||||
if loops: entities.append(dict(**base,kind='hatch',points=loops[0],holes=loops[1:],pattern='SOLID',spacing=3,angle=45,color=color))
|
||||
attrs=[]
|
||||
for a in frame.attribs:
|
||||
if a.dxf.tag=='ES': continue
|
||||
attrs.append(dict(tag=a.dxf.tag,label=a.dxf.tag,value=a.dxf.text,point=p(a.dxf.insert),height=a.dxf.height/100,rotation=a.dxf.get('rotation',0)))
|
||||
texts=[e for e in doc.modelspace().query('TEXT') if 68000<e.dxf.insert.x<82000 and 33500<e.dxf.insert.y<50000]
|
||||
rows={}
|
||||
for e in texts: rows.setdefault(round(e.dxf.insert.y/10)*10,[]).append(e)
|
||||
notes=[]
|
||||
for y,es in sorted(rows.items(),reverse=True):
|
||||
es.sort(key=lambda e:e.dxf.insert.x)
|
||||
notes.append(' '.join(e.dxf.text.strip() for e in es))
|
||||
result=dict(version=1,name='원본 CXBLKA1 도곽 · 20260619',source='[교각-T 형] 01 일반도_20260619.dwg',sha256='156570dc5389650e20840e91c3d9a974a92c40687669f8d00aafb62073fb2b6b',width=841,height=594,entities=entities,fields=attrs,notes='\n'.join(notes))
|
||||
(ROOT/'.cache/reference-template.json').write_text(json.dumps(result,ensure_ascii=False,indent=2),encoding='utf-8')
|
||||
(ROOT/'reference-template.ts').write_text('// Generated from the supplied DWG by extract_template.py.\nexport default '+json.dumps(result,ensure_ascii=False,indent=2)+';\n',encoding='utf-8')
|
||||
print('frame entities',len(entities),'editable fields',len(attrs),'note lines',len(notes))
|
||||
@@ -0,0 +1,16 @@
|
||||
import { templateFrame } from './template';
|
||||
import { entityBox } from './occupancy';
|
||||
import type { DrawingDocument,Point,QualityIssue } from './types';
|
||||
/** Keep the supplied frame unchanged, but flag edited values which cannot fit their original cells. */
|
||||
export function frameQuality(doc:DrawingDocument):QualityIssue[]{
|
||||
const frame=templateFrame(doc),edges:Point[][]=[];
|
||||
for(const e of frame){if(e.kind==='line')edges.push([e.a,e.b]);else if(e.kind==='polyline'){e.points.slice(1).forEach((b,i)=>edges.push([e.points[i],b]));if(e.closed)edges.push([e.points[e.points.length-1],e.points[0]]);}}
|
||||
const issues:QualityIssue[]=[];
|
||||
for(const e of frame)if(e.kind==='text'&&e.source.member.startsWith('frame-field:')&&!e.rotation){
|
||||
const box=entityBox(e),right=edges.filter(([a,b])=>Math.abs(a[0]-b[0])<.01&&a[0]>e.point[0]+.01&&Math.min(a[1],b[1])<=e.point[1]&&Math.max(a[1],b[1])>=e.point[1]).map(([a])=>a[0]);
|
||||
const limit=right.length?Math.min(...right):doc.paper.width;
|
||||
if(box.maxX>limit-.2)issues.push({view:'sheet',kind:'outside',first:-2,message:`표제란 ${e.source.member.slice(12)} 문자가 원본 셀 폭을 넘습니다. 도곽 편집에서 내용을 줄이거나 사용자 도곽을 적용하세요.`});
|
||||
}
|
||||
if(doc.warnings.some(w=>w.startsWith('NOTE 원문이')))issues.push({view:'sheet',kind:'outside',first:-2,message:'NOTE 내용이 원본 일반사항 영역을 넘습니다.'});
|
||||
return issues;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { Rect } from './types';
|
||||
/** Optional future log input. No strata, elevations or boring positions are inferred. */
|
||||
export type GeologicalLog={id:string;source:string;groundEl:number|null;waterEl:number|null;layers:{name:string;topEl:number|null;bottomEl:number|null}[]};
|
||||
export type GeologicalPanel={title:string;area:Rect;logs:readonly GeologicalLog[]};
|
||||
export function geologicalPanel(area:Rect,logs:readonly GeologicalLog[]):GeologicalPanel|null {
|
||||
if(!logs.length)return null;
|
||||
for(const log of logs){
|
||||
if(!log.source.trim())throw new Error('지질주상도 원본 출처가 필요합니다.');
|
||||
for(const layer of log.layers)if(layer.topEl!==null&&layer.bottomEl!==null&&layer.topEl<layer.bottomEl)throw new Error('지층 상·하단 표고를 확인하세요.');
|
||||
}
|
||||
return {title:'지질주상도',area,logs};
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { Entity, Point, ViewBreak } from './types';
|
||||
const id=(p:Point)=>`${Math.round(p[0]*1000)},${Math.round(p[1]*1000)}`;
|
||||
export function hatchGeometry(entities:Entity[],scale:number):Entity[] {
|
||||
const out:Entity[]=[],groups=new Map<string,Extract<Entity,{kind:'line'}>[]>();
|
||||
for(const e of entities){
|
||||
if(!['STRUCTURE','BEARING'].includes(e.layer))continue;
|
||||
if(e.kind==='polyline'&&e.closed)out.push({kind:'hatch',points:e.points,pattern:e.source.member==='pile'?'ANSI31':'SOLID',angle:45,spacing:2*scale,color:e.source.member==='blinding'?'#e7e9ec':'#cccccc',source:e.source,layer:'HATCH'});
|
||||
if(e.kind==='circle'&&e.source.member==='column')out.push({kind:'hatch',points:Array.from({length:48},(_,i)=>[e.center[0]+e.radius*Math.cos(i*Math.PI/24),e.center[1]+e.radius*Math.sin(i*Math.PI/24)] as Point),pattern:'SOLID',angle:45,spacing:2*scale,color:'#cccccc',source:e.source,layer:'HATCH'});
|
||||
if(e.kind==='line'){const key=`${e.source.member}:${e.source.index}`;if(!groups.has(key))groups.set(key,[]);groups.get(key)!.push(e);}
|
||||
}
|
||||
for(const group of groups.values()){
|
||||
const unused=new Set(group);
|
||||
while(unused.size){const first=unused.values().next().value!;unused.delete(first);const points=[first.a,first.b];
|
||||
while(id(points[points.length-1])!==id(points[0])){
|
||||
const last=id(points[points.length-1]),next=[...unused].find(e=>id(e.a)===last||id(e.b)===last);
|
||||
if(!next)break;unused.delete(next);points.push(id(next.a)===last?next.b:next.a);
|
||||
}
|
||||
if(points.length>3&&id(points[0])===id(points[points.length-1]))out.push({kind:'hatch',points:points.slice(0,-1),pattern:first.source.member==='blinding'?'ANSI31':'SOLID',spacing:2*scale,angle:45,color:'#cccccc',layer:'HATCH',source:first.source});
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
const clipY=(points:Point[],y:number,above:boolean):Point[]=>{
|
||||
const out:Point[]=[];
|
||||
for(let i=0;i<points.length;i++){
|
||||
const a=points[i],b=points[(i+1)%points.length],da=(a[1]-y)*(above?1:-1),db=(b[1]-y)*(above?1:-1);
|
||||
if(da>=0)out.push(a);
|
||||
if((da>=0)!==(db>=0)){const t=(y-a[1])/(b[1]-a[1]);out.push([a[0]+(b[0]-a[0])*t,y]);}
|
||||
}return out;
|
||||
};
|
||||
export function cutHatch(points:Point[],cuts:readonly ViewBreak[]):Point[][] {
|
||||
let polygons=[points];
|
||||
for(const cut of cuts)polygons=polygons.flatMap(p=>[clipY(p,cut.from,false),clipY(p,cut.to,true)]).filter(p=>p.length>2);
|
||||
return polygons;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Vector3 } from 'three';
|
||||
|
||||
export type DepthFace<T>={vertices:Vector3[];edges:boolean[];data:T};
|
||||
/** BSP painter for an orthographic camera at negative depth. Split crossing faces,
|
||||
* preserving only original edges so partition boundaries never become CAD outlines. */
|
||||
export function visibleOrder<T>(input:DepthFace<T>[]):DepthFace<T>[] {
|
||||
const visit=(faces:DepthFace<T>[]):DepthFace<T>[]=>{
|
||||
if(!faces.length)return [];
|
||||
const seed=faces[0],a=seed.vertices[0];let normal=new Vector3();
|
||||
for(let i=1;i<seed.vertices.length-1&&normal.lengthSq()<1e-16;i++)normal=seed.vertices[i].clone().sub(a).cross(seed.vertices[i+1].clone().sub(a));
|
||||
if(normal.lengthSq()<1e-16)return visit(faces.slice(1));
|
||||
normal.normalize();const offset=normal.dot(a),front:DepthFace<T>[]=[],back:DepthFace<T>[]=[],same:DepthFace<T>[]=[seed];
|
||||
for(const face of faces.slice(1)){
|
||||
const ds=face.vertices.map(p=>normal.dot(p)-offset),positive=ds.some(d=>d>1e-5),negative=ds.some(d=>d< -1e-5);
|
||||
if(!positive&&!negative){same.push(face);continue;}
|
||||
if(!negative){front.push(face);continue;}if(!positive){back.push(face);continue;}
|
||||
for(const [sign,list] of [[1,front],[-1,back]] as const){
|
||||
const vertices:Vector3[]=[],incoming:boolean[]=[];
|
||||
for(let i=0;i<face.vertices.length;i++){
|
||||
const j=(i+1)%face.vertices.length,p=face.vertices[i],q=face.vertices[j],inside=ds[i]*sign>=-1e-5,next=ds[j]*sign>=-1e-5;
|
||||
if(inside&&next){vertices.push(q);incoming.push(face.edges[i]);}
|
||||
else if(inside&&!next){vertices.push(p.clone().lerp(q,ds[i]/(ds[i]-ds[j])));incoming.push(face.edges[i]);}
|
||||
else if(!inside&&next){vertices.push(p.clone().lerp(q,ds[i]/(ds[i]-ds[j])),q);incoming.push(false,face.edges[i]);}
|
||||
}
|
||||
if(vertices.length>=3)list.push({vertices,edges:incoming.map((_,i)=>incoming[(i+1)%incoming.length]),data:face.data});
|
||||
}
|
||||
}
|
||||
return normal.z<0?[...visit(back),...same,...visit(front)]:[...visit(front),...same,...visit(back)];
|
||||
};
|
||||
return visit(input.flatMap(face=>Array.from({length:face.vertices.length-2},(_,j)=>{const i=j+1;return {vertices:[face.vertices[0],face.vertices[i],face.vertices[i+1]],edges:[i===1&&face.edges[0],face.edges[i],i===face.vertices.length-2&&face.edges[i+1]],data:face.data};})));
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { presentationEntities } from './presentation';
|
||||
import type { Bounds,DrawingView,Entity,Point,QualityIssue } from './types';
|
||||
export const overlaps=(a:Bounds,b:Bounds,pad=0)=>Math.min(a.maxX,b.maxX)-Math.max(a.minX,b.minX)>pad&&Math.min(a.maxY,b.maxY)-Math.max(a.minY,b.minY)>pad;
|
||||
export const union=(boxes:Bounds[]):Bounds=>boxes.length?{minX:Math.min(...boxes.map(b=>b.minX)),minY:Math.min(...boxes.map(b=>b.minY)),maxX:Math.max(...boxes.map(b=>b.maxX)),maxY:Math.max(...boxes.map(b=>b.maxY))}:{minX:0,minY:0,maxX:1,maxY:1};
|
||||
export function textWidth(text:string,height:number):number{return Math.max(height*.35,[...text].reduce((s,c)=>s+(c.charCodeAt(0)>255?1:/[il., :;'|]/.test(c)?.3:/[MW@]/.test(c)?.88:.6),0)*height);}
|
||||
export function entityBox(e:Entity):Bounds {
|
||||
let ps:Point[]=[];
|
||||
if(e.kind==='text'){
|
||||
const w=textWidth(e.text,e.height),left=e.align==='center'?-w/2:e.align==='right'?-w:0,theta=(e.rotation??0)*Math.PI/180;
|
||||
ps=[[left,-.22*e.height],[left+w,-.22*e.height],[left+w,e.height],[left,e.height]].map(([x,y])=>[e.point[0]+x*Math.cos(theta)-y*Math.sin(theta),e.point[1]+x*Math.sin(theta)+y*Math.cos(theta)]);
|
||||
}else if(e.kind==='circle')ps=[[e.center[0]-e.radius,e.center[1]-e.radius],[e.center[0]+e.radius,e.center[1]+e.radius]];
|
||||
else if(e.kind==='line')ps=[e.a,e.b];else if(e.kind==='polyline'||e.kind==='hatch')ps=e.points;
|
||||
if(!ps.length)return{minX:0,minY:0,maxX:0,maxY:0};
|
||||
return{minX:Math.min(...ps.map(p=>p[0])),minY:Math.min(...ps.map(p=>p[1])),maxX:Math.max(...ps.map(p=>p[0])),maxY:Math.max(...ps.map(p=>p[1]))};
|
||||
}
|
||||
export const occupiedBounds=(view:DrawingView):Bounds=>union(presentationEntities(view).map(p=>entityBox(p.entity)));
|
||||
const lineHits=(a:Point,b:Point,r:Bounds):boolean=>{
|
||||
let t0=0,t1=1;const dx=b[0]-a[0],dy=b[1]-a[1];
|
||||
for(const [p,q] of [[-dx,a[0]-r.minX],[dx,r.maxX-a[0]],[-dy,a[1]-r.minY],[dy,r.maxY-a[1]]]){
|
||||
if(Math.abs(p)<1e-12){if(q<0)return false;}else{const t=q/p;if(p<0)t0=Math.max(t0,t);else t1=Math.min(t1,t);if(t0>t1)return false;}
|
||||
}return true;
|
||||
};
|
||||
export function viewCollisions(view:DrawingView):QualityIssue[]{
|
||||
const rendered=presentationEntities(view),texts=rendered.filter(p=>p.entity.kind==='text'),issues:QualityIssue[]=[],s=view.scale;
|
||||
const structural=rendered.filter(p=>['STRUCTURE','BEARING'].includes(p.entity.layer));
|
||||
for(let i=0;i<texts.length;i++){
|
||||
const a=texts[i],box=entityBox(a.entity);
|
||||
for(const b of texts.slice(i+1))if(overlaps(box,entityBox(b.entity),.1*s))issues.push({view:view.id,kind:'text-text',first:a.index,second:b.index,message:'문자 점유 영역 중첩'});
|
||||
if(['bearing-detail','bearing-number','keyplan-label','keyplan','column-section-inline'].includes(a.entity.source.member))continue;
|
||||
for(const p of structural){
|
||||
const e=p.entity;if(!overlaps(box,entityBox(e),0))continue;
|
||||
const segments=e.kind==='line'?[[e.a,e.b]]:e.kind==='polyline'?e.points.slice(1).map((b,i)=>[e.points[i],b]).concat(e.closed?[[e.points[e.points.length-1],e.points[0]]]:[]):[];
|
||||
if(segments.some(([a,b])=>lineHits(a,b,box))){issues.push({view:view.id,kind:'text-structure',first:a.index,second:p.index,message:'문자와 구조 외형선 간섭'});break;}
|
||||
}
|
||||
}
|
||||
return issues;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { dimensionGraphics } from './dimension-graphics';
|
||||
import { breakSymbols, shortenEntity, shortenedBounds, shortenedY } from './breaks';
|
||||
import type { DrawingView, Entity, Point } from './types';
|
||||
|
||||
export function presentationEntities(view:DrawingView):{entity:Entity;index:number}[] {
|
||||
const cuts=view.breaks??[];
|
||||
const out=view.entities.flatMap((entity,index)=>{
|
||||
if(entity.kind==='dimension'){
|
||||
const p=(p:Point):Point=>[p[0],shortenedY(p[1],cuts)];
|
||||
// Centre the label on the displayed dimension, retaining its original measured value.
|
||||
const displayed={...entity,a:p(entity.a),b:p(entity.b),position:entity.axis==='x'?shortenedY(entity.position,cuts):entity.position};
|
||||
return dimensionGraphics(displayed).map(entity=>({entity,index}));
|
||||
}
|
||||
return shortenEntity(entity,cuts,entity.source.member!=='annotation').map(entity=>({entity,index}));
|
||||
});
|
||||
for(const entity of breakSymbols(view.entities,cuts,view.scale))out.push({entity,index:view.entities.findIndex(e=>e.source===entity.source)});
|
||||
const cut=cuts.find(c=>c.member==='column'),column=view.entities.find(e=>e.kind==='polyline'&&e.source.member==='column');
|
||||
if(view.id==='front'&&cut&&column?.kind==='polyline'){
|
||||
const xs=column.points.map(p=>p[0]),x=(Math.min(...xs)+Math.max(...xs))/2,r=(Math.max(...xs)-Math.min(...xs))/2;
|
||||
const y=(shortenedBounds({minX:0,maxX:0,minY:cut.from,maxY:cut.to},cuts).minY+shortenedBounds({minX:0,maxX:0,minY:cut.from,maxY:cut.to},cuts).maxY)/2;
|
||||
const source={...column.source,member:'column-section-inline'},base={source,layer:'SECTION' as const},index=view.entities.indexOf(column);
|
||||
out.push({index,entity:{...base,kind:'circle',center:[x,y],radius:r}},
|
||||
{index,entity:{...base,kind:'text',point:[x+r+2*view.scale,y],text:'A-A',height:2.5*view.scale}});
|
||||
if(view.entities.some(e=>e.layer==='CENTER'))out.push(
|
||||
{index,entity:{...base,layer:'CENTER',kind:'line',a:[x-r-view.scale,y],b:[x+r+view.scale,y]}},
|
||||
{index,entity:{...base,layer:'CENTER',kind:'line',a:[x,y-r-view.scale],b:[x,y+r+view.scale]}});
|
||||
if(view.entities.some(e=>e.kind==='dimension'))out.push(...dimensionGraphics({kind:'dimension',a:[x-r,y],b:[x+r,y],axis:'x',position:y+r+1.5*view.scale,height:2.8*view.scale,value:2*r,source,layer:'DIMENSION'}).map(entity=>({entity,index})));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
export function paperPoint(view:DrawingView,p:Point):Point {
|
||||
if(view.paperOrigin)return[view.paperOrigin[0]+p[0]/view.scale,view.paperOrigin[1]-p[1]/view.scale];
|
||||
const b=shortenedBounds(view.bounds,view.breaks),s=view.slot;
|
||||
return[s.x+(s.width-(b.maxX-b.minX)/view.scale)/2+(p[0]-b.minX)/view.scale,
|
||||
s.y+(s.height-(b.maxY-b.minY)/view.scale)/2+(b.maxY-p[1])/view.scale];
|
||||
}
|
||||
export function paperEntity(view:DrawingView,e:Entity):Entity {
|
||||
const point=(p:Point):Point=>{const q=paperPoint(view,p);return[q[0],-q[1]];};
|
||||
if(e.kind==='line')return{...e,a:point(e.a),b:point(e.b)};
|
||||
if(e.kind==='polyline')return{...e,points:e.points.map(point)};
|
||||
if(e.kind==='hatch')return{...e,points:e.points.map(point),holes:e.holes?.map(loop=>loop.map(point)),spacing:e.spacing/view.scale};
|
||||
if(e.kind==='circle')return{...e,center:point(e.center),radius:e.radius/view.scale};
|
||||
if(e.kind==='text')return{...e,point:point(e.point),height:e.height/view.scale};
|
||||
throw new Error('Paper entities require expanded dimension graphics.');
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { Vector3 } from 'three';
|
||||
import type { PierModel } from '../src/core/geometry';
|
||||
import { modelPrimitives, projectPoint, projectModel } from './projection';
|
||||
import { modelBreaks, shortenedY } from './breaks';
|
||||
import type { Entity, Point, Source } from './types';
|
||||
import { visibleOrder } from './iso-visibility';
|
||||
|
||||
function clipFace(points:Vector3[],y:number,above:boolean):Vector3[]{
|
||||
const out:Vector3[]=[];
|
||||
for(let i=0;i<points.length;i++){const a=points[i],b=points[(i+1)%points.length],da=(a.y-y)*(above?1:-1),db=(b.y-y)*(above?1:-1);
|
||||
if(da>=0)out.push(a);if((da>=0)!==(db>=0))out.push(a.clone().lerp(b,(y-a.y)/(b.y-a.y)));}
|
||||
return out;
|
||||
}
|
||||
/** Vector axonometry from the very same concrete/bearing solids and cylinder dimensions as the live model. */
|
||||
export function isometricEntities(model:PierModel,shorten:boolean,direction:number):Entity[]{
|
||||
const primitives=modelPrimitives(model),faces:{points:Vector3[];source:Source;layer:Entity['layer']}[]=[];
|
||||
for(const solid of primitives.solids)for(const points of solid.faces)faces.push({points,source:solid.source,layer:solid.layer});
|
||||
for(const c of primitives.cylinders){
|
||||
const ring=(z:number)=>Array.from({length:24},(_,i)=>new Vector3(c.center.x+c.radius*Math.cos(i*Math.PI/12),z,c.center.z+c.radius*Math.sin(i*Math.PI/12)));
|
||||
const low=ring(c.bottom),high=ring(c.top);faces.push({points:[...low].reverse(),source:c.source,layer:'STRUCTURE'},{points:high,source:c.source,layer:'STRUCTURE'});
|
||||
low.forEach((a,i)=>faces.push({points:[a,low[(i+1)%24],high[(i+1)%24],high[i]],source:c.source,layer:'STRUCTURE'}));
|
||||
}
|
||||
const cuts=shorten?modelBreaks(model,100):[],theta=direction*Math.PI/2,datum=Math.min(...model.columns.map(c=>c.bottom),...model.piles.map(c=>c.bottom),...model.footings.map(c=>c.bottom))*1000;
|
||||
const project=(p:Vector3):{p:Point;depth:number}=>{
|
||||
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),z=shortenedY(p.y*1000,cuts)-shortenedY(datum,cuts);
|
||||
return{p:[(a-b)*Math.sqrt(3)/2,z+(a+b)*.5],depth:a+b-2*z};
|
||||
};
|
||||
const projected=faces.flatMap(face=>{
|
||||
let parts=[face.points];for(const c of cuts)parts=parts.flatMap(p=>[clipFace(p,c.from/1000,false),clipFace(p,c.to/1000,true)]).filter(p=>p.length>2);
|
||||
return parts.map(points=>{const projected=points.map(project),v=points[1].clone().sub(points[0]),w=points[2].clone().sub(points[0]),normal=v.cross(w).normalize();
|
||||
const gray=Math.round(185+40*Math.abs(normal.y)+12*Math.abs(normal.x));return{vertices:projected.map(p=>new Vector3(p.p[0],p.p[1],p.depth)),edges:points.map(()=>true),data:{color:`#${gray.toString(16).repeat(3)}`,source:face.source,layer:face.layer}};});
|
||||
});
|
||||
return visibleOrder(projected).flatMap(({vertices,edges,data:face}):Entity[]=>{
|
||||
const points:Point[]=vertices.map(p=>[p.x,p.y]);
|
||||
return [{kind:'hatch',points,pattern:'SOLID',spacing:100,angle:45,color:face.color,source:face.source,layer:'HATCH'},
|
||||
...points.flatMap((a,i):Entity[]=>edges[i]?[{kind:'line',a,b:points[(i+1)%points.length],source:face.source,layer:face.layer}]:[])];
|
||||
});
|
||||
}
|
||||
|
||||
const keyOutlineCache=new WeakMap<PierModel,Entity[]>();
|
||||
/** E/N key plan: the connecting polyline joins actual pier centres, without inventing a road alignment. */
|
||||
export function keyplanEntities(models:readonly PierModel[],selected:PierModel,scale:number,startLabel:string,endLabel:string):Entity[]{
|
||||
const available=models.length?models:[selected],origin=available[0].pivot,entities:Entity[]=[],centres:Point[]=[];
|
||||
const lastModel=available[available.length-1],theta=available.length>1?Math.atan2(lastModel.pivot.n-origin.n,lastModel.pivot.e-origin.e):0;
|
||||
const rotate=(p:Point):Point=>[p[0]*Math.cos(theta)+p[1]*Math.sin(theta),-p[0]*Math.sin(theta)+p[1]*Math.cos(theta)];
|
||||
const source={member:'keyplan',pier:selected.name,index:0};
|
||||
for(const model of available){
|
||||
const centre=rotate([(model.pivot.e-origin.e)*1000,(model.pivot.n-origin.n)*1000]);centres.push(centre);
|
||||
let outline=keyOutlineCache.get(model);if(!outline){outline=projectModel(model,'top').filter(e=>e.layer==='STRUCTURE'&&e.source.member==='coping');keyOutlineCache.set(model,outline);}
|
||||
const transform=(p:Point):Point=>rotate([(model.bp1.e-origin.e)*1000+p[0]*model.u.e+p[1]*model.nrm.e,(model.bp1.n-origin.n)*1000+p[0]*model.u.n+p[1]*model.nrm.n]);
|
||||
for(const e of outline)if(e.kind==='line')entities.push({...e,a:transform(e.a),b:transform(e.b),layer:model.name===selected.name?'BEARING':'STRUCTURE',source:{...e.source,member:'keyplan-pier'}});
|
||||
entities.push({kind:'circle',center:centre,radius:1.2*scale,layer:model.name===selected.name?'BEARING':'CENTER',source:{member:'keyplan-centre',pier:model.name,index:0}},
|
||||
{kind:'text',point:[centre[0]+1.8*scale,centre[1]+2*scale],text:model.name,height:2.2*scale,layer:'TEXT',source:{member:'keyplan-label',pier:model.name,index:0}});
|
||||
}
|
||||
if(centres.length>1)entities.unshift({kind:'polyline',points:centres,closed:false,layer:'CENTER',source});
|
||||
const first=centres[0],last=centres[centres.length-1];
|
||||
entities.push({kind:'text',point:[first[0],first[1]-5*scale],text:startLabel,height:2*scale,layer:'TEXT',source},
|
||||
{kind:'text',point:[last[0],last[1]+6*scale],text:endLabel,height:2*scale,layer:'TEXT',source});
|
||||
const x=Math.max(...centres.map(p=>p[0]))+10*scale,y=0,north:Point=[Math.sin(theta),Math.cos(theta)],tip:Point=[x+north[0]*7*scale,y+north[1]*7*scale];
|
||||
entities.push({kind:'line',a:[x,y],b:tip,layer:'TEXT',source},{kind:'text',point:[tip[0]+scale,tip[1]+scale],text:'N',height:2*scale,layer:'TEXT',source},
|
||||
{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;
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
import { Vector3, type Plane } from 'three';
|
||||
import { DIM, blockFramePoint, type PierModel } from '../src/core/geometry';
|
||||
import { INTEGRATED_BLOCK } from '../src/core/integrated-block';
|
||||
import { solplateGeometry } from '../src/core/solplate';
|
||||
import { stretchMatrix, mitrePlane } from '../src/view/scene';
|
||||
import type { Bounds, Entity, Layer, Point, Source, ViewId } from './types';
|
||||
|
||||
type Face = Vector3[];
|
||||
type Solid = { faces: Face[]; source: Source; layer: Layer };
|
||||
export type Cylinder = { center: Vector3; bottom: number; top: number; radius: number; source: Source };
|
||||
const EPS = .001; // mm, drawing topology tolerance; source dimensions are never rounded here.
|
||||
const cross = (a: Point, b: Point) => a[0]*b[1]-a[1]*b[0];
|
||||
const sub = (a: Point, b: Point): Point => [a[0]-b[0],a[1]-b[1]];
|
||||
const key = (p: Point) => `${Math.round(p[0]/EPS)},${Math.round(p[1]/EPS)}`;
|
||||
const edgeKey = (a: Point,b: Point) => [key(a),key(b)].sort().join('|');
|
||||
|
||||
function clip(face: Face, plane?: Plane): Face {
|
||||
if (!plane) return face;
|
||||
const out: Face=[];
|
||||
for (let i=0;i<face.length;i++) {
|
||||
const a=face[i],b=face[(i+1)%face.length],da=plane.distanceToPoint(a),db=plane.distanceToPoint(b);
|
||||
if (da>=-1e-9) out.push(a);
|
||||
if ((da>=0)!==(db>=0)) out.push(a.clone().lerp(b,da/(da-db)));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
export function localPoint(model: PierModel,k: number,s: number,z: number,n: number): Vector3 {
|
||||
return new Vector3(s,z,n).applyMatrix4(stretchMatrix(model,k));
|
||||
}
|
||||
export function projectPoint(model: PierModel,point: Vector3,view: ViewId): Point {
|
||||
const east=point.x+model.pivot.e-model.bp1.e,north=-point.z+model.pivot.n-model.bp1.n;
|
||||
const along=(east*model.u.e+north*model.u.n)*1000;
|
||||
const across=(east*model.nrm.e+north*model.nrm.n)*1000;
|
||||
return view==='front' ? [along,point.y*1000] : view==='side' ? [across,point.y*1000] : [along,across];
|
||||
}
|
||||
function area(p: Point[]): number { return p.reduce((sum,a,i)=>sum+cross(a,p[(i+1)%p.length]),0)/2; }
|
||||
function inside(p: Point,poly: Point[]): boolean {
|
||||
let hit=false;
|
||||
for(let i=0,j=poly.length-1;i<poly.length;j=i++) {
|
||||
const a=poly[i],b=poly[j];
|
||||
if((a[1]>p[1])!==(b[1]>p[1]) && p[0]<(b[0]-a[0])*(p[1]-a[1])/(b[1]-a[1])+a[0]) hit=!hit;
|
||||
}
|
||||
return hit;
|
||||
}
|
||||
|
||||
/** Boundary of the union of orthographically projected solid faces (no screenshot or mesh raster). */
|
||||
export function silhouette(polygons: Point[][]): [Point,Point][] {
|
||||
const valid=polygons.filter(p=>p.length>=3&&Math.abs(area(p))>EPS*EPS);
|
||||
const edges=valid.flatMap(p=>p.map((a,i)=>[a,p[(i+1)%p.length]] as [Point,Point]));
|
||||
const result=new Map<string,[Point,Point]>();
|
||||
for(const [a,b] of edges) {
|
||||
const d=sub(b,a),length=Math.hypot(...d); if(length<EPS)continue;
|
||||
const ts=[0,1];
|
||||
for(const [c,e] of edges) {
|
||||
const r=sub(e,c),q=sub(c,a),det=cross(d,r);
|
||||
if(Math.abs(det)>1e-8) {
|
||||
const t=cross(q,r)/det,u=cross(q,d)/det;
|
||||
if(t>0&&t<1&&u>=-1e-9&&u<=1+1e-9)ts.push(t);
|
||||
} else if(Math.abs(cross(q,d))<EPS*length) {
|
||||
for(const p of [c,e]) { const v=sub(p,a),t=(v[0]*d[0]+v[1]*d[1])/(length*length);if(t>0&&t<1)ts.push(t); }
|
||||
}
|
||||
}
|
||||
const sorted=[...new Set(ts.map(t=>Math.round(t*1e10)/1e10))].sort((x,y)=>x-y);
|
||||
const at=(t: number): Point=>[a[0]+d[0]*t,a[1]+d[1]*t];
|
||||
for(let i=1;i<sorted.length;i++) {
|
||||
const start=at(sorted[i-1]),end=at(sorted[i]);if(Math.hypot(...sub(end,start))<EPS)continue;
|
||||
const middle=at((sorted[i-1]+sorted[i])/2),offset=EPS*.2;
|
||||
const left: Point=[middle[0]-d[1]/length*offset,middle[1]+d[0]/length*offset];
|
||||
const right: Point=[middle[0]+d[1]/length*offset,middle[1]-d[0]/length*offset];
|
||||
if(valid.some(p=>inside(left,p))!==valid.some(p=>inside(right,p)))result.set(edgeKey(start,end),[start,end]);
|
||||
}
|
||||
}
|
||||
return [...result.values()];
|
||||
}
|
||||
|
||||
function prism(model: PierModel,k: number,profile: Point[],n0: number,n1: number,source: Source,layer: Layer,cut=true): Solid {
|
||||
const front=profile.map(([s,z])=>localPoint(model,k,s,z,n0));
|
||||
const back=profile.map(([s,z])=>localPoint(model,k,s,z,n1));
|
||||
const faces=[front,[...back].reverse(),...front.map((a,i)=>[a,front[(i+1)%front.length],back[(i+1)%back.length],back[i]])];
|
||||
return { faces:faces.map(f=>clip(f,cut?mitrePlane(model,k):undefined)).filter(f=>f.length>=3),source,layer };
|
||||
}
|
||||
function box(model: PierModel,k: number,s: number,n: number,length: number,width: number,bottom: number,top: number,source: Source,layer: Layer,skew=0,cut=false): Solid {
|
||||
const corners=[[-length/2,-width/2],[length/2,-width/2],[length/2,width/2],[-length/2,width/2]].map(([a,c])=>blockFramePoint({s,n,rowSkewDeg:skew},a,c));
|
||||
const low=corners.map(([a,c])=>localPoint(model,k,a,bottom,c));
|
||||
const high=corners.map(([a,c])=>localPoint(model,k,a,top,c));
|
||||
return {source,layer,faces:[[...low].reverse(),high,...low.map((a,i)=>[a,low[(i+1)%4],high[(i+1)%4],high[i]])].map(f=>clip(f,cut?mitrePlane(model,k):undefined)).filter(f=>f.length>=3)};
|
||||
}
|
||||
|
||||
/** Derived, disposable drawing primitives; the supplied PierModel remains the only geometry source. */
|
||||
export function modelPrimitives(model: PierModel): { solids: Solid[]; cylinders: Cylinder[] } {
|
||||
const solids: Solid[]=[],cylinders: Cylinder[]=[];
|
||||
const source=(member: string,index: number): Source=>({member,index,pier:model.name});
|
||||
model.copingParts.forEach((p,i)=>solids.push(prism(model,p.k,p.profile,p.nFrom,p.nTo,source('coping',i),'STRUCTURE')));
|
||||
model.footings.forEach((f,i)=>solids.push(box(model,f.k,f.s,0,f.length,f.width,f.bottom,f.top,source('footing',i),'STRUCTURE',0,true)));
|
||||
model.blindings.forEach((b,i)=>solids.push(box(model,b.k,b.s,0,b.length,b.width,b.top-b.thickness,b.top,source('blinding',i),'STRUCTURE',0,true)));
|
||||
model.columns.forEach((c,i)=>cylinders.push({center:localPoint(model,c.k,c.s,0,0),bottom:c.bottom,top:c.top,radius:c.diameter/2,source:source('column',i)}));
|
||||
model.piles.forEach((p,i)=>cylinders.push({center:localPoint(model,p.k,p.s,0,p.n),bottom:p.bottom,top:p.top,radius:p.diameter/2,source:source('pile',i)}));
|
||||
model.blocks.forEach((b,i)=>{
|
||||
if(!model.integratedBlocks.length)solids.push(box(model,b.k,b.s,b.n,DIM.blockSize,DIM.blockSize,b.bottom,b.top,source('block',i),'BEARING',b.rowSkewDeg));
|
||||
solids.push(box(model,b.k,b.s,b.n,DIM.padSize,DIM.padSize,b.padTop-DIM.padThickness,b.padTop,source('pad',i),'BEARING',b.rowSkewDeg));
|
||||
if(b.plan) {
|
||||
solids.push(box(model,b.k,b.s,b.n,(b.plan.B+b.plan.margin)/1000-.2,(b.plan.A+b.plan.margin)/1000-.2,b.padTop,b.padTop+b.plan.h3/1000,source('bearing',i),'BEARING',b.rowSkewDeg));
|
||||
const corners=solplateGeometry(b,b.plan.h3,{A:b.plan.A,B:b.plan.B,margin:b.plan.margin,T:b.plan.T,slopePct:b.plan.S}).corners.map(([s,z,n])=>localPoint(model,b.k,s,z,n));
|
||||
const faces=[[0,4,6,2],[1,3,7,5],[0,1,5,4],[2,6,7,3],[0,2,3,1],[4,5,7,6]].map(indices=>indices.map(index=>corners[index]));
|
||||
solids.push({source:source('solplate',i),layer:'BEARING',faces});
|
||||
}
|
||||
});
|
||||
model.integratedBlocks.forEach((b,i)=>{
|
||||
const k=model.blocks.find(p=>p.no===b.frontNo&&p.row==='전열')!.k;
|
||||
for(const [top,n] of [[b.topFront,b.n-INTEGRATED_BLOCK.length/4000],[b.topBack,b.n+INTEGRATED_BLOCK.length/4000]])
|
||||
solids.push(box(model,k,b.s,n,INTEGRATED_BLOCK.depth/1000,INTEGRATED_BLOCK.length/2000,b.bottom,top,source('integrated-block',i),'BEARING',b.rowSkewDeg));
|
||||
});
|
||||
return {solids,cylinders};
|
||||
}
|
||||
|
||||
export function projectModel(model: PierModel,view: ViewId,primitives=modelPrimitives(model)): Entity[] {
|
||||
const entities: Entity[]=[];
|
||||
const footingOnly=view==='footing';
|
||||
for(const solid of primitives.solids) {
|
||||
if(footingOnly&&!['footing','blinding'].includes(solid.source.member))continue;
|
||||
if(view==='top'&&['footing','blinding'].includes(solid.source.member))continue;
|
||||
const polygons=solid.faces.map(face=>face.map(p=>projectPoint(model,p,view)));
|
||||
const edges=silhouette(polygons),outer=new Set(edges.map(([a,b])=>edgeKey(a,b)));
|
||||
for(const [a,b] of edges)entities.push({kind:'line',a,b,layer:solid.layer,source:solid.source});
|
||||
const hidden=new Set<string>();
|
||||
for(const poly of polygons)for(let i=0;i<poly.length;i++) {
|
||||
const a=poly[i],b=poly[(i+1)%poly.length],id=edgeKey(a,b);
|
||||
if(!outer.has(id)&&!hidden.has(id)&&Math.hypot(...sub(a,b))>EPS) { hidden.add(id); entities.push({kind:'line',a,b,layer:'HIDDEN',source:solid.source}); }
|
||||
}
|
||||
}
|
||||
for(const cylinder of primitives.cylinders) {
|
||||
if(view==='top'&&cylinder.source.member==='pile')continue;
|
||||
const center=projectPoint(model,cylinder.center,view),r=cylinder.radius*1000;
|
||||
if(view==='top'||view==='footing')entities.push({kind:'circle',center,radius:r,layer:cylinder.source.member==='pile'?'HIDDEN':'STRUCTURE',source:cylinder.source});
|
||||
else {
|
||||
const bottom=cylinder.bottom*1000,top=cylinder.top*1000;
|
||||
entities.push({kind:'polyline',points:[[center[0]-r,bottom],[center[0]+r,bottom],[center[0]+r,top],[center[0]-r,top]],closed:true,layer:'STRUCTURE',source:cylinder.source});
|
||||
}
|
||||
}
|
||||
return entities;
|
||||
}
|
||||
export function entityBounds(entities: Entity[]): Bounds {
|
||||
const points: Point[]=entities.flatMap(e=>e.kind==='line'?[e.a,e.b]:e.kind==='polyline'||e.kind==='hatch'?e.points:e.kind==='circle'?[[e.center[0]-e.radius,e.center[1]-e.radius],[e.center[0]+e.radius,e.center[1]+e.radius]] as Point[]:e.kind==='dimension'?(e.axis==='x'?[[e.a[0],e.position],[e.b[0],e.position],e.a,e.b]:[[e.position,e.a[1]],[e.position,e.b[1]],e.a,e.b]) as Point[]:[e.point]);
|
||||
if(!points.length)return{minX:0,minY:0,maxX:1,maxY:1};
|
||||
return{minX:Math.min(...points.map(p=>p[0])),minY:Math.min(...points.map(p=>p[1])),maxX:Math.max(...points.map(p=>p[0])),maxY:Math.max(...points.map(p=>p[1]))};
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
import type { Bounds,DrawingDocument,DrawingView,Entity,Point,QualityIssue,Rect,ViewId } from './types';
|
||||
import { CAD_STYLE } from './cad-style';
|
||||
import { entityBox,occupiedBounds,overlaps,textWidth,union,viewCollisions } from './occupancy';
|
||||
import { presentationEntities } from './presentation';
|
||||
import { shortenedY } from './breaks';
|
||||
import { frameQuality } from './frame-quality';
|
||||
|
||||
export const DRAWING_SCALES=[25,40,50,75,100,125,150,200,250,300,400,500,750,1000,1500,2000,3000,5000];
|
||||
type Factory=(id:ViewId,scale:number)=>DrawingView;
|
||||
const physical=(b:Bounds,s:number):Bounds=>({minX:b.minX/s,minY:b.minY/s,maxX:b.maxX/s,maxY:b.maxY/s});
|
||||
function normalise(view:DrawingView):void{
|
||||
const table=['schedule','heights'].includes(view.id);
|
||||
view.entities=view.entities.map(e=>{
|
||||
if(e.kind==='dimension')return{...e,height:CAD_STYLE.dimensionText*view.scale};
|
||||
if(e.kind==='text')return{...e,height:Math.max(e.height,(table?CAD_STYLE.tableText:e.layer==='ELEVATION'?CAD_STYLE.elevationText:CAD_STYLE.text)*view.scale)};
|
||||
return e;
|
||||
});
|
||||
}
|
||||
/** Interval lanes: nested/overall dimensions are placed after the shorter detail intervals. */
|
||||
function dimensionLanes(view:DrawingView):void{
|
||||
const s=view.scale,b=view.geometryBounds,wide=view.id==='front'||view.id==='side';
|
||||
const ordinary=presentationEntities({...view,entities:view.entities.filter(e=>e.kind!=='dimension')}).filter(p=>p.entity.layer!=='CENTER');
|
||||
const base=union([physical({minX:b.minX,maxX:b.maxX,minY:shortenedY(b.minY,view.breaks),maxY:shortenedY(b.maxY,view.breaks)},s),...ordinary.map(p=>physical(entityBox(p.entity),s))]);
|
||||
const groups=new Map<string,Extract<Entity,{kind:'dimension'}>[]>();
|
||||
for(const e of view.entities)if(e.kind==='dimension'){
|
||||
const visualMid=(shortenedY(b.minY,view.breaks)+shortenedY(b.maxY,view.breaks))/2;
|
||||
const key=e.axis==='x'?(shortenedY(e.position,view.breaks)<visualMid?'bottom':'top'):(wide||e.position<(b.minX+b.maxX)/2?'left':'right');
|
||||
if(!groups.has(key))groups.set(key,[]);groups.get(key)!.push(e);
|
||||
}
|
||||
for(const [side,dims] of groups){
|
||||
const lanes:{lo:number;hi:number;textLo:number;textHi:number}[][]=[];
|
||||
dims.sort((a,b)=>a.value-b.value);
|
||||
for(const e of dims){
|
||||
const a=e.axis==='x'?e.a[0]/s:shortenedY(e.a[1],view.breaks)/s,c=e.axis==='x'?e.b[0]/s:shortenedY(e.b[1],view.breaks)/s;
|
||||
const lo=Math.min(a,c),hi=Math.max(a,c),width=textWidth(e.value.toLocaleString('en-US',{maximumFractionDigits:1}),CAD_STYLE.dimensionText),mid=(lo+hi)/2;
|
||||
const item={lo,hi,textLo:mid-width/2-.6,textHi:mid+width/2+.6};
|
||||
let lane=0;
|
||||
while(lanes[lane]?.some(p=>Math.min(p.hi,item.hi)-Math.max(p.lo,item.lo)>.05||Math.min(p.textHi,item.textHi)-Math.max(p.textLo,item.textLo)>0))lane++;
|
||||
(lanes[lane]??=[]).push(item);
|
||||
const offset=lane*CAD_STYLE.laneGap;
|
||||
e.position=(side==='left'?base.minX-3-offset:side==='right'?base.maxX+6+offset:side==='bottom'?b.minY/s+(base.minY-shortenedY(b.minY,view.breaks)/s)-6-offset: b.maxY/s+(base.maxY-shortenedY(b.maxY,view.breaks)/s)+3+offset)*s;
|
||||
}
|
||||
}
|
||||
}
|
||||
function rawY(view:DrawingView,y:number):number{
|
||||
let removed=0;for(const c of view.breaks??[]){const lo=c.from-removed,hi=lo+c.gap;if(y<=lo)return y+removed;if(y<hi)return c.from+(y-lo)/c.gap*(c.to-c.from);removed+=c.to-c.from-c.gap;}return y+removed;
|
||||
}
|
||||
function tidyElevations(view:DrawingView):void{
|
||||
const s=view.scale,labels=view.entities.filter((e):e is Extract<Entity,{kind:'text'}>=>e.kind==='text'&&e.layer==='ELEVATION').sort((a,b)=>a.point[1]-b.point[1]);
|
||||
let previous=-Infinity;
|
||||
for(const e of labels){
|
||||
const old=[...e.point],display=shortenedY(e.point[1],view.breaks),y=Math.max(display,previous+4.5*s);previous=y;
|
||||
e.point=[view.geometryBounds.maxX+4*s,rawY(view,y)];
|
||||
const leader=view.entities.find(x=>x.kind==='line'&&x.layer==='ELEVATION'&&Math.abs(x.b[1]-(old[1]-.3*s))<s*.15);
|
||||
if(leader?.kind==='line')leader.b=[e.point[0]-.8*s,e.point[1]-.3*s];
|
||||
}
|
||||
}
|
||||
function finish(view:DrawingView):DrawingView{
|
||||
measureCache.delete(view);
|
||||
normalise(view);tidyElevations(view);
|
||||
if(view.id==='detail'){
|
||||
const labels=view.entities.filter((e):e is Extract<Entity,{kind:'text'}>=>e.kind==='text'&&e.text.includes('EL.X')).sort((a,b)=>a.point[0]-b.point[0]);
|
||||
const placed:Bounds[]=[];
|
||||
labels.forEach(e=>{let moves=0;while(placed.some(b=>overlaps(b,entityBox(e),-.2*view.scale))&&moves<10){e.point=[e.point[0],e.point[1]+4*view.scale];moves++;}placed.push(entityBox(e));});
|
||||
}
|
||||
if(view.id==='keyplan'){
|
||||
const used:Bounds[]=[];
|
||||
const priority=(e:Extract<Entity,{kind:'text'}>)=>e.text==='N'?0:e.source.member==='keyplan-label'?2:1;
|
||||
for(const e of view.entities.filter((e):e is Extract<Entity,{kind:'text'}>=>e.kind==='text').sort((a,b)=>priority(a)-priority(b)||a.point[0]-b.point[0])){
|
||||
const old=[...e.point] as Point;let steps=0;
|
||||
while(used.some(b=>overlaps(b,entityBox(e),-.3*view.scale))&&steps<8){e.point=[e.point[0],e.point[1]+4*view.scale];steps++;}
|
||||
used.push(entityBox(e));if(steps)view.entities.push({kind:'line',a:[old[0]-1.8*view.scale,old[1]-2*view.scale],b:[e.point[0],e.point[1]-.5*view.scale],layer:'CENTER',source:{...e.source,member:'keyplan-leader'}});
|
||||
}
|
||||
}
|
||||
if(['front','side','top','footing','section'].includes(view.id))dimensionLanes(view);
|
||||
const b=occupiedBounds(view);view.bounds={minX:b.minX-.8*view.scale,maxX:b.maxX+.8*view.scale,minY:rawY(view,b.minY-.8*view.scale),maxY:rawY(view,b.maxY+.8*view.scale)};
|
||||
return view;
|
||||
}
|
||||
const collisionCache=new WeakMap<DrawingView,number>();
|
||||
const collisionCount=(v:DrawingView):number=>{let n=collisionCache.get(v);if(n===undefined){n=viewCollisions(v).length;collisionCache.set(v,n);}return n;};
|
||||
const measureCache=new WeakMap<DrawingView,Bounds>();
|
||||
const measure=(v:DrawingView):Bounds=>{let b=measureCache.get(v);if(!b){b=physical(occupiedBounds(v),v.scale);measureCache.set(v,b);}return b;};
|
||||
function fit(view:DrawingView,area:Rect):void{
|
||||
const b=measure(view),w=b.maxX-b.minX,h=b.maxY-b.minY;
|
||||
view.paperOrigin=[area.x+(area.width-w)/2-b.minX,area.y+(area.height-h)/2+b.maxY];
|
||||
view.slot={x:view.paperOrigin[0]+b.minX,y:view.paperOrigin[1]-b.maxY,width:w,height:h};
|
||||
}
|
||||
|
||||
/** Preserve template zoning, solve view scales, rows and column widths from measured CAD content. */
|
||||
export function optimiseLayout(doc:DrawingDocument,factory:Factory):void{
|
||||
const geometrySignature=()=>JSON.stringify(doc.views.filter(v=>['front','side','top','footing','section'].includes(v.id)).map(v=>v.entities.filter(e=>['STRUCTURE','BEARING','HIDDEN'].includes(e.layer)&&e.kind!=='hatch')));
|
||||
const originalGeometry=geometrySignature();
|
||||
const before=doc.views.filter(v=>!['iso','keyplan'].includes(v.id)).reduce((n,v)=>n+viewCollisions(v).length,0);
|
||||
const cache=new Map<string,DrawingView>();
|
||||
const get=(id:ViewId,scale:number)=>{const key=`${id}:${scale}`;if(!cache.has(key))cache.set(key,finish(factory(id,scale)));return cache.get(key)!;};
|
||||
const k=doc.paper.width/841,ky=doc.paper.height/594,major:Rect={x:54*k,y:55*ky,width:466*k,height:472*ky},gap=CAD_STYLE.viewGap;
|
||||
const choices=doc.settings.scale==='auto'?DRAWING_SCALES:[doc.settings.scale];
|
||||
let best:{views:DrawingView[];bs:Bounds[];left:number;right:number;top:number;bottom:number;score:number}|undefined;
|
||||
for(const elevation of choices){
|
||||
for(const plan of doc.settings.scale==='auto'?choices.filter(p=>p>=elevation*.7&&p<=elevation*1.8):choices){
|
||||
const vs=[get('front',elevation),get('side',elevation),get('top',plan),get('footing',plan)],bs=vs.map(measure);
|
||||
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;
|
||||
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.
|
||||
if(best&&best.score<10000&&elevation>best.views[0].scale)break;
|
||||
}
|
||||
if(!best)return;
|
||||
const extraW=Math.max(0,major.width-best.left-best.right-gap),leftW=best.left+extraW*.55,rightW=best.right+extraW*.45;
|
||||
const extraH=Math.max(0,major.height-best.top-best.bottom-gap-12),topY=major.y+5+extraH*.2,bottomY=topY+best.top+gap+6+extraH*.3;
|
||||
const maxTop=Math.max(best.bs[0].maxY,best.bs[1].maxY);
|
||||
for(let i=0;i<4;i++){
|
||||
const v=best.views[i],b=best.bs[i],left=i%2===0,x=left?major.x:major.x+leftW+gap,w=left?leftW:rightW;
|
||||
const y=i<2?topY:bottomY;
|
||||
v.paperOrigin=[x+(w-(b.maxX-b.minX))/2-b.minX,y+(i<2?maxTop:b.maxY)];
|
||||
v.slot={x:v.paperOrigin[0]+b.minX,y:v.paperOrigin[1]-b.maxY,width:b.maxX-b.minX,height:b.maxY-b.minY};
|
||||
doc.views[doc.views.findIndex(p=>p.id===v.id)]=v;
|
||||
}
|
||||
// One aligned geometry centre per column; the top/bottom views retain independent scales.
|
||||
for(const column of [[best.views[0],best.views[2]],[best.views[1],best.views[3]]]){
|
||||
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;
|
||||
}
|
||||
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};
|
||||
let pair:{iso:DrawingView;detail:DrawingView;ih:number;dh:number;score:number}|undefined;
|
||||
for(const isoScale of DRAWING_SCALES)for(const detailScale of DRAWING_SCALES.filter(s=>s<=300)){
|
||||
const iso=get('iso',isoScale),detail=get('detail',detailScale),a=measure(iso),b=measure(detail),iw=a.maxX-a.minX,dw=b.maxX-b.minX,ih=a.maxY-a.minY,dh=b.maxY-b.minY;
|
||||
const over=Math.max(0,Math.max(iw,dw)-small.width)+Math.max(0,ih+dh+14-small.height),score=over*10000+collisionCount(detail)*100000+isoScale+detailScale;
|
||||
if(!pair||score<pair.score)pair={iso,detail,ih,dh,score};
|
||||
}
|
||||
if(pair){
|
||||
const remaining=Math.max(0,small.height-pair.ih-pair.dh-14),isoY=small.y+remaining*.2;
|
||||
fit(pair.iso,{x:small.x,y:isoY,width:small.width,height:pair.ih});fit(pair.detail,{x:small.x,y:isoY+pair.ih+14+remaining*.3,width:small.width,height:pair.dh});
|
||||
for(const v of [pair.iso,pair.detail])doc.views[doc.views.findIndex(p=>p.id===v.id)]=v;
|
||||
}
|
||||
const keyOriginal=doc.views.find(v=>v.id==='keyplan')!;let keyView:DrawingView|undefined,keyScore=Infinity;
|
||||
for(const ratio of [.8,1,1.1,1.25,1.5,2,3]){const v=get('keyplan',Math.ceil(keyOriginal.scale*ratio/100)*100),b=measure(v),w=b.maxX-b.minX,h=b.maxY-b.minY,over=Math.max(0,w-122*k)+Math.max(0,h-58*ky);const score=over*10000+v.scale;if(score<keyScore){keyScore=score;keyView=v;}}
|
||||
if(keyView){const b=measure(keyView),height=b.maxY-b.minY;fit(keyView,{x:687*k,y:78*ky-height,width:122*k,height});doc.views[doc.views.findIndex(v=>v.id==='keyplan')]=keyView;}
|
||||
for(const id of ['heights','section'] as ViewId[]){const original=doc.views.find(v=>v.id===id)!;const v=finish(original);fit(v,original.slot);}
|
||||
const visible=doc.views.filter(v=>!['heights','section'].includes(v.id)),issues:QualityIssue[]=visible.flatMap(viewCollisions),occupied:Record<string,Rect>={};
|
||||
let minimumTextMm=Infinity;
|
||||
for(const v of visible){occupied[v.id]=v.slot;for(const {entity} of presentationEntities(v))if(entity.kind==='text')minimumTextMm=Math.min(minimumTextMm,entity.height/v.scale);
|
||||
if(v.slot.x<23*k||v.slot.y<20*ky||v.slot.x+v.slot.width>822*k||v.slot.y+v.slot.height>532*ky)issues.push({view:v.id,kind:'outside',first:-1,message:'도면 점유 영역이 출력 가능 영역을 넘습니다.'});
|
||||
}
|
||||
for(let i=0;i<visible.length;i++)for(const b of visible.slice(i+1)){const a=visible[i],rect=(v:DrawingView):Bounds=>({minX:v.slot.x,minY:v.slot.y-4.5,maxX:v.slot.x+v.slot.width,maxY:v.slot.y+v.slot.height});if(overlaps(rect(a),rect(b),.5))issues.push({view:a.id,kind:'view-overlap',first:-1,message:`${a.title} / ${b.title} 점유 영역 중첩`});}
|
||||
const reserved=[{x:680*k,y:82*ky,width:140*k,height:273*ky},{x:50*k,y:537*ky,width:770*k,height:57*ky}];
|
||||
for(const v of visible)for(const r of reserved)if(overlaps({minX:v.slot.x,minY:v.slot.y-6,maxX:v.slot.x+v.slot.width,maxY:v.slot.y+v.slot.height},{minX:r.x,minY:r.y,maxX:r.x+r.width,maxY:r.y+r.height},.5))issues.push({view:v.id,kind:'view-overlap',first:-1,message:'일반사항/표제란 보호 영역과 중첩'});
|
||||
issues.push(...frameQuality(doc));
|
||||
doc.quality={before,after:issues.length,issues,occupied,minimumTextMm,layout:'reference-zones / measured CAD bounds / interval dimension lanes',geometryUnchanged:originalGeometry===geometrySignature()};
|
||||
if(!doc.quality.geometryUnchanged)throw new Error('배치 중 원본 투영 형상이 바뀌었습니다. 출력을 중단합니다.');
|
||||
if(issues.length)doc.warnings.push(`도면 품질 검사: 간섭/범위 확인 ${issues.length}건. 품질 검사 결과를 확인하세요.`);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { DrawingDocument,ViewId } from './types';
|
||||
export function createQualityPanel(host:HTMLElement,read:()=>DrawingDocument|undefined,select:(id:ViewId)=>void):()=>void{
|
||||
const dialog=window.document.createElement('dialog');dialog.className='ga-sheet-dialog ga-quality-dialog';dialog.setAttribute('aria-label','도면 품질 검사');
|
||||
dialog.innerHTML='<header><strong>도면 품질 검사</strong><button type="button" data-close>닫기</button></header><p data-summary></p><p>출력 단위에서 문자 폭·회전·치수 화살표를 포함해 검사합니다. 보조선의 형상 접점과 의도된 상세 번호 표기는 별도로 취급합니다. CAD 글꼴에 따른 최종 검토는 필요합니다.</p><label><input type="checkbox" data-highlight/> 간섭 위치 강조</label><div data-list></div><footer><span>표의 버튼을 누르면 해당 투영도로 이동합니다.</span><button type="button" data-save>검사 결과 JSON 저장</button></footer>';
|
||||
host.append(dialog);const summary=dialog.querySelector<HTMLElement>('[data-summary]')!,list=dialog.querySelector<HTMLElement>('[data-list]')!;
|
||||
dialog.querySelector('[data-close]')!.addEventListener('click',()=>dialog.close());
|
||||
dialog.querySelector('[data-highlight]')!.addEventListener('change',e=>host.classList.toggle('ga-show-quality',(e.target as HTMLInputElement).checked));
|
||||
dialog.querySelector('[data-save]')!.addEventListener('click',()=>{const doc=read();if(!doc)return;const url=URL.createObjectURL(new Blob([JSON.stringify({project:doc.project,pier:doc.pier,paper:doc.paper,quality:doc.quality,scales:doc.views.map(v=>({view:v.id,scale:v.scale})),dwg:doc.dwgValidation},null,2)],{type:'application/json'})),a=window.document.createElement('a');a.href=url;a.download=`${doc.pier}_일반도_품질검사.json`.replace(/[\\/:*?"<>|]/g,'_');a.click();setTimeout(()=>URL.revokeObjectURL(url),1000);});
|
||||
return()=>{const doc=read(),q=doc?.quality;if(!doc||!q)return;
|
||||
summary.textContent=`자동 배치 전 ${q.before}건 → 배치 후 ${q.after}건 · 최소 문자 ${q.minimumTextMm.toFixed(1)}mm · 실제 형상 ${q.geometryUnchanged?'유지':'변경 감지'}`;list.replaceChildren();
|
||||
for(const issue of q.issues){const row=window.document.createElement('div'),button=window.document.createElement('button'),text=window.document.createElement('span'),view=doc.views.find(v=>v.id===issue.view);row.className='ga-quality-row';button.type='button';button.textContent=view?.title??(issue.view==='sheet'?'도곽/노트':issue.view);text.textContent=issue.message+(issue.first>=0?` (객체 ${issue.first+1})`:'');button.addEventListener('click',()=>{select(issue.view as ViewId);host.classList.add('ga-show-quality');(dialog.querySelector('[data-highlight]') as HTMLInputElement).checked=true;dialog.close();});row.append(button,text);list.append(row);}
|
||||
if(!q.issues.length)list.textContent='검사 범위 내 문자/외형 및 투영도 간 간섭이 없습니다.';dialog.showModal();
|
||||
};
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,532 @@
|
||||
{
|
||||
"layers": {
|
||||
"0": {
|
||||
"color": 7,
|
||||
"linetype": "Continuous",
|
||||
"lineweight": -3,
|
||||
"entities": [
|
||||
[
|
||||
"INSERT:256",
|
||||
2
|
||||
]
|
||||
]
|
||||
},
|
||||
"CS-TEXT": {
|
||||
"color": 3,
|
||||
"linetype": "A-ISONS01",
|
||||
"lineweight": -3,
|
||||
"entities": [
|
||||
[
|
||||
"TEXT:256",
|
||||
70
|
||||
],
|
||||
[
|
||||
"CIRCLE:256",
|
||||
30
|
||||
],
|
||||
[
|
||||
"HATCH:3",
|
||||
30
|
||||
]
|
||||
]
|
||||
},
|
||||
"CS-CONC-MAJR": {
|
||||
"color": 4,
|
||||
"linetype": "A-ISONS01",
|
||||
"lineweight": -3,
|
||||
"entities": [
|
||||
[
|
||||
"LINE:256",
|
||||
594
|
||||
],
|
||||
[
|
||||
"LWPOLYLINE:256",
|
||||
77
|
||||
],
|
||||
[
|
||||
"LINE:8",
|
||||
16
|
||||
],
|
||||
[
|
||||
"ARC:256",
|
||||
12
|
||||
]
|
||||
]
|
||||
},
|
||||
"CS-DIML": {
|
||||
"color": 1,
|
||||
"linetype": "A-ISONS01",
|
||||
"lineweight": -3,
|
||||
"entities": [
|
||||
[
|
||||
"LINE:256",
|
||||
318
|
||||
],
|
||||
[
|
||||
"LWPOLYLINE:256",
|
||||
152
|
||||
],
|
||||
[
|
||||
"DIMENSION:256",
|
||||
150
|
||||
],
|
||||
[
|
||||
"SOLID:256",
|
||||
24
|
||||
]
|
||||
]
|
||||
},
|
||||
"CS-SYMB": {
|
||||
"color": 1,
|
||||
"linetype": "A-ISONS01",
|
||||
"lineweight": -3,
|
||||
"entities": [
|
||||
[
|
||||
"INSERT:256",
|
||||
14
|
||||
],
|
||||
[
|
||||
"TEXT:256",
|
||||
6
|
||||
],
|
||||
[
|
||||
"CIRCLE:256",
|
||||
6
|
||||
],
|
||||
[
|
||||
"LWPOLYLINE:256",
|
||||
6
|
||||
]
|
||||
]
|
||||
},
|
||||
"CS-TXT1": {
|
||||
"color": 6,
|
||||
"linetype": "A-ISONS01",
|
||||
"lineweight": -3,
|
||||
"entities": [
|
||||
[
|
||||
"TEXT:256",
|
||||
18
|
||||
]
|
||||
]
|
||||
},
|
||||
"CZ-LIN4": {
|
||||
"color": 6,
|
||||
"linetype": "A-ISONS01",
|
||||
"lineweight": -3,
|
||||
"entities": [
|
||||
[
|
||||
"LINE:256",
|
||||
6
|
||||
]
|
||||
]
|
||||
},
|
||||
"CS-CNTL": {
|
||||
"color": 1,
|
||||
"linetype": "A-CENTR01",
|
||||
"lineweight": -3,
|
||||
"entities": [
|
||||
[
|
||||
"LINE:256",
|
||||
63
|
||||
],
|
||||
[
|
||||
"CIRCLE:256",
|
||||
2
|
||||
]
|
||||
]
|
||||
},
|
||||
"CX-BORD-TEX2": {
|
||||
"color": 7,
|
||||
"linetype": "Continuous",
|
||||
"lineweight": -3,
|
||||
"entities": [
|
||||
[
|
||||
"TEXT:256",
|
||||
22
|
||||
],
|
||||
[
|
||||
"HATCH:256",
|
||||
6
|
||||
],
|
||||
[
|
||||
"LWPOLYLINE:256",
|
||||
6
|
||||
]
|
||||
]
|
||||
},
|
||||
"CS-PATT254": {
|
||||
"color": 254,
|
||||
"linetype": "A-ISONS01",
|
||||
"lineweight": -3,
|
||||
"entities": [
|
||||
[
|
||||
"HATCH:256",
|
||||
4
|
||||
]
|
||||
]
|
||||
},
|
||||
"CS-CONC-REFR-HIDD": {
|
||||
"color": 8,
|
||||
"linetype": "A-HIDDN01",
|
||||
"lineweight": -3,
|
||||
"entities": [
|
||||
[
|
||||
"LINE:256",
|
||||
6
|
||||
],
|
||||
[
|
||||
"LWPOLYLINE:256",
|
||||
2
|
||||
]
|
||||
]
|
||||
},
|
||||
"CS-PATT253": {
|
||||
"color": 253,
|
||||
"linetype": "Continuous",
|
||||
"lineweight": -3,
|
||||
"entities": [
|
||||
[
|
||||
"HATCH:254",
|
||||
70
|
||||
],
|
||||
[
|
||||
"LINE:35",
|
||||
24
|
||||
],
|
||||
[
|
||||
"HATCH:256",
|
||||
18
|
||||
]
|
||||
]
|
||||
},
|
||||
"CS-PATT252": {
|
||||
"color": 252,
|
||||
"linetype": "Continuous",
|
||||
"lineweight": -3,
|
||||
"entities": [
|
||||
[
|
||||
"HATCH:256",
|
||||
11
|
||||
]
|
||||
]
|
||||
},
|
||||
"CZ-LIN3": {
|
||||
"color": 1,
|
||||
"linetype": "A-ISONS01",
|
||||
"lineweight": -3,
|
||||
"entities": [
|
||||
[
|
||||
"LWPOLYLINE:256",
|
||||
104
|
||||
],
|
||||
[
|
||||
"LINE:256",
|
||||
72
|
||||
],
|
||||
[
|
||||
"ARC:256",
|
||||
20
|
||||
]
|
||||
]
|
||||
},
|
||||
"99_가이드라인": {
|
||||
"color": 1,
|
||||
"linetype": "Continuous",
|
||||
"lineweight": -3,
|
||||
"entities": [
|
||||
[
|
||||
"LINE:191",
|
||||
16
|
||||
],
|
||||
[
|
||||
"LWPOLYLINE:191",
|
||||
8
|
||||
]
|
||||
]
|
||||
},
|
||||
"CS-TABLE-LIN1": {
|
||||
"color": 4,
|
||||
"linetype": "Continuous",
|
||||
"lineweight": -3,
|
||||
"entities": [
|
||||
[
|
||||
"LWPOLYLINE:256",
|
||||
6
|
||||
]
|
||||
]
|
||||
},
|
||||
"CX-BORD-LIN1": {
|
||||
"color": 4,
|
||||
"linetype": "Continuous",
|
||||
"lineweight": -3,
|
||||
"entities": [
|
||||
[
|
||||
"ARC:256",
|
||||
1600
|
||||
]
|
||||
]
|
||||
},
|
||||
"CS-CONC-MINR": {
|
||||
"color": 4,
|
||||
"linetype": "A-HIDDN01",
|
||||
"lineweight": -3,
|
||||
"entities": [
|
||||
[
|
||||
"LINE:256",
|
||||
4
|
||||
],
|
||||
[
|
||||
"CIRCLE:256",
|
||||
2
|
||||
]
|
||||
]
|
||||
},
|
||||
"CS-DIMT": {
|
||||
"color": 3,
|
||||
"linetype": "Continuous",
|
||||
"lineweight": -3,
|
||||
"entities": [
|
||||
[
|
||||
"TEXT:256",
|
||||
146
|
||||
],
|
||||
[
|
||||
"OLE2FRAME:256",
|
||||
2
|
||||
],
|
||||
[
|
||||
"HATCH:43",
|
||||
1
|
||||
]
|
||||
]
|
||||
},
|
||||
"CZ-TEX1": {
|
||||
"color": 6,
|
||||
"linetype": "Continuous",
|
||||
"lineweight": -3,
|
||||
"entities": [
|
||||
[
|
||||
"TEXT:256",
|
||||
3
|
||||
]
|
||||
]
|
||||
},
|
||||
"CZ-TEX2": {
|
||||
"color": 3,
|
||||
"linetype": "Continuous",
|
||||
"lineweight": -3,
|
||||
"entities": [
|
||||
[
|
||||
"TEXT:256",
|
||||
6
|
||||
]
|
||||
]
|
||||
},
|
||||
"CS-PATT": {
|
||||
"color": 1,
|
||||
"linetype": "Continuous",
|
||||
"lineweight": -3,
|
||||
"entities": [
|
||||
[
|
||||
"LINE:256",
|
||||
688
|
||||
],
|
||||
[
|
||||
"LWPOLYLINE:256",
|
||||
68
|
||||
]
|
||||
]
|
||||
},
|
||||
"CS-CUTL": {
|
||||
"color": 1,
|
||||
"linetype": "Continuous",
|
||||
"lineweight": -3,
|
||||
"entities": [
|
||||
[
|
||||
"LINE:256",
|
||||
20
|
||||
]
|
||||
]
|
||||
},
|
||||
"CS-TABL-LIN1": {
|
||||
"color": 4,
|
||||
"linetype": "Continuous",
|
||||
"lineweight": -3,
|
||||
"entities": [
|
||||
[
|
||||
"LWPOLYLINE:256",
|
||||
2
|
||||
]
|
||||
]
|
||||
},
|
||||
"CS-TABL-LIN2": {
|
||||
"color": 1,
|
||||
"linetype": "Continuous",
|
||||
"lineweight": -3,
|
||||
"entities": [
|
||||
[
|
||||
"LINE:256",
|
||||
42
|
||||
],
|
||||
[
|
||||
"CIRCLE:256",
|
||||
20
|
||||
]
|
||||
]
|
||||
},
|
||||
"CS-TABL-TEX2": {
|
||||
"color": 3,
|
||||
"linetype": "Continuous",
|
||||
"lineweight": -3,
|
||||
"entities": [
|
||||
[
|
||||
"TEXT:256",
|
||||
246
|
||||
]
|
||||
]
|
||||
},
|
||||
"CR-BORE-TEXT": {
|
||||
"color": 3,
|
||||
"linetype": "Continuous",
|
||||
"lineweight": -3,
|
||||
"entities": [
|
||||
[
|
||||
"TEXT:256",
|
||||
26
|
||||
]
|
||||
]
|
||||
},
|
||||
"CR-BORE-MISC": {
|
||||
"color": 1,
|
||||
"linetype": "Continuous",
|
||||
"lineweight": -3,
|
||||
"entities": [
|
||||
[
|
||||
"LINE:256",
|
||||
110
|
||||
],
|
||||
[
|
||||
"CIRCLE:256",
|
||||
2
|
||||
]
|
||||
]
|
||||
},
|
||||
"CS-XXXXCS-CENT": {
|
||||
"color": 1,
|
||||
"linetype": "CENTER",
|
||||
"lineweight": -3,
|
||||
"entities": [
|
||||
[
|
||||
"LINE:256",
|
||||
176
|
||||
]
|
||||
]
|
||||
},
|
||||
"CS-XXXXCS-HATH": {
|
||||
"color": 1,
|
||||
"linetype": "Continuous",
|
||||
"lineweight": -3,
|
||||
"entities": [
|
||||
[
|
||||
"LWPOLYLINE:256",
|
||||
40
|
||||
]
|
||||
]
|
||||
},
|
||||
"CS-XXXXCS-STRS": {
|
||||
"color": 3,
|
||||
"linetype": "Continuous",
|
||||
"lineweight": -3,
|
||||
"entities": [
|
||||
[
|
||||
"TEXT:256",
|
||||
32
|
||||
],
|
||||
[
|
||||
"CIRCLE:256",
|
||||
20
|
||||
]
|
||||
]
|
||||
},
|
||||
"CS-XXXXCS-MIS1": {
|
||||
"color": 1,
|
||||
"linetype": "Continuous",
|
||||
"lineweight": -3,
|
||||
"entities": [
|
||||
[
|
||||
"LINE:256",
|
||||
600
|
||||
]
|
||||
]
|
||||
},
|
||||
"CS-CONC-변단": {
|
||||
"color": 251,
|
||||
"linetype": "A-DASHD01",
|
||||
"lineweight": -3,
|
||||
"entities": [
|
||||
[
|
||||
"LINE:256",
|
||||
93
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"styles": [
|
||||
{
|
||||
"name": "Standard",
|
||||
"font": "gulim.ttc",
|
||||
"bigfont": "",
|
||||
"width": 1.0
|
||||
},
|
||||
{
|
||||
"name": "TB_TEXT",
|
||||
"font": "BareunDotumPro1.ttf",
|
||||
"bigfont": "",
|
||||
"width": 1.0
|
||||
},
|
||||
{
|
||||
"name": "TB_WBTXT",
|
||||
"font": "BareunDotumPro3.ttf",
|
||||
"bigfont": "",
|
||||
"width": 1.0
|
||||
},
|
||||
{
|
||||
"name": "00_도곽$0$TB_WBTXT",
|
||||
"font": "BareunDotumPro3.ttf",
|
||||
"bigfont": "",
|
||||
"width": 1.0
|
||||
},
|
||||
{
|
||||
"name": "00_도곽$0$TB_WMTXT",
|
||||
"font": "gulim.ttc",
|
||||
"bigfont": "",
|
||||
"width": 1.0
|
||||
},
|
||||
{
|
||||
"name": "돋움",
|
||||
"font": "gulim.ttc",
|
||||
"bigfont": "",
|
||||
"width": 1.0
|
||||
},
|
||||
{
|
||||
"name": "ST1",
|
||||
"font": "Dotum.ttf",
|
||||
"bigfont": "",
|
||||
"width": 1.0
|
||||
},
|
||||
{
|
||||
"name": "굴림체",
|
||||
"font": "Dotum.ttf",
|
||||
"bigfont": "",
|
||||
"width": 1.0
|
||||
},
|
||||
{
|
||||
"name": "돋음",
|
||||
"font": "Dotum.ttf",
|
||||
"bigfont": "",
|
||||
"width": 1.0
|
||||
}
|
||||
],
|
||||
"linetypes": {}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { Entity,Point,Source } from './types';
|
||||
/** CZSECMU-ARW2: actual source block vertices, inserted at 80/100 paper scale. */
|
||||
export function sectionCut(point:Point,scale:number,label:string,reverse=false,source:Source):Entity[]{
|
||||
const f=.8*scale,sign=reverse?-1:1,p=(x:number,y:number):Point=>[point[0]+x*f*sign,point[1]-y*f];
|
||||
const layer='SECTION' as const;
|
||||
return [
|
||||
{kind:'line',a:p(-3,0),b:p(-13,0),layer,source},
|
||||
{kind:'line',a:p(0,0),b:p(-1,0),layer,source},
|
||||
{kind:'hatch',points:[p(-13,0),p(-10,0),p(-13,10)],pattern:'SOLID',spacing:scale,angle:0,color:'#ff0000',layer,source},
|
||||
{kind:'text',point:p(-15.07132694382339,12.13685926104699),text:label,height:4.5*f,layer,source},
|
||||
];
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,40 @@
|
||||
import { defaultSheet, FIELD_LABELS, parseSheetConfig, REFERENCE_TEMPLATE } from './template';
|
||||
import type { SheetConfig } from './types';
|
||||
|
||||
export function createSheetEditor(host:HTMLElement,apply:(config:SheetConfig)=>void):{open:(value:SheetConfig)=>void}{
|
||||
const dialog=window.document.createElement('dialog');dialog.className='ga-sheet-dialog';dialog.setAttribute('aria-label','도곽 및 노트 편집');
|
||||
dialog.innerHTML=`<header><div><strong>원본 도곽 · NOTE 편집</strong><p id="ga-template-name"></p></div><button type="button" id="ga-editor-close">닫기</button></header>
|
||||
<p class="ga-editor-intro">첨부 DWG의 CXBLKA1 도곽·표제란과 NOTE 원문입니다. 강도·시공 조건은 현 프로젝트에 맞게 검토하여 수정하세요. {{project}}, {{bridge}}, {{pier}}, {{scale}}, {{paper}}는 현재 도면 값으로 치환됩니다.</p>
|
||||
<div class="ga-editor-tools"><label><input type="checkbox" id="ga-frame-show"/> 도곽 표시</label><label><input type="checkbox" id="ga-logo-show"/> 원본 로고 표시</label><button type="button" id="ga-config-import">설정/도곽 JSON 불러오기</button><button type="button" id="ga-config-export">설정/도곽 JSON 저장</button><button type="button" id="ga-config-reset">원본 복원</button><input type="file" accept=".json,application/json" id="ga-config-file" hidden/></div>
|
||||
<div class="ga-editor-columns"><section><h3>표제란 속성 · 원본 위치/글자 크기 유지</h3><div id="ga-frame-fields"></div></section><section><h3>NOTE 원문</h3><label>문자 높이 (A1 기준 mm)<input id="ga-note-height" type="number" min="1.5" max="6" step=".1"/></label><textarea id="ga-note-text" rows="22" aria-label="노트 내용"></textarea><h3>REFERENCE</h3><textarea id="ga-reference-text" rows="5" aria-label="참조 도면 내용"></textarea><div class="ga-setting-pair"><label>키플랜 시점 표기<input id="ga-start-label"/></label><label>키플랜 종점 표기<input id="ga-end-label"/></label></div></section></div>
|
||||
<footer><span id="ga-editor-status" role="status">설정은 프로젝트별로 이 브라우저에 보관합니다. JSON으로 내보내 다른 환경에서 재사용할 수 있습니다.</span><button type="button" class="ga-primary" id="ga-config-apply">적용 · 도면 갱신</button></footer>`;
|
||||
host.append(dialog);
|
||||
const node=<T extends HTMLElement=HTMLElement>(id:string)=>dialog.querySelector<T>(`#${id}`)!;
|
||||
let working=defaultSheet();
|
||||
const populate=(config:SheetConfig)=>{
|
||||
working=structuredClone(config);const template=working.template??REFERENCE_TEMPLATE;
|
||||
node('ga-template-name').textContent=`${template.name} · ${template.source}`;
|
||||
node<HTMLInputElement>('ga-frame-show').checked=working.showFrame;node<HTMLInputElement>('ga-logo-show').checked=working.showLogo;
|
||||
node<HTMLInputElement>('ga-note-height').value=String(working.noteHeight);node<HTMLTextAreaElement>('ga-note-text').value=working.notes;node<HTMLTextAreaElement>('ga-reference-text').value=working.references;
|
||||
node<HTMLInputElement>('ga-start-label').value=working.startLabel;node<HTMLInputElement>('ga-end-label').value=working.endLabel;
|
||||
const fields=node('ga-frame-fields');fields.replaceChildren();
|
||||
for(const field of template.fields){const row=window.document.createElement('label'),label=window.document.createElement('span'),input=window.document.createElement('input');
|
||||
label.textContent=FIELD_LABELS[field.tag]??field.label??field.tag;input.value=working.fields[field.tag]??field.value;input.dataset.tag=field.tag;input.maxLength=500;input.setAttribute('aria-label',`${label.textContent} (${field.tag})`);
|
||||
const small=window.document.createElement('small');small.textContent=field.tag;row.append(label,input,small);fields.append(row);
|
||||
}
|
||||
};
|
||||
const collect=():SheetConfig=>{
|
||||
const fields={...working.fields};for(const input of node('ga-frame-fields').querySelectorAll<HTMLInputElement>('[data-tag]'))fields[input.dataset.tag!]=input.value;
|
||||
return parseSheetConfig({...working,fields,showFrame:node<HTMLInputElement>('ga-frame-show').checked,showLogo:node<HTMLInputElement>('ga-logo-show').checked,noteHeight:Number(node<HTMLInputElement>('ga-note-height').value),notes:node<HTMLTextAreaElement>('ga-note-text').value,references:node<HTMLTextAreaElement>('ga-reference-text').value,startLabel:node<HTMLInputElement>('ga-start-label').value,endLabel:node<HTMLInputElement>('ga-end-label').value});
|
||||
};
|
||||
const status=(s:string)=>node('ga-editor-status').textContent=s;
|
||||
node('ga-editor-close').addEventListener('click',()=>dialog.close());node('ga-config-reset').addEventListener('click',()=>populate(defaultSheet()));
|
||||
node('ga-config-apply').addEventListener('click',()=>{try{apply(collect());dialog.close();}catch(error){status((error as Error).message);}});
|
||||
node('ga-config-import').addEventListener('click',()=>node<HTMLInputElement>('ga-config-file').click());
|
||||
node<HTMLInputElement>('ga-config-file').addEventListener('change',async event=>{
|
||||
const file=(event.target as HTMLInputElement).files?.[0];if(!file)return;
|
||||
try{if(file.size>1500000)throw new Error('설정 파일은 1.5MB 이하만 지원합니다.');populate(parseSheetConfig(JSON.parse(await file.text())));status('불러왔습니다. 적용 버튼으로 현재 도면에 반영하세요.');}catch(error){status((error as Error).message);}(event.target as HTMLInputElement).value='';
|
||||
});
|
||||
node('ga-config-export').addEventListener('click',()=>{try{const c=collect(),url=URL.createObjectURL(new Blob([JSON.stringify({...c,template:c.template??REFERENCE_TEMPLATE},null,2)],{type:'application/json'})),link=window.document.createElement('a');link.href=url;link.download='Pierzainer_도곽_노트.json';link.click();setTimeout(()=>URL.revokeObjectURL(url),1000);}catch(error){status((error as Error).message);}});
|
||||
return{open:value=>{populate(value);dialog.showModal();}};
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { paperEntity, presentationEntities } from './presentation';
|
||||
import type { DrawingDocument, DrawingView, Entity, Point, Source } from './types';
|
||||
import { templateFrame } from './template';
|
||||
|
||||
/** Paper-space entities in actual paper mm (origin bottom-left), shared by SVG and DXF Layout1. */
|
||||
export function viewPaperEntities(doc:DrawingDocument,view:DrawingView):{entity:Entity;index:number}[] {
|
||||
const up=(p:Point):Point=>[p[0],p[1]+doc.paper.height];
|
||||
return presentationEntities(view).map(({entity,index})=>{
|
||||
const e=paperEntity(view,entity);
|
||||
if(e.kind==='line')return{index,entity:{...e,a:up(e.a),b:up(e.b)}};
|
||||
if(e.kind==='polyline')return{index,entity:{...e,points:e.points.map(up)}};
|
||||
if(e.kind==='hatch')return{index,entity:{...e,points:e.points.map(up),holes:e.holes?.map(loop=>loop.map(up))}};
|
||||
if(e.kind==='circle')return{index,entity:{...e,center:up(e.center)}};
|
||||
if(e.kind==='text')return{index,entity:{...e,point:up(e.point)}};
|
||||
throw new Error('Unexpected paper dimension');
|
||||
});
|
||||
}
|
||||
export function sheetFrame(doc:DrawingDocument):Entity[] {
|
||||
const out:Entity[]=templateFrame(doc),{width:w,height:h}=doc.paper,source:Source={member:'sheet',pier:doc.pier,index:0};
|
||||
const p=(x:number,y:number):Point=>[x,h-y];
|
||||
const line=(x0:number,y0:number,x1:number,y1:number)=>out.push({kind:'line',a:p(x0,y0),b:p(x1,y1),layer:'TEXT',source});
|
||||
const text=(x:number,y:number,t:string,size=2.3,align:'left'|'center'='left')=>out.push({kind:'text',point:p(x,y),text:t,height:size,align,layer:'TEXT',source});
|
||||
for(const view of doc.views){
|
||||
if(['section','schedule','heights'].includes(view.id))continue;
|
||||
text(view.slot.x,view.slot.y-3,`${view.title}${view.id==='keyplan'?'':` 1:${view.scale}`}${view.breaks?.length?' (생략)':''}`,3.0);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import { referenceLayer, REFERENCE_ACI, aciPaper } from './cad-style';
|
||||
import { LAYERS, type DrawingDocument, type Entity, type Layer, type ViewId } from './types';
|
||||
import { sheetFrame, viewPaperEntities } from './sheet';
|
||||
import { entityBox } from './occupancy';
|
||||
export { paperPoint } from './presentation';
|
||||
export const escapeXml=(value:string):string=>value.replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]!));
|
||||
|
||||
function renderEntity(e:Entity,height:number):string {
|
||||
const coord=(p:readonly number[])=>`${p[0].toFixed(4)},${(height-p[1]).toFixed(4)}`;
|
||||
if(e.kind==='text'){
|
||||
const [x,y]=[e.point[0],height-e.point[1]];
|
||||
return `<text x="${x}" y="${y}" stroke="none" fill="${aciPaper(e.cad?.color??REFERENCE_ACI[referenceLayer(e)]??7)}" font-size="${e.height}" text-anchor="${e.align==='center'?'middle':e.align==='right'?'end':'start'}"${e.rotation?` transform="rotate(${-e.rotation} ${x} ${y})"`:''}>${escapeXml(e.text)}</text>`;
|
||||
}
|
||||
if(e.kind==='circle')return `<circle cx="${e.center[0]}" cy="${height-e.center[1]}" r="${e.radius}"/>`;
|
||||
if(e.kind==='line')return `<polyline points="${coord(e.a)} ${coord(e.b)}"/>`;
|
||||
if(e.kind==='polyline')return `<${e.closed?'polygon':'polyline'} points="${e.points.map(coord).join(' ')}"/>`;
|
||||
if(e.kind==='hatch'){
|
||||
const fill=e.color&&/^#[0-9a-f]{6}$/i.test(e.color)?e.color:'#cbd1d6';
|
||||
const d=[e.points,...e.holes??[]].map(loop=>`M${loop.map(coord).join('L')}Z`).join('');
|
||||
if(e.pattern==='SOLID')return `<path d="${d}" fill-rule="evenodd" fill="${fill}" stroke="none"/>`;
|
||||
const id=`hz-${Math.abs(Math.round((e.points[0]?.[0]??0)*10000))}-${Math.abs(Math.round((e.points[0]?.[1]??0)*10000))}-${e.source.member}-${e.source.index}`.replace(/[^a-z0-9-]/gi,'');
|
||||
const spacing=Math.max(.1,e.spacing);
|
||||
return `<defs><pattern id="${id}" width="${spacing}" height="${spacing}" patternUnits="userSpaceOnUse" patternTransform="rotate(${-e.angle})"><path d="M0 0H${spacing}" stroke="${fill}" stroke-width=".13"/></pattern></defs><path d="${d}" fill-rule="evenodd" fill="url(#${id})" stroke="none"/>`;
|
||||
}
|
||||
throw new Error('Expand CAD dimensions before paper rendering');
|
||||
}
|
||||
export function drawingSvg(doc:DrawingDocument,visible:ReadonlySet<Layer>,interactive=false,selected?:ViewId):string {
|
||||
const {width,height}=doc.paper;
|
||||
const render=(e:Entity,view:string,index:number)=>{
|
||||
if(!visible.has(e.layer))return'';
|
||||
const style={...LAYERS[e.layer],color:aciPaper(e.cad?.color??REFERENCE_ACI[referenceLayer(e)]??7)};
|
||||
const issue=interactive&&doc.quality?.issues.some(i=>i.view===view&&i.first===index),b=issue?entityBox(e):undefined;
|
||||
const marker=b?`<rect class="ga-issue-box" x="${b.minX}" y="${height-b.maxY}" width="${b.maxX-b.minX}" height="${b.maxY-b.minY}" fill="#ffeded88" stroke="#d52d2d" stroke-width=".2"/>`:'';
|
||||
return `<g fill="none" stroke="${style.color}" stroke-width="${style.weight}"${style.dash?` stroke-dasharray="${style.dash}"`:''}${interactive&&index>=0?` data-entity="${view}:${index}"`:''}><title>${escapeXml(`${e.source.pier} / ${e.source.member} ${e.source.index+1} / ${e.layer}`)}</title>${marker}${renderEntity(e,height)}</g>`;
|
||||
};
|
||||
let body=`<rect width="${width}" height="${height}" fill="white"/>`;
|
||||
if(!selected)body+=sheetFrame(doc).map(e=>render(e,'sheet',-1)).join('');
|
||||
for(const view of doc.views){
|
||||
if(selected?view.id!==selected:['section','heights'].includes(view.id))continue;
|
||||
body+=`<g data-view="${view.id}" aria-label="${escapeXml(view.title)}">${viewPaperEntities(doc,view).map(({entity,index})=>render(entity,view.id,index)).join('')}</g>`;
|
||||
}
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}mm" height="${height}mm" viewBox="0 0 ${width} ${height}" role="img" aria-label="${escapeXml(doc.pier)} 교각 일반도" style="font-family:'Gulim','굴림','Malgun Gothic','Noto Sans KR',sans-serif">${body}</svg>`;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import source from './reference-template';
|
||||
import type { DrawingDocument, Entity, FrameTemplate, Point, Rect, SheetConfig, ViewId } from './types';
|
||||
export const REFERENCE_TEMPLATE=source as unknown as FrameTemplate;
|
||||
export const FIELD_LABELS:Record<string,string>={TD_HSCAL:'도면축척',TD_VERSION:'버전',TD_CNAME:'사업/공사명',TD_CCOMP:'시공자',TD_DSECT:'시설/공구',TD_DWGCLA:'도면분류',TD_STAN:'근거',TD_SUBSTAN:'기준',TD_PPOSE:'용도',TD_DWGCODE:'도면번호',TD_DWGNO:'편철번호',TD_DNAME_MAIN:'도면명',TD_DNAME_TOP:'교각명',TD_DNAME_BOT:'교량/방향',TB_STITL:'상단 부제',TB_MTITIL:'상단 제목',TB_CSCOP:'상단 공종',TB_CNAME:'상단 공사명'};
|
||||
export function defaultSheet():SheetConfig{return{fields:{TD_CNAME:'{{project}}',TD_DNAME_MAIN:'일반도(1/1)',TD_DNAME_TOP:'{{pier}}',TD_DNAME_BOT:'{{bridge}}',TD_HSCAL:'S=1:{{scale}}@{{paper}}',TD_VERSION:''},notes:REFERENCE_TEMPLATE.notes,references:'종평면도 :\n지질주상도 :\n받침배치도 :\n말뚝 상세도 :',noteHeight:3.5,showFrame:true,showLogo:true,startLabel:'시점',endLabel:'종점'};}
|
||||
export const referenceRect=(width:number,height:number,x:number,y:number,w:number,h:number):Rect=>({x:x*width/841,y:y*height/594,width:w*width/841,height:h*height/594});
|
||||
export function templateSlots(width:number,height:number):Record<ViewId,Rect>{
|
||||
const r=(x:number,y:number,w:number,h:number)=>referenceRect(width,height,x,y,w,h);
|
||||
return{front:r(58,68,255,216),side:r(335,68,172,216),top:r(58,370,255,157),footing:r(335,370,178,157),section:r(58,68,255,216),
|
||||
iso:r(538,48,123,137),detail:r(538,225,125,148),schedule:r(538,398,280,146),heights:r(538,398,280,146),keyplan:r(687,42,122,35)};
|
||||
}
|
||||
export function wrappedLines(text:string,width:number,font:number):string[]{
|
||||
const out:string[]=[];
|
||||
for(const paragraph of text.split('\n')){let line='',length=0;for(const c of paragraph){const n=c.charCodeAt(0)>255?1:.55;if(length+n>width/font&&line){out.push(line);line='';length=0;}line+=c;length+=n;}out.push(line);}
|
||||
return out;
|
||||
}
|
||||
export function templateFrame(doc:DrawingDocument):Entity[]{
|
||||
const cfg=doc.settings.sheet??defaultSheet(),template=cfg.template??REFERENCE_TEMPLATE,out:Entity[]=[],factor=Math.min(doc.paper.width/template.width,doc.paper.height/template.height);
|
||||
const dx=(doc.paper.width-template.width*factor)/2,dy=(doc.paper.height-template.height*factor)/2;
|
||||
const p=(p:Point):Point=>[dx+p[0]*factor,dy+p[1]*factor];
|
||||
const source={member:'reference-frame',pier:doc.pier,index:0};
|
||||
const resolve=(s:string)=>s.replace(/\{\{(project|bridge|pier|paper|scale)\}\}/g,(_,key)=>({project:doc.project,bridge:doc.bridge,pier:doc.pier,paper:doc.paper.name,scale:String(doc.views[0].scale)}[key as 'pier']));
|
||||
if(cfg.showFrame){
|
||||
for(const e of template.entities){
|
||||
if(!cfg.showLogo&&e.source.member==='reference-logo')continue;
|
||||
const base={...e,source:{...e.source,pier:doc.pier}};
|
||||
if(e.kind==='line')out.push({...base,kind:'line',a:p(e.a),b:p(e.b)});
|
||||
if(e.kind==='polyline')out.push({...e,points:e.points.map(p),source});
|
||||
if(e.kind==='hatch')out.push({...e,points:e.points.map(p),holes:e.holes?.map(loop=>loop.map(p)),spacing:e.spacing*factor,source});
|
||||
if(e.kind==='circle')out.push({...e,center:p(e.center),radius:e.radius*factor,source});
|
||||
if(e.kind==='text')out.push({...e,point:p(e.point),height:e.height*factor,source});
|
||||
}
|
||||
for(const f of template.fields){const text=resolve(cfg.fields[f.tag]??f.value);out.push({kind:'text',point:p(f.point),text,height:f.height*factor,rotation:f.rotation,layer:'FRAME',source:{member:`frame-field:${f.tag}`,index:0,pier:doc.pier}});}
|
||||
}
|
||||
const k=doc.paper.width/841,sourceNote={member:'reference-note',pier:doc.pier,index:0};
|
||||
const pp=(x:number,y:number):Point=>[x*k,doc.paper.height-y*doc.paper.height/594];
|
||||
const text=(x:number,y:number,t:string,size=3.3)=>out.push({kind:'text',point:pp(x,y),text:t,height:size*k,layer:'FRAME',source:sourceNote});
|
||||
const line=(x:number,y:number,x1:number,y1:number)=>out.push({kind:'line',a:pp(x,y),b:pp(x1,y1),layer:'FRAME',source:sourceNote});
|
||||
line(680,82,820,82);line(680,82,680,347);line(820,82,820,347);line(680,347,820,347);
|
||||
text(680,93,'NOTE',4.2);
|
||||
wrappedLines(cfg.notes,133,cfg.noteHeight).forEach((s,i)=>text(686,104+i*cfg.noteHeight*1.65,s,cfg.noteHeight));
|
||||
text(680,275,'LEGEND',4);line(680,281,820,281);
|
||||
for(const [i,label] of ['코핑','기둥','기초'].entries()){
|
||||
const x=687+i*44;out.push({kind:'hatch',points:[pp(x,287),pp(x+10,287),pp(x+10,292),pp(x,292)],pattern:i===2?'ANSI31':'SOLID',angle:45,spacing:1.2*k,color:'#bfc6cc',layer:'FRAME',source:sourceNote});text(x+12,292,label,3.1);
|
||||
}
|
||||
text(680,310,'REFERENCE',4);
|
||||
cfg.references.split('\n').forEach((s,i)=>text(686,320+i*5.5,s,3.1));
|
||||
text(680,355,'NOTE: 참고 DWG 원문 · 현 프로젝트 조건 확인',2.5);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Import only finite CAD primitives and bounded text, not executable content. */
|
||||
export function parseSheetConfig(value:unknown):SheetConfig{
|
||||
const c=value as SheetConfig;
|
||||
if(!c||typeof c.notes!=='string'||typeof c.references!=='string'||!c.fields||typeof c.fields!=='object'||c.notes.length>20000||c.references.length>4000)throw new Error('도곽/노트 설정 JSON 형식이 올바르지 않습니다.');
|
||||
if(!Number.isFinite(c.noteHeight)||c.noteHeight<1.5||c.noteHeight>6)throw new Error('노트 문자 높이는 A1 기준 1.5~6mm입니다.');
|
||||
if(typeof c.showFrame!=='boolean'||typeof c.showLogo!=='boolean'||typeof c.startLabel!=='string'||typeof c.endLabel!=='string'||c.startLabel.length>200||c.endLabel.length>200)throw new Error('도곽 표시/키플랜 설정이 올바르지 않습니다.');
|
||||
if(Object.values(c.fields).some(v=>typeof v!=='string'||v.length>500))throw new Error('표제란 값이 올바르지 않습니다.');
|
||||
if(c.template){
|
||||
const t=c.template,pt=(p:unknown)=>Array.isArray(p)&&p.length===2&&p.every(v=>typeof v==='number'&&Number.isFinite(v)&&Math.abs(v)<1e6);
|
||||
if(t.version!==1||!Number.isFinite(t.width)||!Number.isFinite(t.height)||t.width<100||t.height<100||t.width>3000||t.height>3000||!Array.isArray(t.entities)||t.entities.length>10000||!Array.isArray(t.fields))throw new Error('사용자 도곽 크기/객체 형식이 올바르지 않습니다.');
|
||||
for(const e of t.entities){if(!e||!e.source||!['FRAME','HATCH','TEXT','STRUCTURE'].includes(e.layer))throw new Error('지원하지 않는 도곽 레이어입니다.');
|
||||
if(e.cad&&(!e.cad.layer||typeof e.cad.layer!=='string'||e.cad.layer.length>255||!Number.isInteger(e.cad.color)||e.cad.color<0||e.cad.color>256||!Number.isInteger(e.cad.lineweight)||e.cad.lineweight< -3||e.cad.lineweight>211||typeof e.cad.linetype!=='string'))throw new Error('도곽 CAD 스타일 형식이 올바르지 않습니다.');
|
||||
const okay=e.kind==='line'?pt(e.a)&&pt(e.b):e.kind==='circle'?pt(e.center)&&Number.isFinite(e.radius)&&e.radius>0:e.kind==='text'?pt(e.point)&&typeof e.text==='string'&&Number.isFinite(e.height)&&e.height>0:e.kind==='polyline'||e.kind==='hatch'?Array.isArray(e.points)&&e.points.length<10000&&e.points.every(pt):false;
|
||||
if(!okay)throw new Error('도곽에 잘못된 CAD 객체가 있습니다.');
|
||||
if(e.kind==='hatch'&&(!['SOLID','ANSI31'].includes(e.pattern)||!Number.isFinite(e.spacing)||e.spacing<=0||!Number.isFinite(e.angle)))throw new Error('해치 형식이 올바르지 않습니다.');
|
||||
if(e.kind==='hatch'&&e.holes&&(!Array.isArray(e.holes)||e.holes.some(loop=>!Array.isArray(loop)||!loop.every(pt))))throw new Error('해치 내부 경계가 올바르지 않습니다.');
|
||||
}
|
||||
for(const f of t.fields)if(typeof f.tag!=='string'||!/^[A-Z][A-Z0-9_]{0,60}$/i.test(f.tag)||['__proto__','constructor','prototype'].includes(f.tag)||typeof f.value!=='string'||!pt(f.point)||!Number.isFinite(f.height)||f.height<=0||!Number.isFinite(f.rotation))throw new Error('도곽 속성 형식이 올바르지 않습니다.');
|
||||
}
|
||||
return{...defaultSheet(),...c};
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { PierInput } from '../src/core/types';
|
||||
import type { PierModel } from '../src/core/geometry';
|
||||
|
||||
export type Point = [number, number];
|
||||
export type Layer = 'STRUCTURE' | 'HIDDEN' | 'CENTER' | 'DIMENSION' | 'TEXT' | 'ELEVATION' | 'BEARING' | 'SECTION' | 'HATCH' | 'FRAME';
|
||||
export type ViewId = 'front' | 'side' | 'top' | 'footing' | 'section' | 'detail' | 'schedule' | 'iso' | 'keyplan' | 'heights';
|
||||
export type Source = { member: string; index: number; pier: string };
|
||||
type Base = { layer: Layer; source: Source;cad?:{layer:string;color:number;linetype:string;lineweight:number;textStyle?:string} };
|
||||
export type Entity = Base & (
|
||||
{ kind: 'line'; a: Point; b: Point } |
|
||||
{ kind: 'polyline'; points: Point[]; closed: boolean } |
|
||||
{ kind: 'circle'; center: Point; radius: number } |
|
||||
{ kind: 'hatch'; points:Point[]; holes?:Point[][];pattern:'SOLID'|'ANSI31'; spacing:number; angle:number; color?:string } |
|
||||
{ kind: 'text'; point: Point; text: string; height: number; align?: 'left' | 'center' | 'right'; rotation?: number } |
|
||||
{ kind: 'dimension'; a: Point; b: Point; position: number; axis: 'x' | 'y'; height: number; value: number }
|
||||
);
|
||||
export type Bounds = { minX: number; minY: number; maxX: number; maxY: number };
|
||||
export type Rect = { x: number; y: number; width: number; height: number };
|
||||
export type ViewBreak = { from:number; to:number; gap:number; member:'column'|'pile' };
|
||||
export type DrawingView = { id: ViewId; title: string; entities: Entity[]; geometryBounds: Bounds; bounds: Bounds; scale: number; slot: Rect; breaks?:ViewBreak[];paperOrigin?:Point };
|
||||
export type QualityIssue={view:string;kind:'text-text'|'text-structure'|'view-overlap'|'outside';first:number;second?:number;message:string};
|
||||
export type DrawingQuality={before:number;after:number;issues:QualityIssue[];occupied:Record<string,Rect>;minimumTextMm:number;layout:string;geometryUnchanged:boolean};
|
||||
export type FrameTemplate={version:1;name:string;source:string;sha256:string;width:number;height:number;entities:Entity[];fields:{tag:string;label:string;value:string;point:Point;height:number;rotation:number}[];notes:string};
|
||||
export type SheetConfig={template?:FrameTemplate;fields:Record<string,string>;notes:string;references:string;noteHeight:number;showFrame:boolean;showLogo:boolean;startLabel:string;endLabel:string};
|
||||
export type DrawingSettings = { paper: 'A3' | 'A2' | 'A1'; scale: number | 'auto'; dimensions: boolean; centers: boolean; elevations: boolean; shorten?:boolean; detailBearing?:string;hatches?:boolean;isoDirection?:number;sheet?:SheetConfig };
|
||||
export type DrawingDocument = { project: string; bridge: string; pier: string; units: 'mm'; views: DrawingView[]; paper: { name: string; width: number; height: number }; warnings: string[]; notes: string[]; settings: DrawingSettings;quality?:DrawingQuality;dwgValidation?:Record<string,unknown> };
|
||||
export type DrawingSnapshot = {
|
||||
project: { name: string; bridge: string; crs: string };
|
||||
selected: string | null;
|
||||
piers: { input: PierInput; model: PierModel | null; dirty: boolean }[];
|
||||
};
|
||||
export const LAYERS: Record<Layer, { title: string; color: string; weight: number; dash?: string }> = {
|
||||
FRAME: { title:'도곽·노트',color:'#26333d',weight:.18 },
|
||||
HATCH: { title:'콘크리트 해치',color:'#aeb9c2',weight:.09 },
|
||||
STRUCTURE: { title: '구조 외형', color: '#18212b', weight: .35 },
|
||||
HIDDEN: { title: '숨은 형상선', color: '#7c8792', weight: .13, dash: '2,1' },
|
||||
CENTER: { title: '중심선', color: '#456e9a', weight: .13, dash: '5,1,1,1' },
|
||||
DIMENSION: { title: '치수', color: '#1b506d', weight: .15 },
|
||||
TEXT: { title: '문자', color: '#233747', weight: .15 },
|
||||
ELEVATION: { title: '표고 EL', color: '#286c65', weight: .15 },
|
||||
BEARING: { title: '받침·블럭', color: '#2d546d', weight: .25 },
|
||||
SECTION: { title: '단면 표시', color: '#8e4e52', weight: .18 },
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Round-trip CAD checks using the existing ground environment's ezdxf installation."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
import ezdxf
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
CACHE = ROOT / '.cache'
|
||||
path = Path(sys.argv[1]) if len(sys.argv) > 1 else CACHE / 'generated-general.dxf'
|
||||
doc = ezdxf.readfile(path)
|
||||
audit = doc.audit()
|
||||
dimensions = list(doc.modelspace().query('DIMENSION'))
|
||||
assert doc.units == 4, 'Model-space units must be mm'
|
||||
assert not audit.errors and not audit.fixes, 'DXF audit must not require repairs'
|
||||
assert dimensions, 'Native DIMENSION entities are required'
|
||||
assert all(d.dxf.geometry in doc.blocks for d in dimensions)
|
||||
assert all(e.has_xdata('PZ_GA') for e in doc.modelspace()), 'Source member linkage missing'
|
||||
paper = doc.layouts.get('Layout1')
|
||||
frames=list(paper.query('INSERT[name=="PZ_CXBLKA1"]'))
|
||||
assert len(frames)==1, 'Native title block insertion missing'
|
||||
frame=frames[0]
|
||||
assert len(frame.attribs)==38, 'Editable title block attributes missing'
|
||||
assert all(a.dxf.get('paperspace',0)==1 and a.dxf.owner in (paper.layout_key,frame.dxf.handle) for a in frame.attribs), 'Attribute paper-space ownership mismatch'
|
||||
assert len({a.dxf.tag for a in frame.attribs})==38, 'Duplicate title block tags'
|
||||
assert len(list(doc.blocks.get('PZ_CXBLKA1').query('ATTDEF')))==38
|
||||
assert '_Dot' in doc.blocks, 'Reference dimension dot block missing'
|
||||
assert all(doc.dimstyles.get(d.dxf.dimstyle).dxf.dimblk=='_Dot' for d in dimensions)
|
||||
assert len(paper) > 100, 'Shortened general arrangement is missing from Layout1'
|
||||
assert all(e.has_xdata('PZ_GA') for e in paper), 'Paper member linkage missing'
|
||||
assert any(e.dxftype() == 'TEXT' and '교좌면' in e.dxf.text for e in paper), 'Bearing schedule missing'
|
||||
for layer in ['STRUCTURE', 'HIDDEN', 'CENTER', 'DIMENSION', 'TEXT', 'ELEVATION', 'BEARING', 'SECTION']:
|
||||
assert layer in doc.layers, layer
|
||||
delta = None
|
||||
if path.name == 'generated-general.dxf':
|
||||
expected = json.loads((CACHE / 'expected-dimensions.json').read_text())
|
||||
assert len(dimensions) == len(expected)
|
||||
delta = max(abs(float(d.get_measurement()) - value) for d, value in zip(dimensions, expected))
|
||||
assert delta < 1e-6, f'Native dimension measurement changed: {delta}'
|
||||
report = dict(file=path.name, units='mm', entities=len(doc.modelspace()), dimensions=len(dimensions),
|
||||
paper_entities=len(paper), audit_errors=len(audit.errors), audit_fixes=len(audit.fixes), max_dimension_delta_mm=delta)
|
||||
CACHE.mkdir(exist_ok=True)
|
||||
(CACHE / ('dxf-validation-' + path.stem + '.json')).write_text(json.dumps(report, indent=2), encoding='utf-8')
|
||||
print(json.dumps(report, indent=2))
|
||||
@@ -0,0 +1,29 @@
|
||||
.ga-workspace { display:grid; grid-template-columns:245px minmax(0,1fr); gap:12px; padding:0 16px 12px; height:calc(100dvh - 120px); min-height:560px; color:var(--text,#1d2733); font-size:12px; }
|
||||
.ga-workspace[hidden] { display:none; }
|
||||
.ga-workspace button,.ga-workspace select,.ga-workspace input { font:inherit; }
|
||||
.ga-workspace button { border:1px solid #cad5de; background:white; color:#273d51; border-radius:5px; padding:8px 11px; cursor:pointer; }
|
||||
.ga-workspace button:hover { background:#eef5fc; border-color:#7b9dbc; }
|
||||
.ga-workspace button:disabled { opacity:.45; cursor:default; }
|
||||
.ga-workspace .ga-primary { color:white; border-color:#1f5fa8; background:#1f5fa8; }
|
||||
.ga-settings { background:white; border:1px solid #d5dce3; border-radius:8px; overflow:auto; padding:16px; }
|
||||
.ga-workspace .ga-settings input[type="checkbox"] { width:14px; height:14px; min-height:0; margin:0; padding:0; flex:0 0 14px; }
|
||||
.ga-workspace .ga-settings fieldset label { min-height:24px; margin:0; flex-direction:row; }
|
||||
.ga-sidebar-title { font-size:15px; font-weight:700; margin-bottom:18px; }.ga-sidebar-title span { display:block; color:#7890a5; font-size:8px; letter-spacing:.12em; margin-top:4px; }
|
||||
.ga-settings > label,.ga-setting-pair label { display:flex; flex-direction:column; gap:5px; margin-bottom:13px; color:#526677; }
|
||||
.ga-settings select { width:100%; padding:8px; background:#fff; border:1px solid #d1dce5; border-radius:5px; color:#243c50; }
|
||||
.ga-setting-pair { display:grid; grid-template-columns:1fr 1.3fr; gap:8px; }.ga-settings fieldset { border:0; padding:0; margin:15px 0; }.ga-settings legend { font-weight:700; margin-bottom:8px; }.ga-settings fieldset label { display:flex; align-items:center; gap:7px; padding:4px 0; }.ga-settings input { accent-color:#1f5fa8; }
|
||||
#ga-regenerate { width:100%; margin-bottom:10px; }.ga-layers label i { width:9px;height:9px;border-radius:2px; }.ga-layers label small { margin-left:auto; font-size:8px; color:#8a99a7; }
|
||||
.ga-notices { border-top:1px solid #dfe6ec; padding-top:14px; font-size:10px; line-height:1.8; color:#697d8d; }.ga-notices h3 { font-size:12px;color:#304c62; }.ga-notices ul { padding-left:14px;color:#8b602a; }
|
||||
.ga-main { display:flex; flex-direction:column; min-width:0; background:white; border:1px solid #d5dce3; border-radius:8px; overflow:hidden; }
|
||||
.ga-toolbar { display:flex; justify-content:space-between; align-items:center; gap:10px; flex-wrap:wrap; border-bottom:1px solid #d9e2e9; padding:12px 15px; background:#fff; }.ga-toolbar>div:first-child { min-width:140px; }.ga-toolbar span { display:block;font-size:9px;color:#7a8b99; max-width:400px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis; }.ga-toolbar strong { display:block;font-size:15px; margin-top:3px; }.ga-toolbar-actions { display:flex;flex-wrap:wrap;gap:6px; }.ga-toolbar-actions button { padding:7px 9px; font-size:11px; }
|
||||
.ga-viewer { position:relative; flex:1; min-height:350px; background-color:#e8edf2; background-image:linear-gradient(#cfd9e438 1px,transparent 1px),linear-gradient(90deg,#cfd9e438 1px,transparent 1px);background-size:22px 22px; overflow:hidden; }
|
||||
.ga-canvas { width:100%;height:100%;position:absolute;inset:0;cursor:grab;touch-action:none; }.ga-canvas.is-panning { cursor:grabbing; }.ga-canvas>svg { display:block;width:100%;height:100%; user-select:none; }.ga-canvas [data-entity]:hover { filter:drop-shadow(0 0 .25px #087fcb); }
|
||||
#ga-empty { position:absolute;inset:0;display:grid;place-content:center;color:#6d8295;font-size:14px;padding:24px;text-align:center; }#ga-empty[hidden]{display:none;}
|
||||
.ga-zoom { position:absolute;right:15px;bottom:15px;display:flex;gap:5px;box-shadow:0 4px 15px #17324914; }.ga-zoom button{background:#fffffff0;min-width:34px;}
|
||||
.ga-status { display:flex;justify-content:space-between;gap:10px;padding:7px 14px;border-top:1px solid #dce4eb;color:#708393;font-size:10px; }
|
||||
.ga-print-dialog { width:min(1100px,94vw); max-height:92dvh; overflow:auto; padding:15px; border:1px solid #bdccd8; border-radius:10px; background:#e5ebf0; }.ga-print-dialog::backdrop { background:#14253688; }.ga-print-toolbar { display:flex;align-items:center;gap:12px; margin-bottom:15px; }.ga-print-toolbar span { flex:1; font-size:11px;color:#657b8e; }#ga-print-sheet svg { width:100%;height:auto;display:block;box-shadow:0 2px 12px #233e5320; }
|
||||
@media(max-width:1100px){.ga-workspace{grid-template-columns:210px minmax(0,1fr);height:calc(100dvh - 150px)}.ga-toolbar-actions{gap:4px}.ga-toolbar-actions button{font-size:10px;padding:6px}}
|
||||
@media(max-width:760px){.ga-workspace{grid-template-columns:165px minmax(0,1fr);padding:0 6px 6px;gap:6px;min-height:650px}.ga-settings{padding:10px}.ga-setting-pair{grid-template-columns:1fr}.ga-toolbar{padding:9px}.ga-status{flex-direction:column}.ga-toolbar-actions button{min-height:30px}}
|
||||
.ga-sheet-dialog { width:min(1150px,94vw); max-height:92dvh; overflow:auto; border:1px solid #bdccd8; border-radius:9px; padding:20px; background:#fff; color:#243c50; }
|
||||
.ga-sheet-dialog::backdrop { background:#14253699; }.ga-sheet-dialog header,.ga-sheet-dialog footer { display:flex;justify-content:space-between;gap:20px;align-items:center; }.ga-sheet-dialog strong{font-size:18px}.ga-sheet-dialog p{font-size:11px;line-height:1.7;color:#6f8294}.ga-editor-tools{display:flex;gap:10px;align-items:center;flex-wrap:wrap;border-block:1px solid #dfe6ee;padding:12px 0}.ga-editor-tools label{display:flex;gap:5px;align-items:center}.ga-sheet-dialog input[type=checkbox]{width:14px;height:14px;min-height:0}.ga-editor-columns{display:grid;grid-template-columns:1fr 1fr;gap:24px}.ga-editor-columns section{min-width:0}.ga-editor-columns h3{font-size:13px;margin-top:20px}.ga-editor-columns label{display:grid;grid-template-columns:150px minmax(0,1fr);gap:5px;font-size:11px;align-items:center;margin:8px 0}.ga-editor-columns label small{grid-column:2;color:#92a0ac;font-size:9px}.ga-editor-columns input,.ga-editor-columns textarea{box-sizing:border-box;width:100%;border:1px solid #cbd8e3;border-radius:4px;padding:7px;font:inherit;color:#203e55;background:white;min-height:0}.ga-editor-columns textarea{font-size:12px;line-height:1.7;resize:vertical}.ga-sheet-dialog footer{position:sticky;bottom:-20px;padding:14px 0;background:#fff;border-top:1px solid #dce4ec;font-size:11px}.ga-editor-intro{background:#f1f7fc;padding:10px}.ga-editor-columns .ga-setting-pair label{display:flex;flex-direction:column;align-items:stretch}@media(max-width:900px){.ga-editor-columns{grid-template-columns:1fr}.ga-editor-columns label{grid-template-columns:110px minmax(0,1fr)}}
|
||||
.ga-issue-box{display:none}.ga-show-quality .ga-issue-box{display:block}.ga-workspace .ga-quality-dialog{width:min(850px,92vw)}.ga-quality-row{display:flex;align-items:center;gap:12px;border-bottom:1px solid #e1e8ee;padding:9px 0;font-size:12px}.ga-quality-dialog [data-list]{padding:20px 0;min-height:60px}.ga-quality-dialog label{display:flex;align-items:center;gap:8px}.ga-quality-dialog input{height:14px;width:14px;min-height:0}
|
||||
@@ -0,0 +1,130 @@
|
||||
import './workspace.css';
|
||||
import { DEFAULT_SETTINGS, generateDrawing, VIEW_TITLES, formatMm } from './drawing';
|
||||
import { drawingSvg } from './svg';
|
||||
import { exportDxf } from './dxf';
|
||||
import { LAYERS, type DrawingDocument, type DrawingSettings, type DrawingSnapshot, type Layer, type Rect, type ViewId } from './types';
|
||||
import type { PierModel } from '../src/core/geometry';
|
||||
import { defaultSheet, parseSheetConfig } from './template';
|
||||
import { createSheetEditor } from './sheet-editor';
|
||||
import { shortenedBounds } from './breaks';
|
||||
import { createQualityPanel } from './quality-panel';
|
||||
|
||||
export function createDrawingWorkspace(host: HTMLElement,read:()=>DrawingSnapshot,select:(id:string)=>void): { update:()=>void } {
|
||||
host.className='ga-workspace';
|
||||
host.innerHTML=`<aside class="ga-settings"><div class="ga-sidebar-title">도면 설정 <span>GENERAL ARRANGEMENT</span></div>
|
||||
<label>교각 선택<select id="ga-pier" aria-label="도면 교각 선택"></select></label>
|
||||
<label>도면 종류<select id="ga-view" aria-label="표시 도면"><option value="sheet">일반도 · 전체</option>${Object.entries(VIEW_TITLES).map(([id,label])=>`<option value="${id}">${label}</option>`).join('')}</select></label>
|
||||
<label>상세 A 받침 선택<select id="ga-bearing" aria-label="받침 상세 선택"></select></label>
|
||||
<label>아이소메트릭 방향<select id="ga-iso"><option value="0">등각 1</option><option value="1">등각 2</option><option value="2">등각 3</option><option value="3">등각 4</option></select></label>
|
||||
<button id="ga-sheet-editor" type="button">도곽 · 노트 편집 / 불러오기</button>
|
||||
<div class="ga-setting-pair"><label>용지<select id="ga-paper"><option>A3</option><option>A2</option><option>A1</option></select></label>
|
||||
<label>도면 축척<select id="ga-scale"><option value="auto">자동 · 뷰별</option>${[25,50,75,100,150,200,250,300,400,500,750,1000].map(n=>`<option value="${n}">1:${n}</option>`).join('')}</select></label></div>
|
||||
<fieldset><legend>표시 설정</legend><label><input id="ga-hatches" type="checkbox" checked/> 콘크리트 해치</label><label><input id="ga-shorten" type="checkbox" checked/> 기둥·말뚝 생략 표현</label><label><input id="ga-dimensions" type="checkbox" checked/> 치수선</label><label><input id="ga-centers" type="checkbox" checked/> 중심선</label><label><input id="ga-elevations" type="checkbox" checked/> 표고 EL</label></fieldset>
|
||||
<button id="ga-regenerate" type="button" class="ga-primary">도면 재생성</button>
|
||||
<fieldset class="ga-layers"><legend>도면 레이어</legend>${Object.entries(LAYERS).map(([id,layer])=>`<label><input type="checkbox" data-layer="${id}" ${id==='HIDDEN'?'':'checked'}/><i style="background:${layer.color}"></i><span>${layer.title}</span><small>${id}</small></label>`).join('')}</fieldset>
|
||||
<section class="ga-notices"><h3>데이터 확인</h3><p id="ga-data-note"></p><ul id="ga-warnings"></ul></section></aside>
|
||||
<section class="ga-main"><header class="ga-toolbar"><div><span id="ga-project">프로젝트 미지정</span><strong id="ga-title">교각 일반도</strong></div><div class="ga-toolbar-actions"><button id="ga-generate" type="button">도면 생성</button><button id="ga-update" type="button">업데이트</button><button id="ga-fit" type="button">전체 맞춤</button><button id="ga-quality" type="button">품질 검사</button><button id="ga-dwg" class="ga-primary" type="button">DWG 내보내기</button><button id="ga-dxf" type="button">DXF 내보내기</button><button id="ga-preview" type="button">출력 미리보기</button></div></header>
|
||||
<div class="ga-viewer"><div id="ga-empty">교각 형상을 생성하면 일반도가 표시됩니다.</div><div id="ga-canvas" class="ga-canvas" aria-label="교각 일반도 CAD 미리보기" tabindex="0"></div>
|
||||
<div class="ga-zoom"><button id="ga-zoom-in" type="button" aria-label="도면 확대">+</button><button id="ga-zoom-out" type="button" aria-label="도면 축소">−</button><button id="ga-svg" type="button">SVG 저장</button></div></div>
|
||||
<footer class="ga-status"><span id="ga-object-info">휠 확대·축소 / 드래그 이동 / 객체 클릭</span><span id="ga-count">모델 mm · EL m</span></footer></section>
|
||||
<dialog class="ga-print-dialog" id="ga-print-dialog" aria-label="일반도 출력 미리보기"><div class="ga-print-toolbar"><strong>출력 미리보기</strong><span id="ga-print-paper"></span><button id="ga-print" class="ga-primary" type="button">인쇄 / PDF</button><button id="ga-print-close" type="button">닫기</button></div><div id="ga-print-sheet"></div></dialog>`;
|
||||
const node=<T extends HTMLElement=HTMLElement>(id:string)=>host.querySelector<T>(`#${id}`)!;
|
||||
const pier=node<HTMLSelectElement>('ga-pier'),view=node<HTMLSelectElement>('ga-view'),paper=node<HTMLSelectElement>('ga-paper'),scale=node<HTMLSelectElement>('ga-scale');
|
||||
const bearing=node<HTMLSelectElement>('ga-bearing');
|
||||
const canvas=node('ga-canvas'),empty=node('ga-empty'),warnings=node('ga-warnings'),note=node('ga-data-note');
|
||||
const visible=new Set<Layer>((Object.keys(LAYERS) as Layer[]).filter(l=>l!=='HIDDEN'));
|
||||
let sheetConfig=defaultSheet(),sheetKey='',lastContextModels:(PierModel|null)[]=[];
|
||||
let document:DrawingDocument|undefined,lastModel:PierModel|undefined,lastKey='',optionKey='',camera:Rect={x:0,y:0,width:420,height:297};
|
||||
let press:{x:number;y:number;anchor:DOMPoint;entity?:string}|undefined;
|
||||
const settings=():DrawingSettings=>({paper:paper.value as DrawingSettings['paper'],scale:scale.value==='auto'?'auto':Number(scale.value),
|
||||
dimensions:node<HTMLInputElement>('ga-dimensions').checked,centers:node<HTMLInputElement>('ga-centers').checked,elevations:node<HTMLInputElement>('ga-elevations').checked,
|
||||
shorten:node<HTMLInputElement>('ga-shorten').checked,detailBearing:bearing.value,hatches:node<HTMLInputElement>('ga-hatches').checked,isoDirection:Number(node<HTMLSelectElement>('ga-iso').value),sheet:sheetConfig});
|
||||
const svg=()=>canvas.querySelector<SVGSVGElement>('svg');
|
||||
const setCamera=()=>svg()?.setAttribute('viewBox',`${camera.x} ${camera.y} ${camera.width} ${camera.height}`);
|
||||
const fit=()=>{
|
||||
if(!document)return;
|
||||
const target=document.views.find(v=>v.id===view.value);
|
||||
if(target){const b=shortenedBounds(target.bounds,target.breaks),width=Math.max(target.slot.width,(b.maxX-b.minX)/target.scale),height=Math.max(target.slot.height,(b.maxY-b.minY)/target.scale);camera={x:target.slot.x+(target.slot.width-width)/2-5,y:target.slot.y+(target.slot.height-height)/2-5,width:width+10,height:height+12};}
|
||||
else camera={x:0,y:0,width:document.paper.width,height:document.paper.height};
|
||||
setCamera();
|
||||
};
|
||||
const redraw=(reset=false)=>{
|
||||
if(!document){canvas.replaceChildren();empty.hidden=false;return;}
|
||||
empty.hidden=true;canvas.innerHTML=drawingSvg(document,visible,true,view.value==='sheet'?undefined:view.value as ViewId);if(reset)fit();else setCamera();
|
||||
node('ga-count').textContent=`${document.views.reduce((n,v)=>n+v.entities.filter(e=>visible.has(e.layer)).length,0)} 객체 · 간섭 ${document.quality?.after??'미검사'}건 · mm / EL m`;
|
||||
};
|
||||
const enable=(on:boolean)=>['ga-dxf','ga-dwg','ga-quality','ga-svg','ga-preview','ga-generate','ga-update','ga-regenerate'].forEach(id=>{node<HTMLButtonElement>(id).disabled=!on;});
|
||||
const update=(force=false)=>{
|
||||
host.style.height=`${Math.max(480,window.innerHeight-host.getBoundingClientRect().top-12)}px`;
|
||||
const snapshot=read(),items=snapshot.piers;
|
||||
const nextSheetKey=`pierzainer.ga.sheet.v1:${snapshot.project.name}|${snapshot.project.bridge}`;
|
||||
if(sheetKey!==nextSheetKey){sheetKey=nextSheetKey;sheetConfig=defaultSheet();try{const saved=localStorage.getItem(sheetKey);if(saved)sheetConfig=parseSheetConfig(JSON.parse(saved));}catch{/* Keep original template if browser storage is unavailable or corrupt. */}}
|
||||
const nextOptionKey=items.map(p=>`${p.input.name}:${p.input.type}:${!!p.model}`).join('|');
|
||||
if(optionKey!==nextOptionKey){optionKey=nextOptionKey;pier.replaceChildren();
|
||||
if(!items.length)pier.add(new Option('교각 없음',''));
|
||||
for(const p of items)pier.add(new Option(`${p.input.name} · ${p.input.type}${p.model?'':' · 형상 없음'}`,p.input.name));}
|
||||
pier.value=snapshot.selected??items[0]?.input.name??'';
|
||||
const selected=items.find(p=>p.input.name===pier.value);
|
||||
node('ga-project').textContent=[snapshot.project.name,snapshot.project.bridge].filter(Boolean).join(' · ')||'프로젝트 미지정';
|
||||
node('ga-title').textContent=selected?`${selected.input.name} 교각 일반도`:'교각 일반도';
|
||||
host.dataset.pier=selected?.input.name??'';
|
||||
note.textContent=selected?.dirty?'현재 입력이 3D 형상에 아직 반영되지 않았습니다. 상단에서 형상을 다시 생성하면 도면도 갱신됩니다.':'현재 생성된 3D 형상 데이터를 사용합니다. 모델 변경 시 자동 갱신합니다.';
|
||||
enable(!!selected?.model);warnings.replaceChildren();
|
||||
if(!selected?.model){document=undefined;lastModel=undefined;empty.textContent=selected?'선택한 교각의 형상이 없습니다. 형상 생성 오류를 먼저 확인하세요.':'교각 형상을 생성하면 일반도가 표시됩니다.';redraw();return;}
|
||||
if(lastModel!==selected.model){const previous=bearing.value;bearing.replaceChildren();for(const b of selected.model.blocks)bearing.add(new Option(`${b.row} ${b.no}`,`${b.row}:${b.no}`));if(Array.from(bearing.options).some(o=>o.value===previous))bearing.value=previous;}
|
||||
const key=JSON.stringify([settings(),snapshot.project.name,snapshot.project.bridge]);
|
||||
try{
|
||||
const models=items.map(p=>p.model),contextChanged=models.length!==lastContextModels.length||models.some((m,i)=>m!==lastContextModels[i]);
|
||||
if(force||lastModel!==selected.model||lastKey!==key||contextChanged){document=generateDrawing(selected.model,selected.input,snapshot.project,settings(),{models:models.filter((m):m is PierModel=>!!m)});lastModel=selected.model;lastContextModels=models;lastKey=key;redraw(true);}
|
||||
for(const message of document?.warnings??[]){const li=window.document.createElement('li');li.textContent=message;warnings.append(li);}
|
||||
}catch(error){document=undefined;lastModel=undefined;enable(false);empty.textContent=(error as Error).message;redraw();}
|
||||
};
|
||||
const download=(text:string,extension:string,mime:string)=>{
|
||||
if(!document)return;
|
||||
const url=URL.createObjectURL(new Blob([text],{type:mime})),link=window.document.createElement('a');
|
||||
link.href=url;link.download=`${document.bridge||document.project||'교량'}_${document.pier}_일반도.${extension}`.replace(/[\\/:*?"<>|]/g,'_');link.click();window.setTimeout(()=>URL.revokeObjectURL(url),1000);
|
||||
};
|
||||
pier.addEventListener('change',()=>select(pier.value));view.addEventListener('change',()=>redraw(true));
|
||||
[paper,scale,bearing,node('ga-iso'),node('ga-hatches'),node('ga-shorten'),node('ga-dimensions'),node('ga-centers'),node('ga-elevations')].forEach(e=>e.addEventListener('change',()=>update(true)));
|
||||
const editor=createSheetEditor(host,config=>{sheetConfig=config;try{localStorage.setItem(sheetKey,JSON.stringify(config));}catch{node('ga-object-info').textContent='브라우저 저장 공간이 부족합니다. 도곽/노트 JSON 저장을 사용하세요.';}update(true);});
|
||||
node('ga-sheet-editor').addEventListener('click',()=>editor.open(sheetConfig));
|
||||
['ga-generate','ga-update','ga-regenerate'].forEach(id=>node(id).addEventListener('click',()=>update(true)));
|
||||
node('ga-fit').addEventListener('click',fit);
|
||||
host.querySelectorAll<HTMLInputElement>('[data-layer]').forEach(checkbox=>checkbox.addEventListener('change',()=>{const layer=checkbox.dataset.layer as Layer;if(checkbox.checked)visible.add(layer);else visible.delete(layer);redraw();}));
|
||||
const point=(event:{clientX:number;clientY:number}):DOMPoint=>new DOMPoint(event.clientX,event.clientY).matrixTransform(svg()!.getScreenCTM()!.inverse());
|
||||
const zoom=(factor:number,anchor?:DOMPoint)=>{
|
||||
if(!document||!svg())return;
|
||||
const target=anchor??new DOMPoint(camera.x+camera.width/2,camera.y+camera.height/2),width=Math.min(document.paper.width*20,Math.max(document.paper.width*.025,camera.width*factor)),ratio=width/camera.width;
|
||||
camera={x:target.x-(target.x-camera.x)*ratio,y:target.y-(target.y-camera.y)*ratio,width,height:camera.height*ratio};setCamera();
|
||||
};
|
||||
node('ga-zoom-in').addEventListener('click',()=>zoom(.8));node('ga-zoom-out').addEventListener('click',()=>zoom(1.25));
|
||||
canvas.addEventListener('wheel',event=>{if(svg()){event.preventDefault();zoom(Math.exp(Math.max(-.4,Math.min(.4,event.deltaY*.0015))),point(event));}},{passive:false});
|
||||
canvas.addEventListener('pointerdown',event=>{if(!svg()||event.button>1)return;event.preventDefault();press={x:event.clientX,y:event.clientY,anchor:point(event),entity:(event.target as Element).closest<SVGGElement>('[data-entity]')?.dataset.entity};canvas.setPointerCapture(event.pointerId);canvas.classList.add('is-panning');});
|
||||
canvas.addEventListener('pointermove',event=>{if(!press||!svg())return;const current=point(event);camera.x+=press.anchor.x-current.x;camera.y+=press.anchor.y-current.y;setCamera();});
|
||||
const release=(event:PointerEvent)=>{if(!press)return;const clicked=Math.hypot(event.clientX-press.x,event.clientY-press.y)<4;if(clicked&&press.entity&&document){const [viewId,index]=press.entity.split(':');const e=document.views.find(v=>v.id===viewId)?.entities[Number(index)];if(e)node('ga-object-info').textContent=`${e.source.pier} / ${e.source.member} ${e.source.index+1} · ${e.layer}${e.kind==='dimension'?` · ${formatMm(e.value)} mm`:''}`;}press=undefined;canvas.classList.remove('is-panning');};
|
||||
canvas.addEventListener('pointerup',release);canvas.addEventListener('pointercancel',()=>{press=undefined;canvas.classList.remove('is-panning');});
|
||||
node('ga-dxf').addEventListener('click',()=>{if(document)download(exportDxf(document,visible),'dxf','application/dxf;charset=utf-8');});
|
||||
const qualityPanel=createQualityPanel(host,()=>document,id=>{view.value=id;redraw(true);});
|
||||
node('ga-quality').addEventListener('click',qualityPanel);
|
||||
node('ga-dwg').addEventListener('click',async()=>{
|
||||
const snapshot=document;if(!snapshot)return;const button=node<HTMLButtonElement>('ga-dwg');button.disabled=true;button.textContent='DWG 검증 중…';
|
||||
try{
|
||||
const response=await fetch('/__pierzainer/general-arrangement/dwg',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({dxf:exportDxf(snapshot,new Set(visible))})});
|
||||
if(!response.ok){const error=await response.json().catch(()=>({error:'DWG 출력 서버를 찾을 수 없습니다. 개발 서버를 다시 실행하세요.'}));throw new Error(error.error);}
|
||||
snapshot.dwgValidation=JSON.parse(decodeURIComponent(response.headers.get('X-Pierzainer-Validation')??'%7B%7D'));
|
||||
const url=URL.createObjectURL(await response.blob()),a=window.document.createElement('a');a.href=url;a.download=`${snapshot.bridge||snapshot.project}_${snapshot.pier}_일반도.dwg`.replace(/[\\/:*?"<>|]/g,'_');a.click();setTimeout(()=>URL.revokeObjectURL(url),1000);
|
||||
node('ga-object-info').textContent='DWG 2000 변환 · 내부 객체/치수 비교 통과(CAD 직접 열기 미검증). 열리지 않으면 DXF 내보내기를 사용하고 CAD 오류 문구를 확인하세요.';
|
||||
}catch(error){node('ga-object-info').textContent=`DWG 출력 실패: ${(error as Error).message}`;}finally{button.disabled=false;button.textContent='DWG 내보내기';}
|
||||
});
|
||||
node('ga-svg').addEventListener('click',()=>{if(document)download(drawingSvg(document,visible),'svg','image/svg+xml;charset=utf-8');});
|
||||
const preview=node<HTMLDialogElement>('ga-print-dialog');
|
||||
node('ga-preview').addEventListener('click',()=>{if(!document)return;node('ga-print-sheet').innerHTML=drawingSvg(document,visible);node('ga-print-paper').textContent=`${document.paper.name} 가로 · 각 뷰 축척 적용`;preview.showModal();});
|
||||
node('ga-print-close').addEventListener('click',()=>preview.close());
|
||||
node('ga-print').addEventListener('click',()=>{
|
||||
if(!document)return;const frame=window.document.createElement('iframe');frame.style.cssText='position:fixed;width:1px;height:1px;left:-9999px;border:0';
|
||||
frame.srcdoc=`<!doctype html><html lang="ko"><head><meta charset="utf-8"><title>교각 일반도</title><style>@page{size:${document.paper.width}mm ${document.paper.height}mm;margin:0}html,body{margin:0}svg{display:block}</style></head><body>${drawingSvg(document,visible)}</body></html>`;
|
||||
frame.onload=()=>{frame.contentWindow?.addEventListener('afterprint',()=>frame.remove(),{once:true});frame.contentWindow?.print();};host.append(frame);
|
||||
});
|
||||
window.addEventListener('resize',()=>{if(!host.hidden)host.style.height=`${Math.max(480,window.innerHeight-host.getBoundingClientRect().top-12)}px`;});
|
||||
return{update:()=>update()};
|
||||
}
|
||||
Reference in new issue
Block a user