Files
Pier_v1/수량산출/model.ts
T

51 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {bearingDimensions} from './bearing-dimensions';
import {straightCopingFormulas} from './coping-formulas';
import type {PierInput} from '../src/core/types';
import {DIM,type PierModel} from '../src/core/geometry';
import {variables,quantityRows,template,templateHash} from './template-data';
import {FormulaBook,cellKey,type Overrides} from './formula';
import {DETAIL,INPUTS,extraNumbers,specifications} from './schema';
export {DETAIL,INPUTS,extraNumbers,specifications} from './schema';
export type QuantityInputs={values:Record<string,number|string|null>;reviewed:boolean;lastSignature?:string};
declare module '../src/core/types'{interface PierInput{quantityTakeoff?:QuantityInputs}}
export type Mapped={key:string;name:string;symbol:string;unit:string;value:number|null;source:string;automatic:boolean;notApplicable?:boolean};
export function withTemplateDefaults(stored:QuantityInputs):QuantityInputs{
const values={...stored.values};
for(const v of extraNumbers.filter(x=>x.key!=='rebar'))if(values[v.key]==null||values[v.key]==='')values[v.key]=Number(template[DETAIL][v.cell].cached);
for(const v of specifications)if(values[v.key]==null||values[v.key]==='')values[v.key]=String(template[DETAIL][v.cell].value);
return {...stored,values};
}
export function automaticInputs(input:PierInput,m:PierModel):Mapped[]{
const uniform=(v:(number|undefined)[])=>v.length&&v.every(x=>x!==undefined&&Number.isFinite(x)&&Math.abs(x!-v[0]!)<1e-7)?v[0]!:null;
const footing=m.footings[0],nopiles=m.piles.length===0,bearing=bearingDimensions(m);
const slope=m.copingL>0&&m.segments.length?((m.segments[m.segments.length-1].top-m.segments[0].top)/m.copingL)*100:null;
const auto:Record<number,[number|null,string]>={18:[m.copingRefTop,'model.copingRefTop (원본 Zb 의미 검토)'],19:[input.groundEl,'input.groundEl'],20:[input.groundWaterOn===true&&Number.isFinite(input.groundWaterEl)?input.groundWaterEl!:null,'input.groundWaterEl / 지하수 미지정은 직접 확인'],24:[m.copingL,'model.copingL'],25:[m.copingB,'model.copingB'],26:[m.h2,'model.h2 · 하부 테이퍼 높이'],27:[m.h1,'model.h1 · 상부 높이'],28:[slope,'모델 양단 상면 높이차/Lc ×100 · 단차 포함 등가경사'],29:[0,'현재 코핑 모델은 수직 끝단 · 별도 돌출부 없음'],30:[null,'직선 모델: 곡률 없음 · 원본 곡률항의 직선 극한 사용'],31:[uniform(m.columns.map(c=>c.diameter)),'model.columns[].diameter'],32:[footing?.bottom??null,'model.footings[0].bottom'],33:[footing?.length??null,'model.footings[0].length'],34:[footing?.width??null,'model.footings[0].width'],35:[footing?footing.top-footing.bottom:null,'기초 상면-하면'],36:[nopiles?0:uniform(m.piles.map(p=>p.diameter)),'model.piles[].diameter'],37:[nopiles?0:input.pileThicknessMm!=null?input.pileThicknessMm/1000:null,'입력 말뚝두께 mm→m'],38:[nopiles?0:uniform(m.piles.map(p=>p.top-p.bottom)),'말뚝 상면-하면'],39:[m.piles.length,'model.piles.length'],40:[bearing.length,'3D 받침 본체: (plan.A+margin)/1000−0.2 m'],41:[bearing.width,'3D 받침 본체: (plan.B+margin)/1000−0.2 m'],42:[m.blocks.length,'model.blocks.length'],43:[m.integratedBlocks.length?null:DIM.blockSize,'모델 분리블럭 크기'],44:[m.integratedBlocks.length?null:DIM.blockSize,'모델 분리블럭 크기'],45:[m.blocks.length?m.blocks.reduce((s,b)=>s+b.top-b.bottom,0)/m.blocks.length:null,'동일 평면 블럭 높이 합/Nb (원식의 등가 높이)'],46:[uniform(m.blocks.map(b=>b.plan?.T!==undefined?b.plan.T/1000:undefined))??(input.solplate?.T!=null?input.solplate.T/1000:null),'실제 솔플레이트 plan.T 또는 입력 공통 T mm→m']};
return variables.map(v=>{const a=auto[v.row];return {...v,value:a?.[0]??null,source:a?.[1]??`${INPUTS}!N${v.row} · 모델에 없는 조건`,automatic:a?.[0]!=null||v.row===30,notApplicable:v.row===30};});
}
export function geometrySignature(input:PierInput,m:PierModel):string{const {quantityTakeoff,...raw}=input;return JSON.stringify({raw,auto:automaticInputs(input,m),version:templateHash});}
export function supportIssues(input:PierInput,m:PierModel):string[]{const issues:string[]=[];if(m.columns.length!==1)issues.push('원본은 기둥 1개 산식입니다. 다기둥 수량 규칙이 없어 자동 완료할 수 없습니다.');if(m.footings.length!==1)issues.push('원본은 기초 1개 산식입니다. 기초 수/형식을 검토해야 합니다.');if(m.stretches.length!==1)issues.push('꺾인 교각의 원본 수량 규칙이 없습니다.');if(m.integratedBlocks.length)issues.push('일체 교좌면을 개별 블럭 Nb배로 계산할 수 없습니다.');if(input.pileKind==='PHC')issues.push('원본 복합말뚝 산식은 PHC 시공수량에 적용하지 않습니다.');const p=m.piles;if(p.length&&p.some(x=>Math.abs(x.diameter-p[0].diameter)>1e-7||Math.abs(x.top-x.bottom-p[0].top+p[0].bottom)>1e-7))issues.push('원본은 말뚝 직경·길이 단일값을 전제로 합니다.');return issues;}
export function overridesFor(mapped:Mapped[],stored:QuantityInputs):Overrides{
stored=withTemplateDefaults(stored);
const result:Overrides={};for(const v of mapped){const raw=v.notApplicable?0:v.automatic?v.value:stored.values[v.key];result[cellKey(INPUTS,'N'+v.key.slice(1))]=typeof raw==='number'&&Number.isFinite(raw)?raw:null;}
for(const v of extraNumbers){const raw=stored.values[v.key];result[cellKey(DETAIL,v.cell)]=typeof raw==='number'&&Number.isFinite(raw)?raw:null;}
return result;
}
export function calculate(input:PierInput,m:PierModel,stored:QuantityInputs,rebarTon?:number|null){
stored=withTemplateDefaults(stored);
const mapped=automaticInputs(input,m),overrides=overridesFor(mapped,stored),copingMode='straight' as const,formulas=straightCopingFormulas,book=new FormulaBook(overrides,template,formulas);
if(rebarTon!=null&&Number.isFinite(rebarTon))overrides[cellKey(DETAIL,'W67')]=rebarTon;
const results=quantityRows.map(row=>{const dependencies=book.references(DETAIL,row.cell),missing=dependencies.filter(k=>Object.hasOwn(overrides,k)&&overrides[k]===null);let value:number|null=null,error='';
try{if(!missing.length)value=Number(book.cell(DETAIL,row.cell));}catch(e){error=(e as Error).message;}
return {...row,value,error,missing,dependencies,formula:formulas[cellKey(DETAIL,row.cell)]??template[DETAIL][row.cell]?.value,sourceFormula:template[DETAIL][row.cell]?.value};});
const needed=new Set(results.flatMap(r=>r.dependencies));
const missing=mapped.filter(v=>!v.automatic&&needed.has(cellKey(INPUTS,'N'+v.key.slice(1)))&&overrides[cellKey(INPUTS,'N'+v.key.slice(1))]===null).map(v=>v.name);
for(const v of extraNumbers)if(overrides[cellKey(DETAIL,v.cell)]===null)missing.push(v.name);
for(const v of specifications)if(typeof stored.values[v.key]!=='string'||!String(stored.values[v.key]).trim())missing.push(v.name);
const issues=supportIssues(input,m);const totalDepth=[21,22,23].map(r=>overrides[cellKey(INPUTS,'N'+r)]);if(totalDepth.every(v=>typeof v==='number')&&totalDepth.reduce((a,b)=>Number(a)+Number(b),0)!>8)issues.push('원본 토공 심도구간은 8m까지입니다. 초과 수량을 완료 처리하지 않습니다.');
for(const [key,v]of Object.entries(overrides))if(typeof v==='number'&&(/!N(21|22|23|24|25|26|27|29|30|31|33|34|35|36|37|38|39|40|41|42|43|44|45|46)$/.test(key)||key.startsWith(DETAIL+'!'))&&v<0)issues.push(`${key}: 음수 입력을 확인하세요.`);
const warnings=['원본 W51의 실제 기둥 거푸집식은 설명 Hcol과 다릅니다(Zb-H1-H2-Zs+Zf). 원식을 유지합니다.','원본 W50 코핑 거푸집식의 계수와 W63 몰탈 0.05m는 표시 설명/규격과 다릅니다.','현재 코핑은 직선/단차 형상: a=0, R 미적용이며 W50/W58의 원본 원호항에 직선 극한(R→∞)을 적용합니다. s는 단차를 포함한 양단 등가경사입니다.','원본 고정 기준: 터파기 여유 각 1m, 버림 두께 0.1m·치수여유 0.2m, 몰탈 두께 0.05m, 솔플레이트 여유 0.1m, 강재비중 7.85.'];
return {mapped,overrides,book,results,copingMode,formulas,missing,issues,warnings,complete:!missing.length&&!issues.length&&!results.some(r=>r.value===null||r.error)&&stored.reviewed,signature:geometrySignature(input,m)+'|rebar:'+String(rebarTon??'manual')} ;
}
export function aggregate(results:ReturnType<typeof calculate>[]){return quantityRows.map((r,i)=>({...r,value:results.every(x=>x.complete)?results.reduce((s,x)=>s+x.results[i].value!,0):null}));}