Merge remote-tracking branch 'origin/main_laptop_1' into sub_laptop_8
This commit is contained in:
@@ -77,6 +77,7 @@ import {
|
|||||||
} from "../../B05_Profile/B05_Profile_UI_Profile_Guard";
|
} from "../../B05_Profile/B05_Profile_UI_Profile_Guard";
|
||||||
import type { RouteProfilePanelCallbacks } from "../../B05_Profile/B05_Profile_UI_Profile_Panel_Types";
|
import type { RouteProfilePanelCallbacks } from "../../B05_Profile/B05_Profile_UI_Profile_Panel_Types";
|
||||||
import type { ElevationWindowResult } from "@util/common_util_chart_ywindow";
|
import type { ElevationWindowResult } from "@util/common_util_chart_ywindow";
|
||||||
|
import { frameOf } from "@util/common_util_view_window";
|
||||||
import type { DesignSideBoard, DesignSideFactory, DesignView } from "./design_profile_slots";
|
import type { DesignSideBoard, DesignSideFactory, DesignView } from "./design_profile_slots";
|
||||||
import { noticeDroppedEdits } from "./design_save";
|
import { noticeDroppedEdits } from "./design_save";
|
||||||
import { createProfileOverlays } from "./design_profile_overlays";
|
import { createProfileOverlays } from "./design_profile_overlays";
|
||||||
@@ -341,7 +342,7 @@ export function createDesignProfile(ctx: DesignProfileContext) {
|
|||||||
scheduleCrossPreview();
|
scheduleCrossPreview();
|
||||||
if (redrawPending) return;
|
if (redrawPending) return;
|
||||||
redrawPending = true;
|
redrawPending = true;
|
||||||
requestAnimationFrame(() => {
|
frameOf(root, () => {
|
||||||
redrawPending = false;
|
redrawPending = false;
|
||||||
draw();
|
draw();
|
||||||
});
|
});
|
||||||
@@ -443,7 +444,7 @@ export function createDesignProfile(ctx: DesignProfileContext) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 접기 — 두 보기가 같은 키 하나를 쓴다(`_Profile_Drag` 몫).
|
// 접기 — 두 보기가 같은 키 하나를 쓴다(`_Profile_Drag` 몫).
|
||||||
bindProfileCollapse(root, panelHandle, () => requestAnimationFrame(draw));
|
bindProfileCollapse(root, panelHandle, () => frameOf(root, draw));
|
||||||
|
|
||||||
/** 배수유역도(평면)에 **구간형 구조물 띠**를 넘긴다 — 종단 레인과 같은 자료다(계획서 3-6). */
|
/** 배수유역도(평면)에 **구간형 구조물 띠**를 넘긴다 — 종단 레인과 같은 자료다(계획서 3-6). */
|
||||||
function syncDrainageSpans(): void {
|
function syncDrainageSpans(): void {
|
||||||
@@ -482,7 +483,7 @@ export function createDesignProfile(ctx: DesignProfileContext) {
|
|||||||
nextDetail.balloon_offsets ?? undefined,
|
nextDetail.balloon_offsets ?? undefined,
|
||||||
);
|
);
|
||||||
draw();
|
draw();
|
||||||
requestAnimationFrame(draw);
|
frameOf(root, draw);
|
||||||
// 저장된 횡단 설계가 옛 계획고로 굳어 있으면 진입 즉시 한 번 맞춘다 —
|
// 저장된 횡단 설계가 옛 계획고로 굳어 있으면 진입 즉시 한 번 맞춘다 —
|
||||||
// 지금까지는 B06에 들어가야만 고쳐져, B05의 횡단 기준 유토곡선과 3D
|
// 지금까지는 B06에 들어가야만 고쳐져, B05의 횡단 기준 유토곡선과 3D
|
||||||
// 예상형상이 옛 설계선을 그대로 썼다(2026-08-23 사용자 보고 · 실측 확인).
|
// 예상형상이 옛 설계선을 그대로 썼다(2026-08-23 사용자 보고 · 실측 확인).
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
import { attachWheelHorizontalScroll } from "../../B05_Profile/B05_Profile_UI_Profile_Wheel";
|
import { attachWheelHorizontalScroll } from "../../B05_Profile/B05_Profile_UI_Profile_Wheel";
|
||||||
import { needsFullRedraw, type ElevationWindowResult } from "@util/common_util_chart_ywindow";
|
import { needsFullRedraw, type ElevationWindowResult } from "@util/common_util_chart_ywindow";
|
||||||
|
import { viewOf } from "@util/common_util_view_window";
|
||||||
|
|
||||||
/** 그래프를 통째로 다시 만들 때까지 기다리는 시간(ms) — 눈금 갱신으로도 못 살릴 때만. */
|
/** 그래프를 통째로 다시 만들 때까지 기다리는 시간(ms) — 눈금 갱신으로도 못 살릴 때만. */
|
||||||
const SCROLL_SETTLE_MS = 80;
|
const SCROLL_SETTLE_MS = 80;
|
||||||
@@ -39,7 +40,7 @@ export function bindProfileViewport(deps: ProfileViewportDeps): () => void {
|
|||||||
const fitted = deps.fitElevation();
|
const fitted = deps.fitElevation();
|
||||||
timerView.clearTimeout(scrollSettleTimer);
|
timerView.clearTimeout(scrollSettleTimer);
|
||||||
if (!needsFullRedraw(fitted)) return;
|
if (!needsFullRedraw(fitted)) return;
|
||||||
timerView = body.ownerDocument.defaultView ?? window;
|
timerView = viewOf(body);
|
||||||
scrollSettleTimer = timerView.setTimeout(() => {
|
scrollSettleTimer = timerView.setTimeout(() => {
|
||||||
if (deps.editing() || Math.abs(body.scrollLeft - settledScrollLeft) < 1) return;
|
if (deps.editing() || Math.abs(body.scrollLeft - settledScrollLeft) < 1) return;
|
||||||
settledScrollLeft = body.scrollLeft;
|
settledScrollLeft = body.scrollLeft;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
|
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
|
||||||
import { readStateRaw, writeStateRaw } from "../A00_Common/b_page_state";
|
import { readStateRaw, writeStateRaw } from "../A00_Common/b_page_state";
|
||||||
|
import { viewOf } from "../common_util/common_util_view_window";
|
||||||
import {
|
import {
|
||||||
computeDetailBasins,
|
computeDetailBasins,
|
||||||
fetchDetailPipePoints,
|
fetchDetailPipePoints,
|
||||||
@@ -172,6 +173,8 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra
|
|||||||
let offsetX = 0;
|
let offsetX = 0;
|
||||||
let offsetY = 0;
|
let offsetY = 0;
|
||||||
let frameHandle = 0;
|
let frameHandle = 0;
|
||||||
|
/** 화면 틀은 판이 놓인 창 것 — 새 창 최대화로 가려진 메인 창 틀은 멈춤(230-2). */
|
||||||
|
let frameView: Window = window;
|
||||||
let loadSequence = 0;
|
let loadSequence = 0;
|
||||||
let requestSeq = 0; // 세부유역 요청 — 최신 응답만 채택
|
let requestSeq = 0; // 세부유역 요청 — 최신 응답만 채택
|
||||||
/** 우클릭으로 바로 놓은 세월교 · 물넘이포장 — 재계산 유량이 오면 빈 월류 높이를 채울 측점(126-1). */
|
/** 우클릭으로 바로 놓은 세월교 · 물넘이포장 — 재계산 유량이 오면 빈 월류 높이를 채울 측점(126-1). */
|
||||||
@@ -301,7 +304,8 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra
|
|||||||
|
|
||||||
function scheduleDraw(): void {
|
function scheduleDraw(): void {
|
||||||
if (frameHandle) return;
|
if (frameHandle) return;
|
||||||
frameHandle = window.requestAnimationFrame(() => {
|
frameView = viewOf(root);
|
||||||
|
frameHandle = frameView.requestAnimationFrame(() => {
|
||||||
frameHandle = 0;
|
frameHandle = 0;
|
||||||
draw();
|
draw();
|
||||||
});
|
});
|
||||||
@@ -667,7 +671,7 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra
|
|||||||
dispose() {
|
dispose() {
|
||||||
loadSequence += 1;
|
loadSequence += 1;
|
||||||
if (frameHandle) {
|
if (frameHandle) {
|
||||||
window.cancelAnimationFrame(frameHandle);
|
frameView.cancelAnimationFrame(frameHandle);
|
||||||
frameHandle = 0;
|
frameHandle = 0;
|
||||||
}
|
}
|
||||||
resizeObserver.disconnect();
|
resizeObserver.disconnect();
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { readStateRaw, stateKey, writeStateRaw } from "../A00_Common/b_page_stat
|
|||||||
import { profilePanelMaxHeight } from "./B05_Profile_UI_Profile_Heights";
|
import { profilePanelMaxHeight } from "./B05_Profile_UI_Profile_Heights";
|
||||||
import { MASSHAUL_MIN_HEIGHT } from "./B05_Profile_UI_Profile_MassHaul";
|
import { MASSHAUL_MIN_HEIGHT } from "./B05_Profile_UI_Profile_MassHaul";
|
||||||
import { TABLE_OVERLAY_MIN_HEIGHT } from "./B05_Profile_UI_Profile_TableOverlay";
|
import { TABLE_OVERLAY_MIN_HEIGHT } from "./B05_Profile_UI_Profile_TableOverlay";
|
||||||
|
import { viewOf } from "../common_util/common_util_view_window";
|
||||||
|
|
||||||
const MIN_CHART_HEIGHT = 100;
|
const MIN_CHART_HEIGHT = 100;
|
||||||
/** 접힌 유토곡선 손잡이가 앉을 테이블 아래 전용 띠 — 곡선 행 클릭을 가리지 않게.
|
/** 접힌 유토곡선 손잡이가 앉을 테이블 아래 전용 띠 — 곡선 행 클릭을 가리지 않게.
|
||||||
@@ -123,9 +124,12 @@ export interface ProfileDrag {
|
|||||||
export function createProfileDrag(ctx: ProfileDragContext): ProfileDrag {
|
export function createProfileDrag(ctx: ProfileDragContext): ProfileDrag {
|
||||||
const { root, body, massHaul, tableOverlay } = ctx;
|
const { root, body, massHaul, tableOverlay } = ctx;
|
||||||
let resizeTimer = 0;
|
let resizeTimer = 0;
|
||||||
|
/** 시계는 종단이 놓인 창 것 — 새 창 최대화로 가려진 메인 창 시계는 늦게 돎(230-2). */
|
||||||
|
let timerView: Window = window;
|
||||||
const scheduleDraw = (): void => {
|
const scheduleDraw = (): void => {
|
||||||
window.clearTimeout(resizeTimer);
|
timerView.clearTimeout(resizeTimer);
|
||||||
resizeTimer = window.setTimeout(ctx.draw, 120);
|
timerView = viewOf(root);
|
||||||
|
resizeTimer = timerView.setTimeout(ctx.draw, 120);
|
||||||
};
|
};
|
||||||
// 위쪽 경계를 끌어 패널 높이를 조절한다. 늘어난 만큼은 그래프만 먹고 도면 테이블은
|
// 위쪽 경계를 끌어 패널 높이를 조절한다. 늘어난 만큼은 그래프만 먹고 도면 테이블은
|
||||||
// 처음 잡힌 높이를 지킨다(사용자 지시) — 테이블 행이 늘었다 줄었다 하면 읽기 어려워서다.
|
// 처음 잡힌 높이를 지킨다(사용자 지시) — 테이블 행이 늘었다 줄었다 하면 읽기 어려워서다.
|
||||||
@@ -211,9 +215,9 @@ export function createProfileDrag(ctx: ProfileDragContext): ProfileDrag {
|
|||||||
mainDragCooldown = false;
|
mainDragCooldown = false;
|
||||||
},
|
},
|
||||||
scheduleDraw,
|
scheduleDraw,
|
||||||
cancelDraw: () => window.clearTimeout(resizeTimer),
|
cancelDraw: () => timerView.clearTimeout(resizeTimer),
|
||||||
dispose() {
|
dispose() {
|
||||||
window.clearTimeout(resizeTimer);
|
timerView.clearTimeout(resizeTimer);
|
||||||
heightResizer.dispose();
|
heightResizer.dispose();
|
||||||
window.removeEventListener("pointerup", clearDragFlags);
|
window.removeEventListener("pointerup", clearDragFlags);
|
||||||
window.removeEventListener("pointercancel", clearDragFlags);
|
window.removeEventListener("pointercancel", clearDragFlags);
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
useDesignStore,
|
useDesignStore,
|
||||||
} from "../A00_Common/design/design_store";
|
} from "../A00_Common/design/design_store";
|
||||||
import { clearState } from "../A00_Common/b_page_state";
|
import { clearState } from "../A00_Common/b_page_state";
|
||||||
|
import { viewOf } from "../common_util/common_util_view_window";
|
||||||
|
|
||||||
/** 버튼 폭(21px)에 여유를 더한 값 — 이보다 가까우면 겹친 것으로 본다. */
|
/** 버튼 폭(21px)에 여유를 더한 값 — 이보다 가까우면 겹친 것으로 본다. */
|
||||||
const BUTTON_CLEARANCE_PX = 23;
|
const BUTTON_CLEARANCE_PX = 23;
|
||||||
@@ -38,13 +39,18 @@ export const HOLD_INTERVAL_MS = 100;
|
|||||||
* 버튼이 아니라 이 컨트롤러가 window 이벤트로 들고 있어야 반복이 끊기거나
|
* 버튼이 아니라 이 컨트롤러가 window 이벤트로 들고 있어야 반복이 끊기거나
|
||||||
* 반대로 멈추지 않는 상황이 생기지 않는다.
|
* 반대로 멈추지 않는 상황이 생기지 않는다.
|
||||||
*/
|
*/
|
||||||
function createHoldRepeater(): { start: (action: () => void) => void; stop: () => void } {
|
function createHoldRepeater(node: Node): {
|
||||||
|
start: (action: () => void) => void;
|
||||||
|
stop: () => void;
|
||||||
|
} {
|
||||||
let delayTimer = 0;
|
let delayTimer = 0;
|
||||||
let repeatTimer = 0;
|
let repeatTimer = 0;
|
||||||
|
/** 시계는 버튼 층이 놓인 창 것 — 새 창 최대화로 가려진 메인 창 시계는 늦게 돎(230-2). */
|
||||||
|
let timerView: Window = window;
|
||||||
|
|
||||||
function stop(): void {
|
function stop(): void {
|
||||||
window.clearTimeout(delayTimer);
|
timerView.clearTimeout(delayTimer);
|
||||||
window.clearInterval(repeatTimer);
|
timerView.clearInterval(repeatTimer);
|
||||||
delayTimer = 0;
|
delayTimer = 0;
|
||||||
repeatTimer = 0;
|
repeatTimer = 0;
|
||||||
window.removeEventListener("pointerup", stop);
|
window.removeEventListener("pointerup", stop);
|
||||||
@@ -54,8 +60,9 @@ function createHoldRepeater(): { start: (action: () => void) => void; stop: () =
|
|||||||
|
|
||||||
function start(action: () => void): void {
|
function start(action: () => void): void {
|
||||||
stop();
|
stop();
|
||||||
delayTimer = window.setTimeout(() => {
|
timerView = viewOf(node);
|
||||||
repeatTimer = window.setInterval(action, HOLD_INTERVAL_MS);
|
delayTimer = timerView.setTimeout(() => {
|
||||||
|
repeatTimer = timerView.setInterval(action, HOLD_INTERVAL_MS);
|
||||||
}, HOLD_DELAY_MS);
|
}, HOLD_DELAY_MS);
|
||||||
window.addEventListener("pointerup", stop);
|
window.addEventListener("pointerup", stop);
|
||||||
window.addEventListener("pointercancel", stop);
|
window.addEventListener("pointercancel", stop);
|
||||||
@@ -329,7 +336,7 @@ export function createEditOverlay(options: EditOverlayOptions): HTMLElement {
|
|||||||
layer.className = "b05-profile-edit";
|
layer.className = "b05-profile-edit";
|
||||||
layer.style.width = `${width}px`;
|
layer.style.width = `${width}px`;
|
||||||
|
|
||||||
const repeater = createHoldRepeater();
|
const repeater = createHoldRepeater(layer);
|
||||||
const edited = new Set(Object.keys(alignment.edits.station_offsets));
|
const edited = new Set(Object.keys(alignment.edits.station_offsets));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
import { MASSHAUL_MIN_HEIGHT } from "./B05_Profile_UI_Profile_MassHaul";
|
import { MASSHAUL_MIN_HEIGHT } from "./B05_Profile_UI_Profile_MassHaul";
|
||||||
import { TABLE_OVERLAY_MIN_HEIGHT } from "./B05_Profile_UI_Profile_TableOverlay";
|
import { TABLE_OVERLAY_MIN_HEIGHT } from "./B05_Profile_UI_Profile_TableOverlay";
|
||||||
import { STRUCTURE_LANE_HEIGHT_PX } from "./B05_Profile_UI_Structures_Marks";
|
import { STRUCTURE_LANE_HEIGHT_PX } from "./B05_Profile_UI_Structures_Marks";
|
||||||
|
import { frameOf } from "../common_util/common_util_view_window";
|
||||||
|
|
||||||
/** 그래프가 이보다 낮아지면 종단선을 읽을 수 없다 — 어떤 배분에서도 지키는 하한(px). */
|
/** 그래프가 이보다 낮아지면 종단선을 읽을 수 없다 — 어떤 배분에서도 지키는 하한(px). */
|
||||||
const MIN_CHART_HEIGHT = 100;
|
const MIN_CHART_HEIGHT = 100;
|
||||||
@@ -182,7 +183,7 @@ export function createHeightCascade(deps: HeightCascadeDeps): HeightCascade {
|
|||||||
function scheduleLightSync(): void {
|
function scheduleLightSync(): void {
|
||||||
if (lightSyncPending) return;
|
if (lightSyncPending) return;
|
||||||
lightSyncPending = true;
|
lightSyncPending = true;
|
||||||
requestAnimationFrame(() => {
|
frameOf(deps.body, () => {
|
||||||
lightSyncPending = false;
|
lightSyncPending = false;
|
||||||
syncHeightsLight();
|
syncHeightsLight();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ import { createPanelResizer } from "@ui/ui_template_resizer";
|
|||||||
import "@util/common_util_mass_haul.css";
|
import "@util/common_util_mass_haul.css";
|
||||||
import "./B05_Profile_UI_Style_MassHaul.css";
|
import "./B05_Profile_UI_Style_MassHaul.css";
|
||||||
import { attachWheelHorizontalScroll } from "./B05_Profile_UI_Profile_Wheel";
|
import { attachWheelHorizontalScroll } from "./B05_Profile_UI_Profile_Wheel";
|
||||||
|
import { frameOf } from "../common_util/common_util_view_window";
|
||||||
|
|
||||||
/** 2차 패널 펼침 여부 — 세션 동안만 유지(패널 높이·접힘과 같은 수명). */
|
/** 2차 패널 펼침 여부 — 세션 동안만 유지(패널 높이·접힘과 같은 수명). */
|
||||||
/* 펼침 상태·높이·범례는 화면 취향(①) — 키는 등록표(`b_page_state`)가 만든다. */
|
/* 펼침 상태·높이·범례는 화면 취향(①) — 키는 등록표(`b_page_state`)가 만든다. */
|
||||||
@@ -235,7 +236,7 @@ export function createRouteMassHaulPanel(
|
|||||||
syncHandlePosition();
|
syncHandlePosition();
|
||||||
if (resizeRedrawPending) return;
|
if (resizeRedrawPending) return;
|
||||||
resizeRedrawPending = true;
|
resizeRedrawPending = true;
|
||||||
requestAnimationFrame(() => {
|
frameOf(overlay, () => {
|
||||||
resizeRedrawPending = false;
|
resizeRedrawPending = false;
|
||||||
onChanged();
|
onChanged();
|
||||||
});
|
});
|
||||||
@@ -488,9 +489,13 @@ export function createRouteMassHaulPanel(
|
|||||||
);
|
);
|
||||||
drawnOnce = true;
|
drawnOnce = true;
|
||||||
// 세로창이 아직 목표까지 안 갔으면 다음 프레임에 한 걸음 더 — 확 튀지 않고 미끄러진다.
|
// 세로창이 아직 목표까지 안 갔으면 다음 프레임에 한 걸음 더 — 확 튀지 않고 미끄러진다.
|
||||||
scheduleMassHaulSettle(windowState, () => {
|
scheduleMassHaulSettle(
|
||||||
if (lastParams) draw(lastParams);
|
windowState,
|
||||||
});
|
() => {
|
||||||
|
if (lastParams) draw(lastParams);
|
||||||
|
},
|
||||||
|
overlay,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
* ========================================================================== */
|
* ========================================================================== */
|
||||||
|
|
||||||
import { isCtrlKey } from "../A00_Common/key_shortcut";
|
import { isCtrlKey } from "../A00_Common/key_shortcut";
|
||||||
|
import { viewOf } from "../common_util/common_util_view_window";
|
||||||
import type {
|
import type {
|
||||||
AlignmentBase,
|
AlignmentBase,
|
||||||
AlignmentEdits,
|
AlignmentEdits,
|
||||||
@@ -254,11 +255,13 @@ export function createPanelTools(ctx: PanelToolsContext): PanelTools {
|
|||||||
* 건드린다 — 키를 뗀 뒤 이만큼 기다렸다 한 덩어리로 기록한다. */
|
* 건드린다 — 키를 뗀 뒤 이만큼 기다렸다 한 덩어리로 기록한다. */
|
||||||
const HOLD_SETTLE_MS = 700;
|
const HOLD_SETTLE_MS = 700;
|
||||||
let settleTimer = 0;
|
let settleTimer = 0;
|
||||||
|
/** 시계는 패널이 놓인 창 것 — 새 창 최대화로 가려진 메인 창 시계는 늦게 돎(230-2). */
|
||||||
|
let timerView: Window = window;
|
||||||
|
|
||||||
/** 반복을 끊고, 잠시 뒤 그 동안의 변화를 이력 한 덩어리로 남긴다. */
|
/** 반복을 끊고, 잠시 뒤 그 동안의 변화를 이력 한 덩어리로 남긴다. */
|
||||||
function stopHold(): void {
|
function stopHold(): void {
|
||||||
window.clearTimeout(holdDelayTimer);
|
timerView.clearTimeout(holdDelayTimer);
|
||||||
window.clearInterval(holdRepeatTimer);
|
timerView.clearInterval(holdRepeatTimer);
|
||||||
holdDelayTimer = 0;
|
holdDelayTimer = 0;
|
||||||
holdRepeatTimer = 0;
|
holdRepeatTimer = 0;
|
||||||
if (heldKey === null) return;
|
if (heldKey === null) return;
|
||||||
@@ -267,8 +270,8 @@ export function createPanelTools(ctx: PanelToolsContext): PanelTools {
|
|||||||
holdStation = null;
|
holdStation = null;
|
||||||
window.removeEventListener("keyup", onKeyUp);
|
window.removeEventListener("keyup", onKeyUp);
|
||||||
window.removeEventListener("blur", stopHold);
|
window.removeEventListener("blur", stopHold);
|
||||||
window.clearTimeout(settleTimer);
|
timerView.clearTimeout(settleTimer);
|
||||||
settleTimer = window.setTimeout(() => {
|
settleTimer = timerView.setTimeout(() => {
|
||||||
settleTimer = 0;
|
settleTimer = 0;
|
||||||
history.resume();
|
history.resume();
|
||||||
}, HOLD_SETTLE_MS);
|
}, HOLD_SETTLE_MS);
|
||||||
@@ -278,7 +281,7 @@ export function createPanelTools(ctx: PanelToolsContext): PanelTools {
|
|||||||
function flushHold(): void {
|
function flushHold(): void {
|
||||||
stopHold();
|
stopHold();
|
||||||
if (!settleTimer) return;
|
if (!settleTimer) return;
|
||||||
window.clearTimeout(settleTimer);
|
timerView.clearTimeout(settleTimer);
|
||||||
settleTimer = 0;
|
settleTimer = 0;
|
||||||
history.resume();
|
history.resume();
|
||||||
}
|
}
|
||||||
@@ -292,8 +295,9 @@ export function createPanelTools(ctx: PanelToolsContext): PanelTools {
|
|||||||
heldKey = key;
|
heldKey = key;
|
||||||
window.addEventListener("keyup", onKeyUp);
|
window.addEventListener("keyup", onKeyUp);
|
||||||
window.addEventListener("blur", stopHold);
|
window.addEventListener("blur", stopHold);
|
||||||
holdDelayTimer = window.setTimeout(() => {
|
timerView = viewOf(ctx.root); // 앞선 덩어리는 flushHold 가 이미 닫음
|
||||||
holdRepeatTimer = window.setInterval(() => {
|
holdDelayTimer = timerView.setTimeout(() => {
|
||||||
|
holdRepeatTimer = timerView.setInterval(() => {
|
||||||
if (!nudge(key)) stopHold(); // 더 움직일 곳이 없으면 스스로 멈춘다.
|
if (!nudge(key)) stopHold(); // 더 움직일 곳이 없으면 스스로 멈춘다.
|
||||||
}, HOLD_INTERVAL_MS);
|
}, HOLD_INTERVAL_MS);
|
||||||
}, HOLD_DELAY_MS);
|
}, HOLD_DELAY_MS);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { createWorkflowPanelHandle } from "@ui/ui_template_overlay";
|
|||||||
import { createPanelResizer } from "@ui/ui_template_resizer";
|
import { createPanelResizer } from "@ui/ui_template_resizer";
|
||||||
import { TABLE_ROW_COUNT } from "./B05_Profile_UI_Profile_Layout";
|
import { TABLE_ROW_COUNT } from "./B05_Profile_UI_Profile_Layout";
|
||||||
import { attachWheelHorizontalScroll } from "./B05_Profile_UI_Profile_Wheel";
|
import { attachWheelHorizontalScroll } from "./B05_Profile_UI_Profile_Wheel";
|
||||||
|
import { frameOf } from "../common_util/common_util_view_window";
|
||||||
|
|
||||||
/* 펼침·높이는 화면 취향(①) — 키는 등록표(`b_page_state`)가 만든다. */
|
/* 펼침·높이는 화면 취향(①) — 키는 등록표(`b_page_state`)가 만든다. */
|
||||||
const HEIGHT_KEY = stateKey("table-height") ?? "";
|
const HEIGHT_KEY = stateKey("table-height") ?? "";
|
||||||
@@ -87,7 +88,7 @@ export function createProfileTableOverlay(onChanged: () => void): RouteProfileTa
|
|||||||
syncHandlePosition();
|
syncHandlePosition();
|
||||||
if (resizeRedrawPending) return;
|
if (resizeRedrawPending) return;
|
||||||
resizeRedrawPending = true;
|
resizeRedrawPending = true;
|
||||||
requestAnimationFrame(() => {
|
frameOf(overlay, () => {
|
||||||
resizeRedrawPending = false;
|
resizeRedrawPending = false;
|
||||||
onChanged();
|
onChanged();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
* 유토곡선·측점 테이블)이 함께 얻는다.
|
* 유토곡선·측점 테이블)이 함께 얻는다.
|
||||||
* ========================================================================== */
|
* ========================================================================== */
|
||||||
|
|
||||||
|
import { viewOf } from "../common_util/common_util_view_window";
|
||||||
|
|
||||||
/** 프레임마다 남은 거리에서 좁히는 비율. 0.35면 60fps에서 약 0.13초에 도착한다 —
|
/** 프레임마다 남은 거리에서 좁히는 비율. 0.35면 60fps에서 약 0.13초에 도착한다 —
|
||||||
* 눈으로는 이어져 보이면서, 멈춘 뒤 세로 눈금이 따라오는 시간도 예전만큼 짧다. */
|
* 눈으로는 이어져 보이면서, 멈춘 뒤 세로 눈금이 따라오는 시간도 예전만큼 짧다. */
|
||||||
const EASE = 0.35;
|
const EASE = 0.35;
|
||||||
@@ -37,13 +39,6 @@ const BASE_FRAME_MS = 1000 / 60;
|
|||||||
* 쓰면 한 번에 목표까지 튄다. */
|
* 쓰면 한 번에 목표까지 튄다. */
|
||||||
const MAX_FRAME_MS = 64;
|
const MAX_FRAME_MS = 64;
|
||||||
|
|
||||||
/** 스크롤러가 지금 놓인 창 — 종단을 새 창으로 빼면 그 창이다(230-1).
|
|
||||||
* 메인 창 전역 `requestAnimationFrame` 을 쓰면, 새 창을 최대화해 메인 창이 가려질 때
|
|
||||||
* 브라우저가 메인 창 프레임을 멈춰 휠 이동이 한 걸음도 안 나갔다. */
|
|
||||||
function viewOf(scroller: HTMLElement): Window {
|
|
||||||
return scroller.ownerDocument.defaultView ?? window;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function attachWheelHorizontalScroll(scroller: HTMLElement): void {
|
export function attachWheelHorizontalScroll(scroller: HTMLElement): void {
|
||||||
let target = -1;
|
let target = -1;
|
||||||
/** 우리가 마지막으로 써 넣은 값 — 남이 자리를 옮겼는지 가리는 데 쓴다. */
|
/** 우리가 마지막으로 써 넣은 값 — 남이 자리를 옮겼는지 가리는 데 쓴다. */
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
import { etcName, numberDuplicateNames } from "./B05_Profile_UI_Structures_List";
|
import { etcName, numberDuplicateNames } from "./B05_Profile_UI_Structures_List";
|
||||||
import { balloonLines, type ContainerCount } from "./B05_Profile_UI_Structures_Balloon";
|
import { balloonLines, type ContainerCount } from "./B05_Profile_UI_Structures_Balloon";
|
||||||
import { stationLabel as formatStation } from "../common_util/common_util_station";
|
import { stationLabel as formatStation } from "../common_util/common_util_station";
|
||||||
|
import { frameOf } from "../common_util/common_util_view_window";
|
||||||
|
|
||||||
/** 마크를 잡았다고 볼 여유(px) — 시작점에서 이만큼 **벗어나기 전에는** 이동으로
|
/** 마크를 잡았다고 볼 여유(px) — 시작점에서 이만큼 **벗어나기 전에는** 이동으로
|
||||||
* 보지 않고 시각적으로도 움직이지 않는다. 예전 10px 누적 방식은 손떨림이 쌓여
|
* 보지 않고 시각적으로도 움직이지 않는다. 예전 10px 누적 방식은 손떨림이 쌓여
|
||||||
@@ -268,7 +269,8 @@ function buildBalloon(
|
|||||||
return span;
|
return span;
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
requestAnimationFrame(() => flipIfClipped(balloon));
|
// 붙은 뒤 그 창의 틀에서 잼 — 만들 때는 아직 메인 문서 소속이라 새 창 최대화 때 멈춤(230-2).
|
||||||
|
queueMicrotask(() => frameOf(balloon, () => flipIfClipped(balloon)));
|
||||||
return balloon;
|
return balloon;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { MASS_HAUL_BALANCE_KEY } from "./common_util_mass_haul";
|
|||||||
import { appendBalanceLayer, haulPlanChips } from "./common_util_mass_haul_balance_view";
|
import { appendBalanceLayer, haulPlanChips } from "./common_util_mass_haul_balance_view";
|
||||||
import { segmentOf } from "./common_util_mass_haul_curve";
|
import { segmentOf } from "./common_util_mass_haul_curve";
|
||||||
import { L, stationLabel, svgElement, svgText } from "./common_util_svg";
|
import { L, stationLabel, svgElement, svgText } from "./common_util_svg";
|
||||||
|
import { viewOf } from "./common_util_view_window";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 유토곡선 X축을 **호출한 쪽 종단 그래프와 맞추는** 값. 좌우 여백과 누가거리 최댓값이
|
* 유토곡선 X축을 **호출한 쪽 종단 그래프와 맞추는** 값. 좌우 여백과 누가거리 최댓값이
|
||||||
@@ -74,6 +75,8 @@ export interface MassHaulWindowState {
|
|||||||
settled: boolean;
|
settled: boolean;
|
||||||
/** 예약해 둔 다음 프레임(중복 예약 방지). */
|
/** 예약해 둔 다음 프레임(중복 예약 방지). */
|
||||||
frame: number;
|
frame: number;
|
||||||
|
/** `frame` 을 받아 둔 창 — 걷을 때도 같은 창에서(230-2). */
|
||||||
|
view?: Window;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createMassHaulWindowState(): MassHaulWindowState {
|
export function createMassHaulWindowState(): MassHaulWindowState {
|
||||||
@@ -81,11 +84,17 @@ export function createMassHaulWindowState(): MassHaulWindowState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 아직 목표에 못 갔으면 다음 프레임에 한 번 더 그리게 예약한다. */
|
/** 아직 목표에 못 갔으면 다음 프레임에 한 번 더 그리게 예약한다. */
|
||||||
export function scheduleMassHaulSettle(state: MassHaulWindowState, redraw: () => void): void {
|
/** `node` = 곡선이 놓인 요소 — 그 창의 틀에서 잰다(새 창 최대화 때 메인 창 틀은 멈춤 · 230-2). */
|
||||||
if (state.frame) cancelAnimationFrame(state.frame);
|
export function scheduleMassHaulSettle(
|
||||||
|
state: MassHaulWindowState,
|
||||||
|
redraw: () => void,
|
||||||
|
node?: Node,
|
||||||
|
): void {
|
||||||
|
if (state.frame) (state.view ?? window).cancelAnimationFrame(state.frame);
|
||||||
state.frame = 0;
|
state.frame = 0;
|
||||||
if (state.settled) return;
|
if (state.settled) return;
|
||||||
state.frame = requestAnimationFrame(() => {
|
state.view = viewOf(node);
|
||||||
|
state.frame = state.view.requestAnimationFrame(() => {
|
||||||
state.frame = 0;
|
state.frame = 0;
|
||||||
redraw();
|
redraw();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
/* =============================================================================
|
||||||
|
* common_util_view_window.ts
|
||||||
|
* 요소가 지금 놓인 창 — 종단 패널을 새 창으로 빼면 그 창이다(230-1 · 230-2).
|
||||||
|
*
|
||||||
|
* 메인 창 전역 `requestAnimationFrame` · 시계를 쓰면, 새 창을 최대화해 메인 창이 가려질 때
|
||||||
|
* 브라우저가 메인 창 화면 틀을 멈추고 시계를 늦춰 휠 · 편집 · 다시 그리기가 멈췄다.
|
||||||
|
* 화면 틀 · 시계는 이 함수로 그 요소의 창에서 받는다.
|
||||||
|
* ========================================================================== */
|
||||||
|
|
||||||
|
/** 요소가 놓인 창(문서에 안 붙었으면 메인 창). */
|
||||||
|
export function viewOf(node: Node | null | undefined): Window {
|
||||||
|
return node?.ownerDocument?.defaultView ?? window;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 요소가 놓인 창의 다음 화면 틀에 부른다. */
|
||||||
|
export function frameOf(node: Node | null | undefined, callback: FrameRequestCallback): number {
|
||||||
|
return viewOf(node).requestAnimationFrame(callback);
|
||||||
|
}
|
||||||
@@ -128,3 +128,4 @@
|
|||||||
102. B05에서 종단 새창으로 띄우고 브라우져 최대화한경우 마우스 스크롤로 종단을 움직이는 기능이 작동이 안됨. 최대화를 안하면 작동됨.
|
102. B05에서 종단 새창으로 띄우고 브라우져 최대화한경우 마우스 스크롤로 종단을 움직이는 기능이 작동이 안됨. 최대화를 안하면 작동됨.
|
||||||
103. 노면공 범위 관련은 1번 방향이 더 좋을것같아. 대신 하이라이트는 여러군데 연동되어 있어 좀더 폭넓게 조사해보고 작동해줘.
|
103. 노면공 범위 관련은 1번 방향이 더 좋을것같아. 대신 하이라이트는 여러군데 연동되어 있어 좀더 폭넓게 조사해보고 작동해줘.
|
||||||
104. 3D의 측점의 노선을 선택할때 하이라이트되는데 이게 바로 꺼지는 상황이야. 일단 하이라이트가 계속 문제되는데 전체적인 조사가 필요해. 일단 조사부터 해줘. (조사)
|
104. 3D의 측점의 노선을 선택할때 하이라이트되는데 이게 바로 꺼지는 상황이야. 일단 하이라이트가 계속 문제되는데 전체적인 조사가 필요해. 일단 조사부터 해줘. (조사)
|
||||||
|
105. 추가로 하이라이트에서 소수점 단위 구조물 측점들의 경우 1단위로 사사오입하여 측점을 지정하게 반영했던걸로 기억하는데 언제 했고 어떤식으로 했는지 알아보고 어떻게 개선할지 조사만 해봐. (조사)
|
||||||
|
|||||||
@@ -79,9 +79,13 @@ globalThis.ResizeObserver = class {
|
|||||||
disconnect() {}
|
disconnect() {}
|
||||||
};
|
};
|
||||||
|
|
||||||
const wheelMod = load(path.join(ROOT, "B05_Profile", "B05_Profile_UI_Profile_Wheel.ts"));
|
const viewWindow = load(path.join(ROOT, "common_util", "common_util_view_window.ts"));
|
||||||
|
const wheelMod = load(path.join(ROOT, "B05_Profile", "B05_Profile_UI_Profile_Wheel.ts"), {
|
||||||
|
"../common_util/common_util_view_window": viewWindow,
|
||||||
|
});
|
||||||
const viewport = load(path.join(ROOT, "A00_Common", "design", "design_profile_viewport.ts"), {
|
const viewport = load(path.join(ROOT, "A00_Common", "design", "design_profile_viewport.ts"), {
|
||||||
"../../B05_Profile/B05_Profile_UI_Profile_Wheel": wheelMod,
|
"../../B05_Profile/B05_Profile_UI_Profile_Wheel": wheelMod,
|
||||||
|
"@util/common_util_view_window": viewWindow,
|
||||||
"@util/common_util_chart_ywindow": { needsFullRedraw: () => true },
|
"@util/common_util_chart_ywindow": { needsFullRedraw: () => true },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,227 @@
|
|||||||
|
/* 종단 새 창 최대화 — 같은 꼴 나머지(계획서 230-2) 단위검증 헬퍼.
|
||||||
|
* 메인 창은 「가려짐」(화면 틀 · 시계 멈춤) · 종단 요소는 새 창 소속. 실제 TS 의 해당 함수 본문을
|
||||||
|
* 잘라 그 자리에서 트랜스파일해 돌리고 JSON 으로 냄 — 다시 그리기 · 높이 맞춤 · 표 · 유토곡선 ·
|
||||||
|
* 끌기 뒤 그리기 · 길게 누르기 · 배수유역도가 새 창 틀 · 시계로 도는지. */
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||||
|
|
||||||
|
const ROOT = path.join(__dirname, "..", "..");
|
||||||
|
const read = (...p) => fs.readFileSync(path.join(ROOT, ...p), "utf8");
|
||||||
|
const run = (code, scope) => {
|
||||||
|
const js = ts.transpileModule(code, {
|
||||||
|
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||||
|
}).outputText;
|
||||||
|
const names = Object.keys(scope);
|
||||||
|
return new Function(...names, js)(...names.map((n) => scope[n]));
|
||||||
|
};
|
||||||
|
/** `start` 부터 짝 맞는 닫는 중괄호까지 잘라 냄 — 본문 여는 괄호가 반환 타입 뒤면 `open` 으로 짚음. */
|
||||||
|
function block(src, start, open = "{") {
|
||||||
|
const at = src.indexOf(start);
|
||||||
|
if (at < 0) throw new Error(`본문 없음: ${start}`);
|
||||||
|
let depth = 0;
|
||||||
|
for (let i = src.indexOf(open, at) + open.length - 1; i < src.length; i++) {
|
||||||
|
if (src[i] === "{") depth++;
|
||||||
|
else if (src[i] === "}" && --depth === 0) return src.slice(at, i + 1);
|
||||||
|
}
|
||||||
|
throw new Error(`닫힘 없음: ${start}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 가짜 창 — frozen 이면 화면 틀 · 시계가 안 돎 ----
|
||||||
|
function fakeWindow(name, frozen) {
|
||||||
|
const win = { name, frozen, now: 0, timers: [], seq: 0, frames: 0, listeners: [] };
|
||||||
|
win.setTimeout = (fn, ms) => (win.timers.push({ id: ++win.seq, at: win.now + ms, fn }), win.seq);
|
||||||
|
win.clearTimeout = (id) => (win.timers = win.timers.filter((t) => t.id !== id));
|
||||||
|
win.setInterval = (fn, ms) => {
|
||||||
|
const id = ++win.seq;
|
||||||
|
const again = (at) => win.timers.push({ id, at, fn: () => (fn(), again(win.now + ms)) });
|
||||||
|
again(win.now + ms);
|
||||||
|
return id;
|
||||||
|
};
|
||||||
|
win.clearInterval = win.clearTimeout;
|
||||||
|
win.requestAnimationFrame = (fn) => win.setTimeout(() => (win.frames++, fn(win.now)), 16);
|
||||||
|
win.cancelAnimationFrame = win.clearTimeout;
|
||||||
|
win.addEventListener = (type, fn) => win.listeners.push([type, fn]);
|
||||||
|
win.removeEventListener = (type, fn) =>
|
||||||
|
(win.listeners = win.listeners.filter(([t, f]) => t !== type || f !== fn));
|
||||||
|
win.tick = (ms) => {
|
||||||
|
if (win.frozen) return;
|
||||||
|
const end = win.now + ms;
|
||||||
|
for (;;) {
|
||||||
|
const next = win.timers.filter((t) => t.at <= end).sort((a, b) => a.at - b.at)[0];
|
||||||
|
if (!next) break;
|
||||||
|
win.now = next.at;
|
||||||
|
win.clearTimeout(next.id);
|
||||||
|
next.fn();
|
||||||
|
}
|
||||||
|
win.now = end;
|
||||||
|
};
|
||||||
|
return win;
|
||||||
|
}
|
||||||
|
|
||||||
|
const main = fakeWindow("main", true);
|
||||||
|
const popup = fakeWindow("popup", false);
|
||||||
|
globalThis.window = main;
|
||||||
|
globalThis.requestAnimationFrame = main.requestAnimationFrame; // 옛 코드가 쓰던 전역 = 메인 창
|
||||||
|
globalThis.cancelAnimationFrame = main.cancelAnimationFrame;
|
||||||
|
const inPopup = () => ({ ownerDocument: { defaultView: popup }, style: { setProperty() {} } });
|
||||||
|
|
||||||
|
const vw = {};
|
||||||
|
run(
|
||||||
|
read("common_util", "common_util_view_window.ts").replace(/export /g, "") +
|
||||||
|
"\nObject.assign(vw, { viewOf, frameOf });",
|
||||||
|
{ vw },
|
||||||
|
);
|
||||||
|
const { viewOf, frameOf } = vw;
|
||||||
|
const out = {};
|
||||||
|
const mainBusy = () => main.timers.length;
|
||||||
|
|
||||||
|
// ① 공용 함수 — 새 창 요소는 새 창 · 안 붙은 요소는 메인 창
|
||||||
|
out.viewOf = {
|
||||||
|
popup: viewOf(inPopup()).name,
|
||||||
|
detached: viewOf({ ownerDocument: { defaultView: null } }).name,
|
||||||
|
none: viewOf(null).name,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ② 계획고 편집 뒤 다시 그리기(design_profile rebuild) — 두 번 불러도 한 번만 그림
|
||||||
|
{
|
||||||
|
const s = { draws: 0, previews: 0 };
|
||||||
|
const src = block(
|
||||||
|
read("A00_Common", "design", "design_profile.ts"),
|
||||||
|
"function rebuild(): void {",
|
||||||
|
);
|
||||||
|
run(`let redrawPending = false; ${src}\nrebuild(); rebuild(); s.rebuild = true;`, {
|
||||||
|
s,
|
||||||
|
base: null,
|
||||||
|
alignment: null,
|
||||||
|
buildAlignment: () => null,
|
||||||
|
store: { edits: () => ({}) },
|
||||||
|
scheduleCrossPreview: () => s.previews++,
|
||||||
|
draw: () => s.draws++,
|
||||||
|
root: inPopup(),
|
||||||
|
frameOf,
|
||||||
|
});
|
||||||
|
popup.tick(50);
|
||||||
|
out.rebuild = { draws: s.draws, previews: s.previews, mainTimers: mainBusy() };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ③ 높이 맞춤(Heights scheduleLightSync)
|
||||||
|
{
|
||||||
|
const s = { syncs: 0 };
|
||||||
|
const src = block(
|
||||||
|
read("B05_Profile", "B05_Profile_UI_Profile_Heights.ts"),
|
||||||
|
"function scheduleLightSync(): void {",
|
||||||
|
);
|
||||||
|
run(`let lightSyncPending = false; ${src}\nscheduleLightSync(); scheduleLightSync();`, {
|
||||||
|
deps: { body: inPopup() },
|
||||||
|
syncHeightsLight: () => s.syncs++,
|
||||||
|
frameOf,
|
||||||
|
});
|
||||||
|
popup.tick(50);
|
||||||
|
out.heights = { syncs: s.syncs, mainTimers: mainBusy() };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ④ 표 · 유토곡선 크기 끌기(TableOverlay · MassHaul onResize)
|
||||||
|
for (const [key, file] of [
|
||||||
|
["table", "B05_Profile_UI_Profile_TableOverlay.ts"],
|
||||||
|
["massHaul", "B05_Profile_UI_Profile_MassHaul.ts"],
|
||||||
|
]) {
|
||||||
|
const s = { changed: 0, handles: 0 };
|
||||||
|
const src = block(read("B05_Profile", file), "onResize: () => {").replace(
|
||||||
|
/^onResize: /,
|
||||||
|
"const onResize = ",
|
||||||
|
);
|
||||||
|
run(`let resizeRedrawPending = false; ${src}\nonResize(); onResize();`, {
|
||||||
|
syncHandlePosition: () => s.handles++,
|
||||||
|
onChanged: () => s.changed++,
|
||||||
|
overlay: inPopup(),
|
||||||
|
frameOf,
|
||||||
|
});
|
||||||
|
popup.tick(50);
|
||||||
|
out[key] = { changed: s.changed, handles: s.handles, mainTimers: mainBusy() };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ⑤ 유토곡선 세로창 미끄러짐(scheduleMassHaulSettle)
|
||||||
|
{
|
||||||
|
const s = { redraws: 0 };
|
||||||
|
const src = block(
|
||||||
|
read("common_util", "common_util_mass_haul_view.ts"),
|
||||||
|
"export function scheduleMassHaulSettle(",
|
||||||
|
).replace(/^export /, "");
|
||||||
|
const state = { held: null, settled: false, frame: 0 };
|
||||||
|
run(
|
||||||
|
`${src}\nscheduleMassHaulSettle(state, () => s.redraws++, node); scheduleMassHaulSettle(state, () => s.redraws++, node);`,
|
||||||
|
{
|
||||||
|
s,
|
||||||
|
state,
|
||||||
|
node: inPopup(),
|
||||||
|
viewOf,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
popup.tick(50);
|
||||||
|
out.settle = {
|
||||||
|
redraws: s.redraws,
|
||||||
|
frame: state.frame,
|
||||||
|
view: state.view?.name,
|
||||||
|
mainTimers: mainBusy(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ⑥ 끌기 뒤 다시 그리기(Profile_Drag scheduleDraw · 120ms 모음)
|
||||||
|
{
|
||||||
|
const s = { draws: 0 };
|
||||||
|
const src = block(
|
||||||
|
read("B05_Profile", "B05_Profile_UI_Profile_Drag.ts"),
|
||||||
|
"const scheduleDraw = (): void => {",
|
||||||
|
);
|
||||||
|
run(`let resizeTimer = 0; let timerView = window; ${src}\nscheduleDraw(); scheduleDraw();`, {
|
||||||
|
window: main,
|
||||||
|
root: inPopup(),
|
||||||
|
ctx: { draw: () => s.draws++ },
|
||||||
|
viewOf,
|
||||||
|
});
|
||||||
|
popup.tick(200);
|
||||||
|
out.drag = { draws: s.draws, mainTimers: mainBusy() };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ⑦ ▲▼ 길게 누르기(Profile_Edit createHoldRepeater) — 0.5초 뒤 초당 10회
|
||||||
|
{
|
||||||
|
const s = { acts: 0 };
|
||||||
|
const src = block(
|
||||||
|
read("B05_Profile", "B05_Profile_UI_Profile_Edit.ts"),
|
||||||
|
"function createHoldRepeater(",
|
||||||
|
"} {",
|
||||||
|
);
|
||||||
|
run(`${src}\nconst r = createHoldRepeater(node); r.start(() => s.acts++); s.r = r;`, {
|
||||||
|
s,
|
||||||
|
window: main,
|
||||||
|
node: inPopup(),
|
||||||
|
viewOf,
|
||||||
|
HOLD_DELAY_MS: 500,
|
||||||
|
HOLD_INTERVAL_MS: 100,
|
||||||
|
});
|
||||||
|
popup.tick(1000);
|
||||||
|
const acts = s.acts;
|
||||||
|
s.r.stop();
|
||||||
|
popup.tick(1000);
|
||||||
|
out.hold = { acts, afterStop: s.acts, mainTimers: mainBusy(), popupTimers: popup.timers.length };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ⑧ 배수유역도(Drainage_Panel scheduleDraw)
|
||||||
|
{
|
||||||
|
const s = { draws: 0 };
|
||||||
|
const src = block(
|
||||||
|
read("B05_Profile", "B05_Profile_UI_Drainage_Panel.ts"),
|
||||||
|
"function scheduleDraw(): void {",
|
||||||
|
);
|
||||||
|
run(`let frameHandle = 0; let frameView = window; ${src}\nscheduleDraw(); scheduleDraw();`, {
|
||||||
|
window: main,
|
||||||
|
root: inPopup(),
|
||||||
|
draw: () => s.draws++,
|
||||||
|
viewOf,
|
||||||
|
});
|
||||||
|
popup.tick(50);
|
||||||
|
out.drainage = { draws: s.draws, mainTimers: mainBusy() };
|
||||||
|
}
|
||||||
|
|
||||||
|
process.stdout.write(JSON.stringify(out));
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""종단 새 창 최대화 — 같은 꼴 나머지(계획서 230-2) 회귀검증.
|
||||||
|
|
||||||
|
230-1 과 같은 까닭 — 메인 창 전역 화면 틀 · 시계는 새 창 최대화로 메인 창이 가려지면 멈추거나
|
||||||
|
늦게 돎. 새 창으로 가는 종단 패널(종단도 · 표 · 유토곡선 · 배수유역도)의 틀 · 시계는 공용
|
||||||
|
`viewOf` · `frameOf`(common_util_view_window) 로 그 요소가 놓인 창에서 받음.
|
||||||
|
|
||||||
|
`helper_230_2_popout_frames.cjs` 가 메인 창을 「가려짐」(틀 · 시계 멈춤)으로 두고 실제 함수 본문을 돌림.
|
||||||
|
옛 코드로는 아래 다시 그리기가 모두 0 번.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
ROOT = os.path.dirname(os.path.dirname(HERE))
|
||||||
|
|
||||||
|
# 새 창으로 가는 종단 패널의 화면 코드
|
||||||
|
POPOUT_FILES = [
|
||||||
|
"A00_Common/design/design_profile.ts",
|
||||||
|
"A00_Common/design/design_profile_viewport.ts",
|
||||||
|
"B05_Profile/B05_Profile_UI_Profile_Wheel.ts",
|
||||||
|
"B05_Profile/B05_Profile_UI_Profile_Heights.ts",
|
||||||
|
"B05_Profile/B05_Profile_UI_Profile_MassHaul.ts",
|
||||||
|
"B05_Profile/B05_Profile_UI_Profile_TableOverlay.ts",
|
||||||
|
"B05_Profile/B05_Profile_UI_Profile_Drag.ts",
|
||||||
|
"B05_Profile/B05_Profile_UI_Profile_Edit.ts",
|
||||||
|
"B05_Profile/B05_Profile_UI_Profile_Panel_Tools.ts",
|
||||||
|
"B05_Profile/B05_Profile_UI_Drainage_Panel.ts",
|
||||||
|
"B05_Profile/B05_Profile_UI_Structures_Marks.ts",
|
||||||
|
"common_util/common_util_mass_haul_view.ts",
|
||||||
|
]
|
||||||
|
# 메인 창 전역 틀 · 시계 — `x.requestAnimationFrame` 처럼 창을 짚은 것은 제외
|
||||||
|
MAIN_WINDOW_CLOCK = re.compile(
|
||||||
|
r"(?<![.\w])(?:window\.)?(requestAnimationFrame|cancelAnimationFrame|setTimeout|setInterval)\("
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def r():
|
||||||
|
proc = subprocess.run(
|
||||||
|
["node", os.path.join(HERE, "helper_230_2_popout_frames.cjs")],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
encoding="utf-8",
|
||||||
|
timeout=60,
|
||||||
|
)
|
||||||
|
assert proc.returncode == 0, proc.stderr
|
||||||
|
return json.loads(proc.stdout)
|
||||||
|
|
||||||
|
|
||||||
|
def test_공용_함수_놓인_창(r):
|
||||||
|
assert r["viewOf"] == {"popup": "popup", "detached": "main", "none": "main"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_계획고_편집_뒤_다시_그림(r):
|
||||||
|
assert r["rebuild"] == {"draws": 1, "previews": 2, "mainTimers": 0} # 두 번 불러도 한 번
|
||||||
|
|
||||||
|
|
||||||
|
def test_높이_맞춤(r):
|
||||||
|
assert r["heights"] == {"syncs": 1, "mainTimers": 0}
|
||||||
|
|
||||||
|
|
||||||
|
def test_표_유토곡선_크기_끌기_다시_그림(r):
|
||||||
|
assert r["table"] == {"changed": 1, "handles": 2, "mainTimers": 0}
|
||||||
|
assert r["massHaul"] == {"changed": 1, "handles": 2, "mainTimers": 0}
|
||||||
|
|
||||||
|
|
||||||
|
def test_유토곡선_세로창_미끄러짐(r):
|
||||||
|
assert r["settle"] == {"redraws": 1, "frame": 0, "view": "popup", "mainTimers": 0}
|
||||||
|
|
||||||
|
|
||||||
|
def test_끌기_뒤_다시_그림(r):
|
||||||
|
assert r["drag"] == {"draws": 1, "mainTimers": 0}
|
||||||
|
|
||||||
|
|
||||||
|
def test_길게_누르기_새_창_시계(r):
|
||||||
|
# 0.5초 뒤부터 0.1초마다 — 1초 동안 5번 · 멈춘 뒤 더 안 돎
|
||||||
|
assert r["hold"] == {"acts": 5, "afterStop": 5, "mainTimers": 0, "popupTimers": 0}
|
||||||
|
|
||||||
|
|
||||||
|
def test_배수유역도_다시_그림(r):
|
||||||
|
assert r["drainage"] == {"draws": 1, "mainTimers": 0}
|
||||||
|
|
||||||
|
|
||||||
|
def test_새_창_패널_코드에_메인_창_틀_시계_없음():
|
||||||
|
found = []
|
||||||
|
for rel in POPOUT_FILES:
|
||||||
|
with open(os.path.join(ROOT, rel), encoding="utf-8") as f:
|
||||||
|
for no, line in enumerate(f, 1):
|
||||||
|
code = line.split("//")[0]
|
||||||
|
if code.lstrip().startswith("*"):
|
||||||
|
continue
|
||||||
|
if MAIN_WINDOW_CLOCK.search(code):
|
||||||
|
found.append(f"{rel}:{no}: {line.strip()}")
|
||||||
|
assert found == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_design_profile_700줄_안():
|
||||||
|
with open(os.path.join(ROOT, "A00_Common/design/design_profile.ts"), encoding="utf-8") as f:
|
||||||
|
assert sum(1 for _ in f) <= 700
|
||||||
Reference in New Issue
Block a user