fix(532-3): E03 일꾼 stderr 파일로(파이프 막힘으로 해석 멈춤) · 처음 보기 = 그 비탈 확대 · 화면 높이 고정 · 자유물체도 반력 화살 · 목록 줄임
- 일꾼 stderr 를 파이프로 두면 자식 경고가 쌓여 8/57 에서 멈춤 → 임시 파일로 - 횡단 처음 보기 = 탐색 범위 · 임계면 · 격자 중심을 판 가운데(두 번 누름 = 이 보기 · 줌 아웃 = 모형 전체) - 본문 높이 = 화면 높이(커지며 넘치던 것) · 고른 줄 표시 밝음/어두움 같이 · 3D 보기 창 맨 위 - 자유물체도 N · U · S = 바닥으로 들어오는 화살 · 글 = 화살 바깥 끝 - 목록 측점 0+000 꼴 · 좌우 깎/쌓 · 해석 없음 = - Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -170,24 +170,29 @@ def _meta(item: dict[str, Any], design: dict[str, Any]) -> dict[str, Any]:
|
||||
|
||||
|
||||
def _run_worker(job: dict[str, Any], emit: Any) -> None:
|
||||
"""일꾼 프로세스(`E03_StabSection_Worker`) — 한 줄마다 emit · 실패면 RuntimeError."""
|
||||
"""일꾼 프로세스(`E03_StabSection_Worker`) — 한 줄마다 emit · 실패면 RuntimeError.
|
||||
|
||||
stderr 는 파일로(파이프로 두면 경고가 쌓여 일꾼이 멈춤)."""
|
||||
with tempfile.TemporaryDirectory(prefix="aislo_e03_") as folder:
|
||||
path = Path(folder) / "job.json"
|
||||
path.write_text(json.dumps(job, ensure_ascii=False), encoding="utf-8")
|
||||
process = subprocess.Popen(
|
||||
[sys.executable, "-m", "E03_StabSection.E03_StabSection_Worker", str(path)],
|
||||
cwd=PROJECT_ROOT,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
for line in process.stdout:
|
||||
if line.startswith("{"):
|
||||
emit(json.loads(line).get("done"))
|
||||
error = process.stderr.read()
|
||||
if process.wait() != 0:
|
||||
raise RuntimeError(f"해석 일꾼 실패: {error.strip().splitlines()[-1:]}")
|
||||
log = Path(folder) / "stderr.txt"
|
||||
with log.open("w", encoding="utf-8") as errors:
|
||||
process = subprocess.Popen(
|
||||
[sys.executable, "-m", "E03_StabSection.E03_StabSection_Worker", str(path)],
|
||||
cwd=PROJECT_ROOT,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=errors,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
for line in process.stdout:
|
||||
if line.startswith("{"):
|
||||
emit(json.loads(line).get("done"))
|
||||
code = process.wait()
|
||||
if code != 0:
|
||||
tail = log.read_text(encoding="utf-8", errors="replace").strip().splitlines()[-1:]
|
||||
raise RuntimeError(f"해석 일꾼 실패: {tail}")
|
||||
|
||||
|
||||
def run_all(root: Path, plan: dict[str, Any], report: Any) -> int:
|
||||
|
||||
@@ -82,16 +82,22 @@ export function buildList(
|
||||
onToggle(row.key);
|
||||
});
|
||||
tr.append(
|
||||
el("td", { text: row.label?.replace(/^STA\./, "") ?? row.key }),
|
||||
el("td", { text: row.label?.replace(/^STA\./, "").replace(/\.000$/, "") ?? row.key }),
|
||||
el("td", {
|
||||
text: row.sides.length ? roleText(row, { cut: t("E03_Cut"), fill: t("E03_Fill") }) : "-",
|
||||
text: row.sides.length
|
||||
? roleText(row, { cut: t("E03_CutShort"), fill: t("E03_FillShort") })
|
||||
: "-",
|
||||
}),
|
||||
...draw.conditions.map((c) => el("td", { className: "is-num", text: fmt(row.min[c]) })),
|
||||
el("td", {
|
||||
children: [
|
||||
el("span", {
|
||||
className: `e03-verdict e03-verdict--${v}`,
|
||||
text: row.error ? t("E03_Error") : t(`E03_Verdict_${v}` as LocaleKey),
|
||||
text: row.error
|
||||
? t("E03_Error")
|
||||
: v === "none"
|
||||
? "-"
|
||||
: t(`E03_Verdict_${v}` as LocaleKey),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
|
||||
@@ -331,7 +331,9 @@ export function renderE03StabSection(root: HTMLElement): void {
|
||||
}
|
||||
|
||||
async function load(): Promise<void> {
|
||||
const before = result?.status;
|
||||
result = await api<Result2d>(`${base}/result2d`);
|
||||
if (before === "none" && result.status === "ok") active = null; // 첫 해석 뒤 = 가장 낮은 측점으로
|
||||
sections.clear();
|
||||
const conds = result.conditions;
|
||||
if (!conds.includes(condition)) condition = conds[0] ?? "dry";
|
||||
|
||||
@@ -174,11 +174,18 @@ function freeBody(view: SectionView, analysis: Analysis, picked: number | null):
|
||||
"data-value": String(a.value),
|
||||
}),
|
||||
);
|
||||
const tip = a.name.startsWith("E") || a.name.startsWith("X") || a.name === "Q" ? p0 : p1;
|
||||
// 글 = 화살 바깥 끝(들어오는 힘 = 꼬리 · 나가는 힘 = 머리)에서 화살 방향으로 조금 더
|
||||
const len = Math.hypot(p1[0] - p0[0], p1[1] - p0[1]) || 1;
|
||||
const d: Point = [(p1[0] - p0[0]) / len, (p1[1] - p0[1]) / len];
|
||||
const ending = a.name !== "W";
|
||||
const out: Point = ending ? [-d[0], -d[1]] : d;
|
||||
const at: Point = ending ? p0 : p1;
|
||||
const anchor = out[0] > 0.3 ? "start" : out[0] < -0.3 ? "end" : "middle";
|
||||
const label = { x: at[0] + out[0] * 6, y: at[1] + out[1] * 6 + (out[1] > 0.3 ? 10 : 0) };
|
||||
svg.append(
|
||||
svgEl(
|
||||
"text",
|
||||
{ x: tip[0] + 4, y: tip[1] - 4, class: "e03-label" },
|
||||
{ ...label, "text-anchor": anchor, class: "e03-label" },
|
||||
`${a.name} ${fmt(a.value, 1)}`,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
bandIndex,
|
||||
bandLegend,
|
||||
fitView,
|
||||
focusHome,
|
||||
pointsText,
|
||||
sliceShape,
|
||||
toModel,
|
||||
@@ -25,6 +26,7 @@ import {
|
||||
|
||||
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||
const PAD = 28;
|
||||
const MAX_SCALE = 30;
|
||||
|
||||
export function svgEl<K extends keyof SVGElementTagNameMap>(
|
||||
tag: K,
|
||||
@@ -307,7 +309,7 @@ export function createSectionCanvas(onPick: (index: number | null) => void): Sec
|
||||
});
|
||||
svg.addEventListener("mouseleave", () => (tip.style.display = "none"));
|
||||
|
||||
const key = `${view.key}|${width}x${height}`;
|
||||
const key = `${view.key}|${analysis?.condition}|${analysis?.side}|${width}x${height}`;
|
||||
if (key !== zoomKey) zoom = undefined;
|
||||
zoomKey = key;
|
||||
attachSvgZoomPan({
|
||||
@@ -317,7 +319,8 @@ export function createSectionCanvas(onPick: (index: number | null) => void): Sec
|
||||
heightPx: height,
|
||||
pad: { left: 0, right: 0, top: 0, bottom: 0 },
|
||||
wheelZoom: true,
|
||||
maxScale: 20,
|
||||
maxScale: MAX_SCALE,
|
||||
home: focusHome(fit, view, analysis, width, height, MAX_SCALE),
|
||||
initial: zoom,
|
||||
onChange: (state) => (zoom = state),
|
||||
});
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
gap: var(--spacing-8);
|
||||
height: 100%;
|
||||
height: calc(100dvh - var(--app-header-h));
|
||||
padding: var(--spacing-8) var(--spacing-12);
|
||||
box-sizing: border-box;
|
||||
min-height: 0;
|
||||
@@ -89,6 +89,7 @@
|
||||
.e03-body {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(320px, 420px);
|
||||
grid-template-rows: minmax(0, 1fr);
|
||||
gap: var(--spacing-8);
|
||||
min-height: 0;
|
||||
}
|
||||
@@ -214,9 +215,10 @@
|
||||
}
|
||||
|
||||
.e03-list tbody tr.is-active {
|
||||
background: var(--color-lavender-wash);
|
||||
outline: 1px solid var(--color-accent);
|
||||
outline-offset: -1px;
|
||||
background: var(--color-surface-sunken);
|
||||
outline: 2px solid var(--color-accent);
|
||||
outline-offset: -2px;
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.e03-verdict {
|
||||
@@ -508,7 +510,9 @@
|
||||
}
|
||||
|
||||
.e03-table tbody tr.is-picked {
|
||||
background: var(--color-lavender-wash);
|
||||
background: var(--color-surface-sunken);
|
||||
outline: 2px solid var(--color-accent);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.e03-slices {
|
||||
@@ -576,7 +580,7 @@
|
||||
.e03-view3d {
|
||||
position: fixed;
|
||||
inset: 5vh 5vw;
|
||||
z-index: 50;
|
||||
z-index: 800;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: var(--radius-sm);
|
||||
|
||||
@@ -149,7 +149,7 @@ export async function openView3d(
|
||||
const camera = new THREE.PerspectiveCamera(45, 1, 0.5, Math.max(sphere.radius * 20, 1000));
|
||||
camera.position
|
||||
.copy(sphere.center)
|
||||
.add(new THREE.Vector3(0, sphere.radius * 1.4, sphere.radius * 1.2));
|
||||
.add(new THREE.Vector3(0, sphere.radius * 1.9, sphere.radius * 1.6));
|
||||
const controls = new OrbitControls(camera, renderer.domElement);
|
||||
controls.target.copy(sphere.center);
|
||||
controls.update();
|
||||
|
||||
@@ -243,6 +243,43 @@ export function fitView(view: SectionView, width: number, height: number, pad: n
|
||||
return { k, tx: width / 2 + (k * (x0 + x1)) / 2, ty: height / 2 + (k * (z0 + z1)) / 2 };
|
||||
}
|
||||
|
||||
/**
|
||||
* 처음 보기(줌 · 팬) — 그 비탈 탐색 범위 · 임계면 · 격자 중심을 판 가운데 크게(여백 25 %).
|
||||
* 판 전체(scale 1) = 모형 전체 · 두 번 누름 = 이 보기 · 배율은 1~maxScale.
|
||||
*/
|
||||
export function focusHome(
|
||||
fit: ViewFit,
|
||||
view: SectionView,
|
||||
analysis: Analysis | null,
|
||||
width: number,
|
||||
height: number,
|
||||
maxScale: number,
|
||||
): { scale: number; tx: number; ty: number } {
|
||||
if (!analysis) return { scale: 1, tx: 0, ty: 0 };
|
||||
const pts: Point[] = [];
|
||||
const [x0, x1] = analysis.limits;
|
||||
view.surface.filter((p) => p[0] >= x0 && p[0] <= x1).forEach((p) => pts.push(p));
|
||||
analysis.critical?.points.forEach((p) => pts.push(p));
|
||||
if (analysis.grid) {
|
||||
const g = analysis.grid;
|
||||
pts.push([Math.min(...g.x), Math.min(...g.z)], [Math.max(...g.x), Math.max(...g.z)]);
|
||||
}
|
||||
if (!pts.length) return { scale: 1, tx: 0, ty: 0 };
|
||||
const scr = pts.map((p) => toScreen(fit, p));
|
||||
const [sx0, sx1] = [Math.min(...scr.map((p) => p[0])), Math.max(...scr.map((p) => p[0]))];
|
||||
const [sy0, sy1] = [Math.min(...scr.map((p) => p[1])), Math.max(...scr.map((p) => p[1]))];
|
||||
const raw = Math.min(
|
||||
width / Math.max((sx1 - sx0) * 1.25, 1),
|
||||
height / Math.max((sy1 - sy0) * 1.25, 1),
|
||||
);
|
||||
const scale = Math.min(Math.max(raw, 1), maxScale);
|
||||
return {
|
||||
scale,
|
||||
tx: width / 2 - (scale * (sx0 + sx1)) / 2,
|
||||
ty: height / 2 - (scale * (sy0 + sy1)) / 2,
|
||||
};
|
||||
}
|
||||
|
||||
/** 결과 좌표 → 화면 px */
|
||||
export function toScreen(fit: ViewFit, p: Point): Point {
|
||||
return [-fit.k * p[0] + fit.tx, -fit.k * p[1] + fit.ty];
|
||||
@@ -336,9 +373,9 @@ export function forceArrows(
|
||||
return [
|
||||
arrow("W", slice.weight, centroid, [0, -1]),
|
||||
arrow("Q", slice.surcharge, midTop, [0, -1], true),
|
||||
arrow("N", slice.N_eff, midBase, n),
|
||||
arrow("U", slice.U, midBase, n),
|
||||
arrow("S", slice.S, midBase, [-s * t[0], -s * t[1]]),
|
||||
arrow("N", slice.N_eff, midBase, n, true),
|
||||
arrow("U", slice.U, midBase, n, true),
|
||||
arrow("S", slice.S, midBase, [-s * t[0], -s * t[1]], true),
|
||||
arrow("EL", slice.E_left, nearFace, [s, 0], true),
|
||||
arrow("ER", slice.E_right, farFace, [-s, 0], true),
|
||||
arrow("XL", slice.X_left, nearFace, [0, 1], true),
|
||||
|
||||
@@ -50,6 +50,8 @@ export const ui_locales_e03 = {
|
||||
E03_Error: ["오류", "Error"],
|
||||
E03_Cut: ["깎기", "Cut"],
|
||||
E03_Fill: ["쌓기", "Fill"],
|
||||
E03_CutShort: ["깎", "C"],
|
||||
E03_FillShort: ["쌓", "F"],
|
||||
E03_Left: ["좌 비탈", "Left slope"],
|
||||
E03_Right: ["우 비탈", "Right slope"],
|
||||
E03_Side: ["비탈", "Slope"],
|
||||
|
||||
Reference in New Issue
Block a user