Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions .github/workflows/pr-policy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,23 +73,20 @@ jobs:
/^\.github\/PULL_REQUEST_TEMPLATE\.md$/,
/^\.github\/workflows\/pr-policy\.yml$/,
/^CONTRIBUTING\.md$/,
/^README\.md$/,
];

const isDocsPolicyOnlyPr =
changedFiles.length > 0 &&
changedFiles.every((file) =>
docsPolicyOnlyPatterns.some((pattern) => pattern.test(file.filename))
);
const isDocsAreaOnly = selectedAreas.length === 1 && selectedAreas[0] === "Docs";

const issuePattern = /\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?|refs?)\s+#\d+\b/i;
const hasIssueReference = issuePattern.test(relatedIssueSection);
const usesDocsOnlyException = noIssuePattern.test(relatedIssueSection);

if (!hasIssueReference) {
if (!(usesDocsOnlyException && isDocsAreaOnly && isDocsPolicyOnlyPr)) {
errors.push("`## Related Issue` must include an issue reference such as `Closes #12`, unless this is a Docs-only docs/policy PR and the section says `None (docs/policy-only PR)`.");
if (!(usesDocsOnlyException && isDocsPolicyOnlyPr)) {
errors.push("`## Related Issue` must include an issue reference such as `Closes #12`, unless changed files are limited to the docs/policy paths and the section says `None (docs/policy-only PR)`.");
}
}

Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ PR 제목은 아래 형식을 따릅니다.
- 아키텍처 규칙 점검 결과
- 빌드/테스트 검증 결과 또는 미실행 사유
- 남은 리스크나 후속 작업
- docs/policy-only PR이고 변경 경로가 `docs/`, `uml/`, `CONTRIBUTING.md`, PR/issue template, `pr-policy.yml`에만 한정되면 `Related Issue`에는 `None (docs/policy-only PR)`를 사용합니다.

## 아키텍처 체크

Expand Down
66 changes: 37 additions & 29 deletions docs/process/GitHub Project.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,35 @@
- 선행 작업 관계는 GitHub native issue dependency `blocked by`를 사용한다.
- Task 제목은 `Task-short title` 형식을 사용하고, 순서는 제목 번호가 아니라 GitHub issue 번호, Sprint, Parent issue, dependency로 관리한다.

## 현재 필드
- `Title`
- `Status`
- `In Progress`
- `Done`
- `Area`
- `⚙️Engine`
- `🏃Domain`
- `🖥️Application`
- `🔎Analysis`
- `📄Docs`
- 메모: issue form은 `Build`도 받지만, 현재 Project `Area` 필드에는 `Build` 옵션이 없다.
- `Sprint`
- `Sprint 1`
- `Sprint 2`
- `Sprint 3`
- `Later`
- `Parent issue`
- `Sub-issues progress`
## 실제 필드
- 기본 컨텍스트 필드
- `Title`
- `Assignees`
- `Labels`
- `Linked pull requests`
- `Milestone`
- `Repository`
- `Reviewers`
- 작업 추적 필드
- `Status`
- `In Progress`
- `Done`
- `Area`
- `⚙️Engine`
- `🏃Domain`
- `🖥️Application`
- `🔎Analysis`
- `📄Docs`
- `Sprint`
- `Sprint 1`
- `Sprint 2`
- `Sprint 3`
- `Later`
- `Parent issue`
- `Sub-issues progress`
- 메모
- issue form은 `Build`도 받지만, 현재 Project `Area` 필드에는 `Build` 옵션이 없다.
- `Build` 성격 작업은 issue 본문에는 `Build`로 남기고, 보드에서는 가장 가까운 기존 영역으로 배치한다.

## 현재 뷰
- 뷰 수: 1개
Expand All @@ -44,19 +54,18 @@
- `Parent issue`
- `Sub-issues progress`

## 현재 구조
## 현재 상위 구조
- `Sprint 1`
- Epic: `#1 EPIC-1 Engine Foundation`
- Epic: `#2 EPIC-2 Sprint 1 Demo Vertical Slice`
- Engine foundation tasks: `#6 ~ #13`, `#47`
- Demo vertical slice tasks: `#14 ~ #20`, `#52 ~ #55`
- Epic: `#3 EPIC-3 MVP 코어 구조 정렬`
- `Sprint 2`
- Epic: `#3 EPIC-3 Product Completion for Sprint 2`
- Epic: `#4 EPIC-4 Compare and Presentation Readiness`
- Task: `#21 ~ #30` (`Task-...` 형식)
- `Sprint 3`
- Epic: `#5 EPIC-5 Finish and Optional Extensions`
- Task: `#31 ~ #35` (`Task-...` 형식)
- Epic: `#5 EPIC-5 1차 확장 기능`
- `Later`
- Epic: `#97 EPIC-중기 확장 기능`
- Epic: `#98 EPIC-연구 후보 검토`

## 메모
- 작업을 시작하기 전에 먼저 관련 issue가 이미 있는지 확인한다.
Expand All @@ -65,11 +74,10 @@
- Project view에서 `Parent issue`, `Sub-issues progress` 컬럼으로 연결 결과가 보이는지 확인한다.
- `Docs`, `Chore`, `Analysis`는 `Lightweight Task` form으로 가볍게 등록한다.
- `Engine`, `Domain`, `Application`, `Build`는 `Implementation Task` form으로 범위와 검증 계획까지 남긴다.
- 현재 Project 보드의 `Area` 필드에는 `Build` 옵션이 없으므로, Build 성격 task는 issue form과 본문에는 `Build`로 남기고 보드에서는 임시로 가장 가까운 기존 영역에 배치한다.
- docs/policy-only 예외는 변경 경로가 `docs/`, `uml/`, `CONTRIBUTING.md`, PR/issue template, `pr-policy.yml`에만 한정될 때만 사용한다.
- docs/policy-only PR의 `Related Issue` 섹션은 `None (docs/policy-only PR)`로 남긴다.
- 세부 작업명, 부모-자식 관계, dependency는 GitHub Project와 issue 자체를 기준으로 관리한다.
- `blocked by`는 실제로 선행 해결이 필요한 hard dependency에만 건다. 단순한 권장 순서나 같은 Epic 안의 묶음 관계 때문에 불필요하게 직렬화하지 않는다.
- 하나의 Task가 서로 다른 관심사를 함께 묶어 병렬 진행을 막으면, 별도 Task로 분리해서 dependency를 다시 연결한다.
- 문서 또는 기여 정책만 다루는 변경은 별도 issue 없이 진행할 수 있다.
- 변경 범위가 `docs/`, `uml/`, `CONTRIBUTING.md`, PR/issue template, PR 정책 워크플로에만 한정되면 유지보수자는 PR 없이 `main`에 직접 push할 수 있다.
- Task의 순서는 제목 접두사 뒤 숫자로 관리하지 않는다. 중간 작업이 생기면 새 issue를 추가하고 `Sprint`, `Parent issue`, `blocked by`로 위치를 표현한다.
- `#23 Task-Implement drawing import or preprocessing to FacilityLayout2D flow`는 범위가 넓어서 삭제했고, Sprint 1 데모용 import 흐름은 `#52 ~ #55`로 분리했다.
167 changes: 139 additions & 28 deletions src/application/MainWindow.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
#include "application/MainWindow.h"

#include <QFormLayout>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QLabel>
#include <QPushButton>
#include <QTimer>
Expand Down Expand Up @@ -28,6 +31,19 @@ QString stateToString(safecrowd::engine::EngineState state) {
return "Unknown";
}

QLabel* createBodyLabel(const QString& text, QWidget* parent) {
auto* label = new QLabel(text, parent);
label->setWordWrap(true);
label->setTextFormat(Qt::RichText);
return label;
}

QLabel* createValueLabel(QWidget* parent) {
auto* label = new QLabel("-", parent);
label->setTextInteractionFlags(Qt::TextSelectableByMouse);
return label;
}

} // namespace

namespace safecrowd::application {
Expand All @@ -36,64 +52,159 @@ MainWindow::MainWindow(safecrowd::domain::SafeCrowdDomain& domain, QWidget* pare
: QMainWindow(parent),
domain_(domain) {
auto* centralWidget = new QWidget(this);
auto* layout = new QVBoxLayout(centralWidget);
statusLabel_ = new QLabel(this);

auto* startButton = new QPushButton("Start", this);
auto* pauseButton = new QPushButton("Pause", this);
auto* stopButton = new QPushButton("Stop", this);

layout->addWidget(statusLabel_);
layout->addWidget(startButton);
layout->addWidget(pauseButton);
layout->addWidget(stopButton);
auto* rootLayout = new QHBoxLayout(centralWidget);
rootLayout->setContentsMargins(18, 18, 18, 18);
rootLayout->setSpacing(16);

auto* workspaceGroup = new QGroupBox("Project Workspace", centralWidget);
auto* workspaceLayout = new QVBoxLayout(workspaceGroup);
workspaceLayout->setSpacing(12);
workspaceLayout->addWidget(createBodyLabel(
"<b>1. Import &amp; Validate</b><br/>"
"DXF and facility topology import, review, and manual correction will surface here.",
workspaceGroup));
workspaceLayout->addWidget(createBodyLabel(
"<b>2. Scenario Editor</b><br/>"
"Baseline and variation authoring stay in the same workspace but outside the run panel.",
workspaceGroup));
workspaceLayout->addWidget(createBodyLabel(
"<b>3. Results &amp; Recommendation</b><br/>"
"Run summaries, comparison, export, and recommendation remain downstream of persisted artifacts.",
workspaceGroup));
workspaceLayout->addStretch();

auto* workspaceColumn = new QVBoxLayout();
workspaceColumn->setSpacing(16);
workspaceColumn->addWidget(createBodyLabel(
"<b>SafeCrowd Workspace Prototype</b><br/>"
"This shell now mirrors the documented workflow. Only playback control is wired live today; "
"the rest of the workspace is reserved so future application features land in explicit sections.",
centralWidget));

auto* runControlGroup = new QGroupBox("Run Control Panel", centralWidget);
auto* runControlLayout = new QVBoxLayout(runControlGroup);
runControlLayout->setSpacing(10);
runControlLayout->addWidget(createBodyLabel(
"Playback control remains the active path into the current runtime prototype.",
runControlGroup));

auto* buttonLayout = new QHBoxLayout();
startButton_ = new QPushButton("Start Playback", runControlGroup);
pauseButton_ = new QPushButton("Pause Playback", runControlGroup);
stopButton_ = new QPushButton("Stop Playback", runControlGroup);
buttonLayout->addWidget(startButton_);
buttonLayout->addWidget(pauseButton_);
buttonLayout->addWidget(stopButton_);
runControlLayout->addLayout(buttonLayout);
runControlLayout->addWidget(createBodyLabel(
"<b>Planned next:</b> execution readiness checks, repeat runs, and variation selection.",
runControlGroup));

auto* runtimeStatusGroup = new QGroupBox("Runtime Status", centralWidget);
auto* runtimeStatusLayout = new QFormLayout(runtimeStatusGroup);
runtimeStatusLayout->setLabelAlignment(Qt::AlignLeft);
runtimeStatusLayout->setFormAlignment(Qt::AlignTop | Qt::AlignLeft);

runtimeStateValue_ = createValueLabel(runtimeStatusGroup);
frameValue_ = createValueLabel(runtimeStatusGroup);
fixedStepValue_ = createValueLabel(runtimeStatusGroup);
alphaValue_ = createValueLabel(runtimeStatusGroup);
runValue_ = createValueLabel(runtimeStatusGroup);
variationValue_ = createValueLabel(runtimeStatusGroup);

runtimeStatusLayout->addRow("Engine state", runtimeStateValue_);
runtimeStatusLayout->addRow("Rendered frames", frameValue_);
runtimeStatusLayout->addRow("Fixed steps", fixedStepValue_);
runtimeStatusLayout->addRow("Interpolation alpha", alphaValue_);
runtimeStatusLayout->addRow("Current run", runValue_);
runtimeStatusLayout->addRow("Variation", variationValue_);

auto* resultsGroup = new QGroupBox("Results Pipeline", centralWidget);
auto* resultsLayout = new QVBoxLayout(resultsGroup);
resultsLayout->setSpacing(12);
resultsLayout->addWidget(createBodyLabel(
"<b>Run Results Panel</b><br/>"
"Single-run and variation summaries will read persisted artifacts first.",
resultsGroup));
resultsLayout->addWidget(createBodyLabel(
"<b>Comparison View</b><br/>"
"Baseline versus alternative comparisons stay separate from live runtime state.",
resultsGroup));
resultsLayout->addWidget(createBodyLabel(
"<b>Export &amp; Recommendation</b><br/>"
"Artifact export and recommendation evidence remain downstream consumers of saved results.",
resultsGroup));

workspaceColumn->addWidget(runControlGroup);
workspaceColumn->addWidget(runtimeStatusGroup);
workspaceColumn->addWidget(resultsGroup);
workspaceColumn->addStretch();

rootLayout->addWidget(workspaceGroup, 5);
rootLayout->addLayout(workspaceColumn, 7);

tickTimer_ = new QTimer(this);
tickTimer_->setInterval(16);

connect(startButton, &QPushButton::clicked, this, [this]() { startSimulation(); });
connect(pauseButton, &QPushButton::clicked, this, [this]() { pauseSimulation(); });
connect(stopButton, &QPushButton::clicked, this, [this]() { stopSimulation(); });
connect(startButton_, &QPushButton::clicked, this, [this]() { startSimulation(); });
connect(pauseButton_, &QPushButton::clicked, this, [this]() { pauseSimulation(); });
connect(stopButton_, &QPushButton::clicked, this, [this]() { stopSimulation(); });
connect(tickTimer_, &QTimer::timeout, this, [this]() { tickSimulation(); });

setCentralWidget(centralWidget);
setWindowTitle("SafeCrowd");
resize(420, 220);
setWindowTitle("SafeCrowd Workspace");
resize(980, 560);

refreshStatusLabel();
refreshRuntimePanel();
}

void MainWindow::startSimulation() {
domain_.start();
tickTimer_->start();
refreshStatusLabel();
refreshRuntimePanel();
}

void MainWindow::pauseSimulation() {
domain_.pause();
tickTimer_->stop();
refreshStatusLabel();
refreshRuntimePanel();
}

void MainWindow::stopSimulation() {
domain_.stop();
tickTimer_->stop();
refreshStatusLabel();
refreshRuntimePanel();
}

void MainWindow::tickSimulation() {
domain_.update(1.0 / 60.0);
refreshStatusLabel();
refreshRuntimePanel();
}

void MainWindow::refreshStatusLabel() {
void MainWindow::refreshRuntimePanel() {
using safecrowd::engine::EngineState;

const auto summary = domain_.summary();
statusLabel_->setText(
QString("State: %1\nFrames: %2\nFixed Steps: %3\nAlpha: %4")
.arg(stateToString(summary.state))
.arg(summary.frameIndex)
.arg(summary.fixedStepIndex)
.arg(summary.alpha, 0, 'f', 2));
runtimeStateValue_->setText(stateToString(summary.state));
frameValue_->setText(QString::number(summary.frameIndex));
fixedStepValue_->setText(QString::number(summary.fixedStepIndex));
alphaValue_->setText(QString::number(summary.alpha, 'f', 2));

if (summary.state == EngineState::Running || summary.state == EngineState::Paused) {
runValue_->setText("Prototype run 1 / 1");
} else if (summary.frameIndex > 0 || summary.fixedStepIndex > 0) {
runValue_->setText("Last prototype run retained");
} else {
runValue_->setText("Ready for first run");
}

variationValue_->setText("Baseline placeholder (domain wiring pending)");

const bool isRunning = summary.state == EngineState::Running;
const bool isPaused = summary.state == EngineState::Paused;
startButton_->setEnabled(!isRunning);
pauseButton_->setEnabled(isRunning);
stopButton_->setEnabled(isRunning || isPaused || summary.frameIndex > 0 || summary.fixedStepIndex > 0);
}

} // namespace safecrowd::application
13 changes: 11 additions & 2 deletions src/application/MainWindow.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class SafeCrowdDomain;
}

class QLabel;
class QPushButton;
class QTimer;

namespace safecrowd::application {
Expand All @@ -20,10 +21,18 @@ class MainWindow : public QMainWindow {
void pauseSimulation();
void stopSimulation();
void tickSimulation();
void refreshStatusLabel();
void refreshRuntimePanel();

safecrowd::domain::SafeCrowdDomain& domain_;
QLabel* statusLabel_{nullptr};
QPushButton* startButton_{nullptr};
QPushButton* pauseButton_{nullptr};
QPushButton* stopButton_{nullptr};
QLabel* runtimeStateValue_{nullptr};
QLabel* frameValue_{nullptr};
QLabel* fixedStepValue_{nullptr};
QLabel* alphaValue_{nullptr};
QLabel* runValue_{nullptr};
QLabel* variationValue_{nullptr};
QTimer* tickTimer_{nullptr};
};

Expand Down
Loading