Files
Pier_v1/src/core/project.ts
T
ypshimandClaude Haiku 5.5 7cf5835689 feat(rebar): per-member fck on ② for column, coping and footing
Each member can take its own fck on its ② card. A member without one uses the ③ 공통 fck, and fy stays common.
The rebar table and the 3D model both read the resolved value. The I02 note stays until every fck member has its
own value. The project file stores the values as optional fckByMember and refuses a bad value on open.
Changing the fck is not a shape change, so it is not counted in the pending changes.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-23 08:53:39 +09:00

81 lines
3.2 KiB
TypeScript

import type { FckByMember, Material } from './rebar/common';
import type { PierInput } from './types';
/** Project file schema (spec §8). Bump only with a migration. */
export const SCHEMA_VERSION = 1;
export interface ProjectInfo {
name: string;
bridge: string;
client: string;
author: string;
crs: string;
memo: string;
}
export interface ProjectPier {
id: string;
/** Generated input: the values the 3D model was built from. The model is not stored; it is regenerated on open (spec §8). */
input: PierInput;
/** Values as imported from the sheet (기본값). Missing in files saved before change tracking: input is used. */
defaults?: PierInput;
/** Values currently in the cards (현재 입력). Missing in older files: input is used. */
draft?: PierInput;
}
export interface ProjectFile {
schemaVersion: number;
project: ProjectInfo;
source: { fileName: string; sheets: string[] };
piers: ProjectPier[];
history: { version: string; note: string; at: string }[];
/** ③ 공통 설정 (fck · fy). Absent in older files: the defaults are used. */
common?: Material;
/** ② 부재별 fck (MPa). Absent in older files: every member uses the 공통 fck. */
fckByMember?: FckByMember;
}
/** Serialises a project to the pretty-printed JSON stored in <교량명>_교각설계.pzr.json. */
export function serializeProject(p: ProjectFile): string {
return `${JSON.stringify(p, null, 2)}\n`;
}
/**
* Parses and checks a project file. Throws with a message that names the first problem, so a damaged file is
* refused instead of being half-loaded.
*/
export function parseProject(text: string): ProjectFile {
let raw: unknown;
try {
raw = JSON.parse(text);
} catch {
throw new Error('프로젝트 파일이 JSON으로 읽히지 않습니다.');
}
const obj = raw as Partial<ProjectFile>;
if (!obj || typeof obj !== 'object') throw new Error('프로젝트 파일 형식이 아닙니다.');
if (obj.schemaVersion !== SCHEMA_VERSION) {
throw new Error(`지원하지 않는 파일 버전입니다 (schemaVersion ${String(obj.schemaVersion)}, 지원: ${SCHEMA_VERSION}).`);
}
if (!Array.isArray(obj.piers)) throw new Error('piers 목록이 없습니다.');
if (obj.fckByMember !== undefined) {
for (const [member, value] of Object.entries(obj.fckByMember)) {
if (!['column', 'coping', 'footing'].includes(member) || typeof value !== 'number' || !(value > 0)) {
throw new Error(`부재별 fck 값이 맞지 않습니다 (${member}: ${String(value)}).`);
}
}
}
obj.piers.forEach((p, i) => {
if (!p || typeof p.id !== 'string' || !p.input) throw new Error(`교각 ${i + 1}: id 또는 input이 없습니다.`);
const input = p.input;
if (typeof input.name !== 'string' || !Array.isArray(input.ips) || !Array.isArray(input.bearings)) {
throw new Error(`교각 ${i + 1}(${String(input.name)}): 입력 형식이 맞지 않습니다.`);
}
for (const extra of [p.defaults, p.draft]) {
if (extra !== undefined && (typeof extra !== 'object' || extra === null || typeof extra.name !== 'string')) {
throw new Error(`교각 ${i + 1}(${input.name}): defaults 또는 draft 형식이 맞지 않습니다.`);
}
}
});
return obj as ProjectFile;
}