SPipe 데모 v2 (2026-10-30) 서버 배포본
- SPipe_server_2026-10-30.zip 내용을 풀어서 등록 (압축 파일은 올리지 않음) - README에 데모 v2 서버 업데이트 순서 추가, 저장소에 없는 문서 목차 정리 - 패치내역을 docs/로 이동, _worklog.md 추가 - 임시 파일(backend/temp.*)과 API 키가 든 파일은 제외
This commit is contained in:
commit
21ebb16fb7
181 files changed
+57207
No files matched your search
@@ -0,0 +1,143 @@
|
||||
// Whole-network editing tools (관망 도구): pipe length recalculation from
|
||||
// geometry and elevation filling from the nearest node. Pure functions; the
|
||||
// UI wraps them with undo.
|
||||
import type { NetworkModel, InpNode } from './inpModel';
|
||||
import { calculateGeoAwareDistance, type CRS } from './crs.ts';
|
||||
import { getUnitSystem } from './unitSystem.ts';
|
||||
|
||||
export interface Point {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
/** Link ID -> intermediate vertices, read from the raw [VERTICES] section. */
|
||||
export function parseVertices(model: NetworkModel): Record<string, Point[]> {
|
||||
if (model.vertices) return model.vertices;
|
||||
const out: Record<string, Point[]> = {};
|
||||
const r = model.sectionIndices['VERTICES'];
|
||||
if (!r) return out;
|
||||
for (let i = r.start + 1; i <= r.end; i++) {
|
||||
const body = model.rawLines[i]?.split(';')[0].trim();
|
||||
if (!body) continue;
|
||||
const [id, xs, ys] = body.split(/\s+/);
|
||||
const x = Number(xs);
|
||||
const y = Number(ys);
|
||||
if (id && Number.isFinite(x) && Number.isFinite(y)) (out[id] ??= []).push({ x, y });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Polyline length (node1 -> vertices -> node2) in the model's length units. */
|
||||
export function linkGeometryLength(model: NetworkModel, node1: string, node2: string, vertices: Point[], crs: CRS): { length: number; isPhysical: boolean } | null {
|
||||
const a = model.nodes[node1];
|
||||
const b = model.nodes[node2];
|
||||
if (!a || !b) return null;
|
||||
const system = getUnitSystem(model.options.units);
|
||||
const pts: Point[] = [a, ...vertices, b];
|
||||
let total = 0;
|
||||
let isPhysical = true;
|
||||
for (let i = 1; i < pts.length; i++) {
|
||||
const d = calculateGeoAwareDistance(pts[i - 1], pts[i], crs, system);
|
||||
total += d.distance;
|
||||
isPhysical = isPhysical && d.isPhysical;
|
||||
}
|
||||
return { length: total, isPhysical };
|
||||
}
|
||||
|
||||
export interface LengthChange {
|
||||
id: string;
|
||||
before: number;
|
||||
after: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recalculate pipe lengths from coordinates (and [VERTICES]); `ids` limits the
|
||||
* set. Lengths are rounded to 0.01 and a zero-length result keeps the old value.
|
||||
*/
|
||||
export function recalcPipeLengths(model: NetworkModel, crs: CRS, ids?: Iterable<string>): { model: NetworkModel; changes: LengthChange[]; isPhysical: boolean } {
|
||||
const vertices = parseVertices(model);
|
||||
const target = ids ? new Set(ids) : null;
|
||||
const pipes = { ...model.pipes };
|
||||
const changes: LengthChange[] = [];
|
||||
let isPhysical = true;
|
||||
for (const p of Object.values(model.pipes)) {
|
||||
if (target && !target.has(p.id)) continue;
|
||||
const g = linkGeometryLength(model, p.node1, p.node2, vertices[p.id] ?? [], crs);
|
||||
if (!g) continue;
|
||||
isPhysical = isPhysical && g.isPhysical;
|
||||
const after = Math.round(g.length * 100) / 100;
|
||||
if (after > 0 && Math.abs(after - p.length) >= 0.005) {
|
||||
pipes[p.id] = { ...p, length: after };
|
||||
changes.push({ id: p.id, before: p.length, after });
|
||||
}
|
||||
}
|
||||
return { model: changes.length ? { ...model, pipes } : model, changes, isPhysical };
|
||||
}
|
||||
|
||||
/** Pipes touching a node (for re-measuring after the node moved). */
|
||||
export function pipesAtNode(model: NetworkModel, nodeId: string): string[] {
|
||||
return Object.values(model.pipes)
|
||||
.filter((p) => p.node1 === nodeId || p.node2 === nodeId)
|
||||
.map((p) => p.id);
|
||||
}
|
||||
|
||||
/** Elevation of the nearest other node (by planar coordinate distance), or null. */
|
||||
export function nearestNodeElevation(model: NetworkModel, x: number, y: number, exclude?: Set<string>, onlyNonZero = false): number | null {
|
||||
let best: InpNode | null = null;
|
||||
let bestD = Infinity;
|
||||
for (const n of Object.values(model.nodes)) {
|
||||
if (exclude?.has(n.id)) continue;
|
||||
if (onlyNonZero && !n.elevation) continue;
|
||||
const d = (n.x - x) ** 2 + (n.y - y) ** 2;
|
||||
if (d < bestD) {
|
||||
bestD = d;
|
||||
best = n;
|
||||
}
|
||||
}
|
||||
return best ? best.elevation : null;
|
||||
}
|
||||
|
||||
/** Junctions/tanks with elevation 0 take the nearest non-zero node's elevation. */
|
||||
export function fillZeroElevations(model: NetworkModel): { model: NetworkModel; filled: string[] } {
|
||||
const zero = Object.values(model.nodes).filter((n) => n.type !== 'RESERVOIR' && !n.elevation);
|
||||
if (!zero.length) return { model, filled: [] };
|
||||
const nodes = { ...model.nodes };
|
||||
const filled: string[] = [];
|
||||
const zeroIds = new Set(zero.map((n) => n.id));
|
||||
for (const n of zero) {
|
||||
const e = nearestNodeElevation(model, n.x, n.y, zeroIds, true);
|
||||
if (e !== null) {
|
||||
nodes[n.id] = { ...n, elevation: e };
|
||||
filled.push(n.id);
|
||||
}
|
||||
}
|
||||
return { model: filled.length ? { ...model, nodes } : model, filled };
|
||||
}
|
||||
|
||||
// ---- edit options (persisted per browser) ----
|
||||
|
||||
export interface EditOptions {
|
||||
/** Re-measure connected pipe lengths when a node moves (new pipes are always measured). */
|
||||
autoLength: boolean;
|
||||
/** New nodes take the nearest existing node's elevation instead of 0. */
|
||||
nearestElevation: boolean;
|
||||
}
|
||||
|
||||
const EDIT_OPTIONS_KEY = 'spipe_edit_options';
|
||||
export const DEFAULT_EDIT_OPTIONS: EditOptions = { autoLength: true, nearestElevation: true };
|
||||
|
||||
export function loadEditOptions(): EditOptions {
|
||||
try {
|
||||
return { ...DEFAULT_EDIT_OPTIONS, ...JSON.parse(localStorage.getItem(EDIT_OPTIONS_KEY) || '{}') };
|
||||
} catch {
|
||||
return DEFAULT_EDIT_OPTIONS;
|
||||
}
|
||||
}
|
||||
|
||||
export function saveEditOptions(o: EditOptions): void {
|
||||
try {
|
||||
localStorage.setItem(EDIT_OPTIONS_KEY, JSON.stringify(o));
|
||||
} catch {
|
||||
// storage unavailable: options just won't persist
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user