diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 1261df1..4d54eac 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -17,11 +17,14 @@ jobs: contents: read pull-requests: write steps: - - uses: actions/checkout@v5 - - uses: actions/setup-node@v5 - - run: | - npm install - npx eslint ./src/**/*.ts + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: 24 + registry-url: "https://registry.npmjs.org" + - run: npm install -D eslint-plugin-unused-imports + working-directory: ./template + - run: npx eslint ./src/**/*.ts working-directory: ./template windows-browser-test: @@ -30,9 +33,13 @@ jobs: contents: read pull-requests: write steps: - - uses: actions/checkout@v5 - - uses: actions/setup-node@v5 - - run: | - npm install - npx eslint ./src/**/*.ts - working-directory: ./template \ No newline at end of file + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: 24 + registry-url: "https://registry.npmjs.org" + - run: npm install -D eslint-plugin-unused-imports + working-directory: ./template + - run: npx eslint ./src/**/*.ts + working-directory: ./template + \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 4a2fb1d..89ea179 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -5,18 +5,18 @@ on: branches: - main +permissions: + id-token: write + contents: read + jobs: - build: + publish: runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: write steps: - - uses: actions/checkout@v5 - - uses: actions/setup-node@v5 + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 with: - node-version: "22.x" + node-version: 24 registry-url: "https://registry.npmjs.org" - - run: npm publish --access public - env: - NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }} \ No newline at end of file + - run: npm install -g npm@latest + - run: npm publish \ No newline at end of file diff --git a/package.json b/package.json index 12abcc6..7319967 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@next2d/framework-typescript-template", "description": "Next2D Framework default TypeScript template.", - "version": "3.2.7", + "version": "4.0.0", "homepage": "https://next2d.app", "bugs": "https://github.com/Next2D/framework-typescript-template/issues/new", "author": "Toshiyuki Ienaga", diff --git a/template/.gitignore b/template/.gitignore index 5d64a45..0618bf9 100644 --- a/template/.gitignore +++ b/template/.gitignore @@ -25,4 +25,5 @@ dist-ssr src/Packages.ts src/config/Config.ts -electron/resources \ No newline at end of file +electron/resources +electron/package-lock.json \ No newline at end of file diff --git a/template/@types/README.md b/template/@types/README.md new file mode 100644 index 0000000..336642b --- /dev/null +++ b/template/@types/README.md @@ -0,0 +1,50 @@ +# TypeScript Type Definitions + +グローバルな型定義ファイルを格納するディレクトリです。 + +Directory for storing global TypeScript type definition files. + +## 役割 / Role + +このディレクトリには、TypeScriptコンパイラに認識させるグローバルな型定義を配置します。 + +This directory contains global type definitions to be recognized by the TypeScript compiler. + +## ファイル / Files + +### window.d.ts + +グローバルな `Window` インターフェースの拡張定義を行います。 + +Extends the global `Window` interface. + +```typescript +// 例: グローバル変数の型定義 +// Example: Type definition for global variables +declare global { + interface Window { + customProperty: string; + } +} +``` + +## 使用方法 / Usage + +1. このディレクトリに `.d.ts` ファイルを配置します +2. `tsconfig.json` の `include` または `typeRoots` に含まれていることを確認します + +1. Place `.d.ts` files in this directory +2. Ensure it's included in `tsconfig.json`'s `include` or `typeRoots` + +## 注意事項 / Notes + +- アプリケーション固有のインターフェースは `src/interface/` に配置してください +- このディレクトリはグローバルスコープの型拡張にのみ使用します + +- Place application-specific interfaces in `src/interface/` +- Use this directory only for global scope type extensions + +## 関連ドキュメント / Related Documentation + +- [interface/README.md](../src/interface/README.md) - アプリケーション固有のインターフェース +- [tsconfig.json](../tsconfig.json) - TypeScript設定 diff --git a/template/ARCHITECTURE.md b/template/ARCHITECTURE.md new file mode 100644 index 0000000..1fd5b06 --- /dev/null +++ b/template/ARCHITECTURE.md @@ -0,0 +1,402 @@ +# Clean Architecture & MVVM Implementation + +このプロジェクトは、クリーンアーキテクチャとMVVMパターンを組み合わせて実装されています。 + +This project implements a combination of Clean Architecture and MVVM pattern. + +## アーキテクチャの概要 / Architecture Overview + +```mermaid +graph TB + subgraph ViewLayer["🎨 View Layer"] + direction TB + ViewLayerPath["view/*, ui/*"] + View["View"] + ViewDesc["画面の構造を定義"] + ViewModel["ViewModel"] + VMDesc["Viewとビジネスロジックの橋渡し"] + UI["UI Components"] + UIDesc["再利用可能なUIパーツ"] + end + + subgraph InterfaceLayer["📋 Interface Layer"] + direction TB + InterfacePath["interface/*"] + IDraggable["IDraggable"] + ITextField["ITextField"] + IResponse["IHomeTextResponse"] + end + + subgraph ApplicationLayer["⚙️ Application Layer"] + direction TB + AppPath["model/application/*/usecase/*"] + UseCase["UseCase"] + UseCaseDesc["ビジネスロジックの実装"] + AppLogic["アプリケーション固有の処理"] + end + + subgraph DomainLayer["💎 Domain Layer"] + direction TB + DomainPath["model/domain/*"] + DomainLogic["コアビジネスロジック"] + DomainService["ドメインサービス"] + end + + subgraph InfraLayer["🔧 Infrastructure Layer"] + direction TB + InfraPath["model/infrastructure/repository/*"] + Repository["Repository"] + RepoDesc["データソースの抽象化"] + ExternalAPI["外部API・DBアクセス"] + end + + ViewLayer -.->|interface経由| InterfaceLayer + ViewLayer -.->|calls| ApplicationLayer + ApplicationLayer -.->|interface経由| InterfaceLayer + ApplicationLayer -.->|uses| DomainLayer + ApplicationLayer -.->|calls| InfraLayer + InfraLayer -.->|accesses| ExternalAPI + + classDef viewStyle fill:#e1f5ff,stroke:#01579b,stroke-width:2px,color:#000 + classDef interfaceStyle fill:#fff9c4,stroke:#f57f17,stroke-width:2px,color:#000 + classDef appStyle fill:#f3e5f5,stroke:#4a148c,stroke-width:2px,color:#000 + classDef domainStyle fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px,color:#000 + classDef infraStyle fill:#fce4ec,stroke:#880e4f,stroke-width:2px,color:#000 + + class ViewLayer,View,ViewModel,UI viewStyle + class InterfaceLayer,IDraggable,ITextField,IResponse interfaceStyle + class ApplicationLayer,UseCase,AppLogic appStyle + class DomainLayer,DomainLogic,DomainService domainStyle + class InfraLayer,Repository,ExternalAPI infraStyle +``` + +### レイヤー間の依存関係 / Layer Dependencies + +```mermaid +flowchart TD + View["🎨 View Layer
視覚表現"] + Interface["📋 Interface Layer
抽象化"] + App["⚙️ Application Layer
ユースケース"] + Domain["💎 Domain Layer
ビジネスルール"] + Infra["🔧 Infrastructure Layer
外部接続"] + + View -->|depends on| Interface + App -->|depends on| Interface + App -->|depends on| Domain + Infra -->|implements| Interface + UI["UI Components"] -->|implements| Interface + + style View fill:#e1f5ff,stroke:#01579b,stroke-width:3px + style Interface fill:#fff9c4,stroke:#f57f17,stroke-width:3px + style App fill:#f3e5f5,stroke:#4a148c,stroke-width:3px + style Domain fill:#e8f5e9,stroke:#1b5e20,stroke-width:3px + style Infra fill:#fce4ec,stroke:#880e4f,stroke-width:3px + style UI fill:#e1f5ff,stroke:#01579b,stroke-width:2px +``` + +### 依存関係の方向 / Dependency Direction + +クリーンアーキテクチャの原則に従い、依存関係は常に内側(Domain層)に向かい、外側の層は内側の層を知りません。 + +Following Clean Architecture principles, dependencies always point inward (toward the Domain layer), and outer layers don't know about inner layers. + +```mermaid +flowchart TD + View["🎨 View Layer"] + Interface["📋 Interface Layer"] + App["⚙️ Application Layer"] + Domain["💎 Domain Layer"] + Infra["🔧 Infrastructure Layer"] + + View -->|depends on| Interface + App -->|depends on| Interface + App -->|depends on| Domain + Infra -->|implements| Interface + + style View fill:#e1f5ff,stroke:#01579b,stroke-width:2px + style Interface fill:#fff9c4,stroke:#f57f17,stroke-width:2px + style App fill:#f3e5f5,stroke:#4a148c,stroke-width:2px + style Domain fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px + style Infra fill:#fce4ec,stroke:#880e4f,stroke-width:2px +``` + +- **View層**: インターフェースを通じてApplication層を使用 +- **Application層**: インターフェースを通じてDomain層とInfrastructure層を使用 +- **Domain層**: 何にも依存しない(純粋なビジネスロジック) +- **Infrastructure層**: Domain層のインターフェースを実装 + +### ファイル・ディレクトリ一覧 / File & Directory List + +``` +src/ +├── 📋 interface/ # インターフェース定義 +│ ├── IDraggable.ts # ドラッグ可能なオブジェクト +│ ├── ITextField.ts # テキストフィールド +│ ├── IHomeTextResponse.ts # API レスポンス型 +│ └── IViewName.ts # 画面名の型定義 +│ +├── 🎨 view/ # View & ViewModel +│ ├── home/ +│ │ ├── HomeView.ts # 画面の構造定義 +│ │ └── HomeViewModel.ts # ビジネスロジックとの橋渡し +│ └── top/ +│ ├── TopView.ts +│ └── TopViewModel.ts +│ +├── ⚙️ model/ +│ ├── application/ # アプリケーション層 +│ │ ├── home/ +│ │ │ └── usecase/ # ビジネスロジック実装 +│ │ │ ├── StartDragUseCase.ts +│ │ │ ├── StopDragUseCase.ts +│ │ │ └── CenterTextFieldUseCase.ts +│ │ └── top/ +│ │ └── usecase/ +│ │ └── NavigateToViewUseCase.ts +│ │ +│ ├── 💎 domain/ # ドメイン層 +│ │ └── callback/ # コアビジネスロジック +│ │ └── Background.ts +│ │ +│ └── 🔧 infrastructure/ # インフラ層 +│ └── repository/ +│ └── HomeTextRepository.ts # データアクセス +│ +└── 🎨 ui/ # UIコンポーネント + ├── component/ + │ ├── atom/ # 最小単位のコンポーネント + │ │ ├── ButtonAtom.ts + │ │ └── TextAtom.ts + │ └── molecule/ # Atomを組み合わせたコンポーネント + │ ├── HomeBtnMolecule.ts + │ └── TopBtnMolecule.ts + └── content/ # Animation Tool生成コンテンツ + ├── HomeContent.ts + └── TopContent.ts +``` + +## 主要な設計パターン / Key Design Patterns + +### 1. MVVM (Model-View-ViewModel) + +- **View**: 画面の構造と表示を担当。ビジネスロジックは持たない +- **ViewModel**: ViewとModelの橋渡し。UseCaseを保持し、イベントを処理 +- **Model**: ビジネスロジックとデータアクセスを担当 + +### 2. UseCase パターン + +各ユーザーアクションに対して、専用のUseCaseクラスを作成: + +```typescript +// 例: ドラッグ開始のユースケース +export class StartDragUseCase { + execute(target: IDraggable): void { + target.startDrag(); + } +} +``` + +### 3. Dependency Inversion (依存性の逆転) + +具象クラスではなく、インターフェースに依存: + +```typescript +// ❌ 悪い例: 具象クラスに依存 +import { HomeContent } from "@/ui/content/HomeContent"; +function startDrag(content: HomeContent) { ... } + +// ✅ 良い例: インターフェースに依存 +import type { IDraggable } from "@/interface/IDraggable"; +function startDrag(target: IDraggable) { ... } +``` + +### 4. Repository パターン + +データアクセスを抽象化し、エラーハンドリングも実装: + +```typescript +export class HomeTextRepository { + static async get(): Promise { + try { + const response = await fetch(...); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return await response.json(); + } catch (error) { + console.error("Failed to fetch:", error); + throw error; + } + } +} +``` + +## データフロー / Data Flow + +### 例: ドラッグ操作の場合 / Example: Drag Operation + +```mermaid +sequenceDiagram + actor User as 👤 User + participant View as View + participant VM as ViewModel + participant UC as UseCase + participant UI as UI Component + participant Content as Content + + User->>View: 1. Pointer Down + View->>VM: 2. event handler + activate VM + VM->>VM: 3. cast to IDraggable + VM->>UC: 4. execute() + activate UC + UC->>UI: 5. startDrag() + activate UI + UI->>Content: 6. content.startDrag() + activate Content + Content-->>Content: 7. Execute drag + Content-->>UI: 8. Complete + deactivate Content + UI-->>UC: 9. Complete + deactivate UI + UC-->>VM: 10. Complete + deactivate UC + VM-->>View: 11. Complete + deactivate VM + View-->>User: 12. Drag started + + Note over View,Content: Interface-based communication +``` + +### データ取得フロー / Data Fetch Flow + +```mermaid +sequenceDiagram + participant View as View + participant VM as ViewModel + participant UC as UseCase + participant Repo as Repository + participant API as External API + + View->>VM: 1. initialize() + activate VM + VM->>UC: 2. execute() + activate UC + UC->>Repo: 3. get() + activate Repo + Repo->>API: 4. fetch() + activate API + API-->>Repo: 5. JSON Response + deactivate API + Repo->>Repo: 6. Error check + Repo-->>UC: 7. IHomeTextResponse + deactivate Repo + UC->>UC: 8. Business logic + UC-->>VM: 9. Return data + deactivate UC + VM->>View: 10. Set data + deactivate VM + View->>View: 11. Update UI + + Note over Repo,API: Error handling & type safety +``` + +### 画面遷移フロー / View Navigation Flow + +```mermaid +flowchart TD + A["👤 User
clicks button"] + B["View
Button Event"] + C["ViewModel
onClickStartButton"] + D["UseCase
NavigateToViewUseCase"] + E{"ビジネス
ルール
チェック"} + F["app.gotoView
home"] + G["エラー処理"] + H["Framework
Routing"] + I["新しいView
ロード"] + J["ViewModel
initialize"] + K["View
initialize"] + L["View
onEnter"] + M["🎨
画面表示"] + + A --> B + B --> C + C --> D + D --> E + E -->|OK| F + E -->|NG| G + F --> H + H --> I + I --> J + J --> K + K --> L + L --> M + + style A fill:#e1f5ff,stroke:#01579b + style E fill:#fff9c4,stroke:#f57f17 + style F fill:#e8f5e9,stroke:#1b5e20 + style G fill:#ffebee,stroke:#c62828 + style M fill:#e1f5ff,stroke:#01579b +``` + +### コード例 / Code Example + +```typescript +// 1. View: イベントハンドリング +homeContent.addEventListener(PointerEvent.POINTER_DOWN, + this.vm.homeContentPointerDownEvent +); + +// 2. ViewModel: UseCaseの実行 +homeContentPointerDownEvent(event: PointerEvent): void { + const target = event.currentTarget as unknown as IDraggable; + this.startDragUseCase.execute(target); +} + +// 3. UseCase: ビジネスロジック +execute(target: IDraggable): void { + // ビジネスルール: ドラッグ可能かチェック + target.startDrag(); +} + +// 4. UI Component: 実装 +export class HomeBtnMolecule implements IDraggable { + startDrag(): void { + this.homeContent.startDrag(); + } +} +``` + +## テスタビリティ / Testability + +インターフェースと依存性注入により、各層を独立してテスト可能: + +```typescript +// UseCaseのテスト例 +test('StartDragUseCase should call startDrag', () => { + const mockDraggable: IDraggable = { + startDrag: jest.fn(), + stopDrag: jest.fn() + }; + + const useCase = new StartDragUseCase(); + useCase.execute(mockDraggable); + + expect(mockDraggable.startDrag).toHaveBeenCalled(); +}); +``` + +## ベストプラクティス / Best Practices + +1. **インターフェース優先**: 具象型ではなく、常にインターフェースに依存 +2. **単一責任の原則**: 各クラスは1つの責務のみを持つ +3. **依存性注入**: コンストラクタで依存を注入(将来的にDIコンテナも検討可能) +4. **エラーハンドリング**: Repository層で適切にエラーを処理 +5. **型安全性**: `any`型を避け、明示的な型定義を使用 + +## 今後の改善案 / Future Improvements + +1. **DIコンテナの導入**: UseCaseのインスタンス管理を自動化 +2. **State管理の追加**: 複雑な状態管理が必要な場合 +3. **Presenter層の追加**: ViewModelの責務をさらに分離 +4. **E2Eテストの追加**: 実際のユーザーフローをテスト diff --git a/template/README.md b/template/README.md index ed097ca..fac6a0c 100644 --- a/template/README.md +++ b/template/README.md @@ -1,48 +1,287 @@ -# Getting Started with Create Next2D App +# Next2D Framework TypeScript Template + +[Create Next2D App](https://github.com/Next2D/create-next2d-app) でブートストラップされたプロジェクトです。 This project was bootstrapped with [Create Next2D App](https://github.com/Next2D/create-next2d-app). -## Available Scripts +--- + +## 目次 / Table of Contents + +- [必要な環境 / Requirements](#必要な環境--requirements) +- [セットアップ / Setup](#セットアップ--setup) +- [アーキテクチャ / Architecture](#アーキテクチャ--architecture) +- [開発サーバー / Development Server](#開発サーバー--development-server) +- [コード生成 / Code Generation](#コード生成--code-generation) +- [プラットフォームエミュレーター / Platform Emulators](#プラットフォームエミュレーター--platform-emulators) +- [ユニットテスト / Unit Test](#ユニットテスト--unit-test) +- [ビルド / Build](#ビルド--build) +- [ディレクトリ構成 / Directory Structure](#ディレクトリ構成--directory-structure) +- [📚 詳細ドキュメント / Detailed Documentation](#-詳細ドキュメント--detailed-documentation) +- [ライセンス / License](#ライセンス--license) + +--- + +## 必要な環境 / Requirements + +| ツール / Tool | バージョン / Version | +|--------------|---------------------| +| Node.js | 22.x 以上 / 22.x or higher | +| npm | 10.x 以上 / 10.x or higher | + +### オプション / Optional + +iOS/Androidビルドを行う場合は、以下も必要です。 + +For iOS/Android builds, the following are also required: + +- **iOS**: Xcode 14 以上、macOS +- **Android**: Android Studio、JDK 21 以上 + +--- + +## セットアップ / Setup + +### 1. リポジトリのクローン / Clone the repository + +```bash +git clone +cd +``` + +### 2. 依存パッケージのインストール / Install dependencies + +```bash +npm install +``` + +### 3. 開発サーバーの起動 / Start the development server + +```bash +npm start +``` + +ブラウザで [http://localhost:5173](http://localhost:5173) を開いてください。 -In the project directory, you can run: +Open [http://localhost:5173](http://localhost:5173) in your browser. + +--- + +## アーキテクチャ / Architecture + +このプロジェクトは **MVVM + Clean Architecture + Atomic Design** を採用しています。 + +This project adopts **MVVM + Clean Architecture + Atomic Design**. + +```mermaid +flowchart TB + subgraph view["🎨 View Layer"] + view_path["view/, ui/"] + view_desc["View・ViewModel・UI Components"] + end + + subgraph interface["📋 Interface Layer"] + interface_path["interface/"] + interface_desc["型定義・インターフェース"] + end + + subgraph application["⚙️ Application Layer"] + app_path["model/application/"] + app_desc["UseCase: ビジネスロジック"] + end + + subgraph domain["💎 Domain Layer"] + domain_path["model/domain/"] + domain_desc["コアビジネスルール"] + end + + subgraph infrastructure["🔧 Infrastructure Layer"] + infra_path["model/infrastructure/"] + infra_desc["Repository: データアクセス"] + end + + view --> interface + interface --> application + application --> domain + application --> infrastructure + + style view fill:#e3f2fd,stroke:#1565c0 + style interface fill:#fff9c4,stroke:#f9a825 + style application fill:#f3e5f5,stroke:#7b1fa2 + style domain fill:#e8f5e9,stroke:#2e7d32 + style infrastructure fill:#fce4ec,stroke:#c2185b +``` + +詳細は [ARCHITECTURE.md](./ARCHITECTURE.md) を参照してください。 + +See [ARCHITECTURE.md](./ARCHITECTURE.md) for details. + +--- + +## 開発サーバー / Development Server ### `npm start` -Runs the app in the development mode. -Open [http://localhost:5173](http://localhost:5173) to view it in your browser. -The page will reload when you make changes. +開発モードでアプリケーションを起動します。 +[http://localhost:5173](http://localhost:5173) をブラウザで開いてください。 +コードを変更すると自動的にリロードされます。 -## Start the emulator for each platform. +Runs the app in development mode. +Open [http://localhost:5173](http://localhost:5173) to view it in your browser. +The page will reload when you make changes. -### `npm run preview:windows -- --env prd` -### `npm run preview:macos -- --env prd` -### `npm run preview:linux -- --env prd` -### `npm run preview:ios -- --env prd` -### `npm run preview:android -- --env prd` +--- -Launch emulators for various platforms including Windows, macOS, Linux, iOS, Android, and Web (HTML). -You can check the operation of the application in the environment specified by env=***. +## コード生成 / Code Generation ### `npm run generate` -Generate the necessary View and ViewModel classes from the routing JSON file. +`routing.json` の設定に基づいて、必要な View と ViewModel クラスを自動生成します。 +新しい画面を追加する際に便利です。 + +Generates the necessary View and ViewModel classes from the `routing.json` file. +Useful when adding new screens. -## Unit Test +--- + +## プラットフォームエミュレーター / Platform Emulators + +各プラットフォーム向けのエミュレーターを起動します。 +`--env` オプションで環境を指定できます(`dev`, `stg`, `prd` など)。 + +Launch emulators for each platform. +You can specify the environment with the `--env` option (`dev`, `stg`, `prd`, etc.). + +| コマンド / Command | プラットフォーム / Platform | +|-------------------|---------------------------| +| `npm run preview:windows -- --env prd` | Windows | +| `npm run preview:macos -- --env prd` | macOS | +| `npm run preview:linux -- --env prd` | Linux | +| `npm run preview:ios -- --env prd` | iOS | +| `npm run preview:android -- --env prd` | Android | + +--- + +## ユニットテスト / Unit Test ### `npm test` -Launches the test runner. +Vitest を使用してテストを実行します。 + +Runs tests using Vitest. + +```bash +# 全テスト実行 / Run all tests +npm test + +# ウォッチモード / Watch mode +npm test -- --watch + +# カバレッジレポート / Coverage report +npm test -- --coverage +``` + +--- + +## ビルド / Build + +各プラットフォーム向けにビルドを行います。 +`--env` オプションで環境を指定できます。 + +Build for each platform. +You can specify the environment with the `--env` option. + +| コマンド / Command | プラットフォーム / Platform | 出力先 / Output | +|-------------------|---------------------------|----------------| +| `npm run build:web -- --env prd` | Web (HTML) | `dist/web/prd/` | +| `npm run build:steam:windows -- --env prd` | Windows (Steam) | `dist/steam/windows/` | +| `npm run build:steam:macos -- --env prd` | macOS (Steam) | `dist/steam/macos/` | +| `npm run build:steam:linux -- --env prd` | Linux (Steam) | `dist/steam/linux/` | +| `npm run build:ios -- --env prd` | iOS | Xcode project | +| `npm run build:android -- --env prd` | Android | Android Studio project | + +### 環境設定 / Environment Configuration + +環境ごとの設定は `src/config/` ディレクトリで管理されています。 + +Environment-specific settings are managed in the `src/config/` directory. + +--- + +## ディレクトリ構成 / Directory Structure + +``` +src/ +├── config/ # 設定ファイル / Configuration files +├── interface/ # インターフェース定義 / Interface definitions +├── model/ +│ ├── application/ # ユースケース / Use cases +│ ├── domain/ # ドメインロジック / Domain logic +│ └── infrastructure/ # リポジトリ / Repositories +├── ui/ +│ ├── animation/ # アニメーション定義 / Animation definitions +│ ├── component/ +│ │ ├── atom/ # 最小コンポーネント / Smallest components +│ │ └── molecule/ # 複合コンポーネント / Composite components +│ └── content/ # Animation Tool 生成 / Generated content +└── view/ # View & ViewModel +``` + +各ディレクトリの詳細は、ディレクトリ内の `README.md` を参照してください。 + +See the `README.md` in each directory for details. + +--- + +## 📚 詳細ドキュメント / Detailed Documentation + +各ディレクトリには、実装ガイドとなるREADME.mdが配置されています。AIエージェントやコード生成ツールは、これらのドキュメントを参照することで、アーキテクチャに沿った実装が可能です。 + +Each directory contains a README.md that serves as an implementation guide. AI agents and code generation tools can reference these documents to implement code that follows the architecture. + +### アーキテクチャ層 / Architecture Layers + +| ドキュメント / Document | 説明 / Description | +|------------------------|-------------------| +| [src/model/README.md](./src/model/README.md) | Model層全体の概要、3層構造の説明 | +| [src/model/application/README.md](./src/model/application/README.md) | Application層:UseCaseパターンの実装ガイド | +| [src/model/domain/README.md](./src/model/domain/README.md) | Domain層:コアビジネスロジックの実装ガイド | +| [src/model/infrastructure/README.md](./src/model/infrastructure/README.md) | Infrastructure層:Repositoryパターンの実装ガイド | + +### UIコンポーネント / UI Components + +| ドキュメント / Document | 説明 / Description | +|------------------------|-------------------| +| [src/ui/README.md](./src/ui/README.md) | UI層全体の概要、アトミックデザイン階層 | +| [src/ui/component/README.md](./src/ui/component/README.md) | Atom/Molecule/Pageコンポーネントの実装ガイド | +| [src/ui/content/README.md](./src/ui/content/README.md) | Animation Toolコンテンツの実装ガイド | +| [src/ui/animation/README.md](./src/ui/animation/README.md) | アニメーション定義の実装ガイド | + +### View/ViewModel & 設定 / View/ViewModel & Configuration + +| ドキュメント / Document | 説明 / Description | +|------------------------|-------------------| +| [src/view/README.md](./src/view/README.md) | View/ViewModelのMVVMパターン実装ガイド | +| [src/config/README.md](./src/config/README.md) | 設定ファイル(stage.json, config.json, routing.json)の詳細 | +| [src/interface/README.md](./src/interface/README.md) | インターフェース定義と型安全性のガイド | + +### 静的アセット / Static Assets + +| ドキュメント / Document | 説明 / Description | +|------------------------|-------------------| +| [src/assets/README.md](./src/assets/README.md) | 画像・JSONなど静的ファイルの管理ガイド | + +--- + +## ライセンス / License -## Build +MIT License -### `npm run build:web` -### `npm run build:steam:windows` -### `npm run build:steam:macos` -### `npm run build:steam:linux` -### `npm run build -- --platform *** --env***` +--- -Multi-platform builder, writes to various platforms including macOS, Windows, iOS, Android, and Web (HTML). -Builds apps for the environment specified by env=***. +## 関連リンク / Related Links -### Flowchart -![Flowchart](https://raw.githubusercontent.com/Next2D/framework/main/Framework_Flowchart.svg) \ No newline at end of file +- [Next2D Player](https://github.com/Next2D/player) - レンダリングエンジン / Rendering engine +- [Next2D Framework](https://github.com/Next2D/framework) - フレームワーク / Framework +- [Create Next2D App](https://github.com/Next2D/create-next2d-app) - プロジェクト生成ツール / Project generator +- [Next2D Animation Tool](https://tool.next2d.app/) - アニメーション作成ツール / Animation creation tool \ No newline at end of file diff --git a/template/electron/README.md b/template/electron/README.md new file mode 100644 index 0000000..ff659ff --- /dev/null +++ b/template/electron/README.md @@ -0,0 +1,79 @@ +# Electron Configuration + +Windows、macOS、Linux向けのデスクトップアプリケーションビルドに使用するElectron設定ディレクトリです。 + +Directory for Electron configuration used to build desktop applications for Windows, macOS, and Linux. + +## ディレクトリ構造 / Directory Structure + +``` +electron/ +├── icons/ # アプリケーションアイコン +├── index.js # メインプロセス +└── package.json # 依存関係 +``` + +## ファイル説明 / File Description + +### index.js + +Electronのメインプロセスを定義します。ウィンドウの作成、アプリケーションのライフサイクル管理などを行います。 + +Defines the Electron main process. Handles window creation, application lifecycle management, etc. + +### package.json + +Electron用の依存関係と設定を管理します。 + +Manages Electron-specific dependencies and configuration. + +### icons/ + +各プラットフォーム向けのアプリケーションアイコンを格納します。詳細は [icons/README.md](./icons/README.md) を参照してください。 + +Contains application icons for each platform. See [icons/README.md](./icons/README.md) for details. + +## ビルドコマンド / Build Commands + +```bash +# Windows向けビルド / Build for Windows +npm run build:steam:windows -- --env prd + +# macOS向けビルド / Build for macOS +npm run build:steam:macos -- --env prd + +# Linux向けビルド / Build for Linux +npm run build:steam:linux -- --env prd +``` + +## エミュレーター / Emulator + +開発中にデスクトップアプリとしてプレビューできます。 + +Preview as a desktop application during development. + +```bash +# Windows / macOS / Linux エミュレーター +npm run preview:windows -- --env dev +npm run preview:macos -- --env dev +npm run preview:linux -- --env dev +``` + +## カスタマイズ / Customization + +### ウィンドウ設定 / Window Settings + +`index.js` でウィンドウのサイズや設定をカスタマイズできます。 + +Customize window size and settings in `index.js`. + +### アイコン変更 / Icon Change + +`icons/` ディレクトリ内の画像を置き換えてアプリケーションアイコンを変更します。 + +Replace images in the `icons/` directory to change application icons. + +## 関連ドキュメント / Related Documentation + +- [README.md](../README.md) - プロジェクト全体の説明 +- [icons/README.md](./icons/README.md) - アイコン設定 diff --git a/template/mock/api/README.md b/template/mock/api/README.md new file mode 100644 index 0000000..fbeab51 --- /dev/null +++ b/template/mock/api/README.md @@ -0,0 +1,119 @@ +# Mock API + +ローカル開発用のモックAPIデータを格納するディレクトリです。 + +Directory for storing mock API data for local development. + +## 概要 / Overview + +開発環境でAPIレスポンスをシミュレートするためのJSONファイルを配置します。`http://localhost:5173/api/` でアクセス可能です。 + +Place JSON files to simulate API responses in the development environment. Accessible at `http://localhost:5173/api/`. + +## ファイル一覧 / File List + +### home.json + +Home画面用のモックAPIレスポンスです。 + +Mock API response for the Home screen. + +```json +{ + "word": "Hello, World." +} +``` + +**アクセスURL:** `http://localhost:5173/api/home.json` + +### top.json + +Top画面用のモックAPIレスポンスです。 + +Mock API response for the Top screen. + +```json +{ + "title": "Click Me." +} +``` + +**アクセスURL:** `http://localhost:5173/api/top.json` + +## 使用方法 / Usage + +### 1. JSONファイルの作成 + +APIレスポンスの構造に合わせたJSONファイルを作成します。 + +Create JSON files that match the API response structure. + +### 2. configでエンドポイントを設定 + +`src/config/config.json` の `local` 環境でモックサーバーを指定します。 + +Specify the mock server in the `local` environment of `src/config/config.json`. + +```json +{ + "local": { + "api": { + "endPoint": "http://localhost:5173/" + } + } +} +``` + +### 3. Repositoryからアクセス + +Repositoryクラスでモックデータにアクセスします。 + +Access mock data from Repository classes. + +```typescript +const response = await fetch(`${config.api.endPoint}api/home.json`); +const data = await response.json(); +``` + +## モックデータの追加 / Adding Mock Data + +### 手順 / Steps + +1. 対応するインターフェースを確認(`src/interface/`) +2. インターフェースに合わせたJSONファイルを作成 +3. このディレクトリに配置 +4. Repositoryからアクセスをテスト + +### 例 / Example + +```typescript +// src/interface/IUserResponse.ts +export interface IUserResponse { + id: string; + name: string; + email: string; +} + +// mock/api/user.json +{ + "id": "user-001", + "name": "Test User", + "email": "test@example.com" +} +``` + +## 注意事項 / Notes + +- モックデータは開発環境でのみ使用してください +- 本番環境では実際のAPIエンドポイントを設定してください +- `routing.json` のパス設定と重複しないように注意してください + +- Use mock data only in development environments +- Set actual API endpoints in production environments +- Be careful not to conflict with path settings in `routing.json` + +## 関連ドキュメント / Related Documentation + +- [../README.md](../README.md) - Mockディレクトリの説明 +- [../../src/config/README.md](../../src/config/README.md) - 環境設定 +- [../../src/model/infrastructure/README.md](../../src/model/infrastructure/README.md) - Repository層 diff --git a/template/mock/content/README.md b/template/mock/content/README.md new file mode 100644 index 0000000..df294ea --- /dev/null +++ b/template/mock/content/README.md @@ -0,0 +1,119 @@ +# Mock Content + +ローカル開発用のモックコンテンツデータを格納するディレクトリです。 + +Directory for storing mock content data for local development. + +## 概要 / Overview + +Next2D Animation Toolからエクスポートされたコンテンツのモックデータを配置します。`http://localhost:5173/content/` でアクセス可能です。 + +Place mock data for content exported from the Next2D Animation Tool. Accessible at `http://localhost:5173/content/`. + +## ファイル一覧 / File List + +### sample.json + +Next2D Animation Toolから書き出されたJSONファイルです。Next2D Frameworkの`RequestContentRepository`によって読み込まれ、シンボル(MovieClip等)がLoaderInfoMapに登録されます。 + +JSON file exported from the Next2D Animation Tool. It is loaded by `RequestContentRepository` in the Next2D Framework, and symbols (MovieClip, etc.) are registered in the LoaderInfoMap. + +**アクセスURL:** `http://localhost:5173/content/sample.json` + +## 読み込みの仕組み / Loading Mechanism + +```mermaid +sequenceDiagram + participant Config as Config
(routing設定) + participant Framework as Next2D Framework + participant Repo as RequestContentRepository + participant Loader as Loader + participant Map as LoaderInfoMap + + Config->>Framework: 1. content request
(type: "content") + Framework->>Repo: 2. execute() + Repo->>Loader: 3. loader.load(urlRequest) + Loader->>Loader: 4. JSONをパース + Loader-->>Repo: 5. content + Repo->>Map: 6. シンボルを登録
loaderInfoMap.set() + Repo-->>Framework: 7. response +``` + +### Framework側の処理 + +Frameworkの[RequestContentRepository](https://github.com/Next2D/framework/blob/develop/src/infrastructure/Request/repository/RequestContentRepository.ts)で以下の処理が行われます: + +The following processing is performed in the Framework's [RequestContentRepository](https://github.com/Next2D/framework/blob/develop/src/infrastructure/Request/repository/RequestContentRepository.ts): + +1. **JSONの読み込み**: `Loader`クラスでJSONを非同期取得 +2. **シンボル登録**: Animation Toolで設定したシンボル名を`LoaderInfoMap`に登録 +3. **コンテンツ返却**: `ui/content/`のクラスから参照可能に + +## 設定例 / Configuration Example + +`src/config/Config.ts`での設定例: + +```typescript +{ + "routing": { + "@sample": { + "requests": [ + { + "type": "content", + "path": "https://example.com/content/sample.json", + "name": "MainContent", + "cache": true + } + ] + } + } +} +``` + +## 使用方法 / Usage + +### 1. Animation Toolでコンテンツ作成 + +[Next2D Animation Tool](https://tool.next2d.app/)でアニメーションを作成し、JSONとしてエクスポートします。 + +Create animations in [Next2D Animation Tool](https://tool.next2d.app/) and export as JSON. + +### 2. モックディレクトリに配置 + +エクスポートしたJSONをこのディレクトリに配置します。 + +Place the exported JSON in this directory. + +### 3. Configでパスを設定 + +開発環境ではローカルパス、本番環境では実サーバーのパスを設定します。 + +Set local path for development and actual server path for production. + +## モックコンテンツの追加 / Adding Mock Content + +### 手順 / Steps + +1. Animation ToolでMovieClip等を作成 +2. JSONとしてエクスポート +3. このディレクトリに配置 +4. `src/ui/content/`にコンテンツクラスを作成 +5. `Config.ts`のroutingに設定を追加 + +## 注意事項 / Notes + +- モックコンテンツは開発環境でのみ使用してください +- 本番環境では実際のコンテンツサーバーを設定してください +- `cache: true`を設定すると、同じコンテンツの再読み込みを防げます + +- Use mock content only in development environments +- Set actual content server in production environments +- Setting `cache: true` prevents reloading the same content + +## 関連ドキュメント / Related Documentation + +- [../README.md](../README.md) - Mockディレクトリの説明 / Mock directory overview +- [../../file/README.md](../../file/README.md) - n2dファイルの格納場所 / n2d file storage +- [../../src/ui/content/README.md](../../src/ui/content/README.md) - Animation Toolコンテンツクラス / Content classes +- [Next2D Animation Tool](https://tool.next2d.app/) - アニメーション作成ツール / Animation creation tool +- [RequestContentRepository (GitHub)](https://github.com/Next2D/framework/blob/develop/src/infrastructure/Request/repository/RequestContentRepository.ts) - Framework側の読み込み処理 / Framework loading process diff --git a/template/mock/img/README.md b/template/mock/img/README.md new file mode 100644 index 0000000..2a3850c --- /dev/null +++ b/template/mock/img/README.md @@ -0,0 +1,78 @@ +# Mock Images + +ローカル開発用のモック画像を格納するディレクトリです。 + +Directory for storing mock images for local development. + +## 概要 / Overview + +開発環境で使用する画像ファイル(アイコン、サムネイル、背景など)を配置します。`http://localhost:5173/img/` でアクセス可能です。 + +Place image files (icons, thumbnails, backgrounds, etc.) used in the development environment. Accessible at `http://localhost:5173/img/`. + +## ファイル一覧 / File List + +### favicon.png + +アプリケーションのファビコンです。 + +Application favicon. + +**アクセスURL:** `http://localhost:5173/img/favicon.png` + +## 使用方法 / Usage + +### 1. 画像の配置 + +開発に必要な画像をこのディレクトリに配置します。 + +Place images needed for development in this directory. + +### 2. アクセス + +開発サーバー経由で画像にアクセスします。 + +Access images through the development server. + +```typescript +// 画像URLの例 +const imageUrl = "http://localhost:5173/img/favicon.png"; +``` + +```html + + +``` + +## モック画像の追加 / Adding Mock Images + +### 手順 / Steps + +1. 画像ファイルを準備(PNG, JPG, SVG, WebP など) +2. このディレクトリに配置 +3. 開発サーバーからアクセスをテスト + +### 対応フォーマット / Supported Formats + +- PNG - 透過が必要な画像に推奨 +- JPG/JPEG - 写真などに推奨 +- SVG - アイコンやベクター画像に推奨 +- WebP - 軽量な画像に推奨 +- GIF - アニメーション画像に使用 + +## 注意事項 / Notes + +- モック画像は開発環境でのみ使用してください +- 本番環境では適切なCDNや画像サーバーを設定してください +- 大きなファイルは開発サーバーのパフォーマンスに影響する可能性があります +- `routing.json` のパス設定と重複しないように注意してください + +- Use mock images only in development environments +- Set up appropriate CDN or image server for production environments +- Large files may affect development server performance +- Be careful not to conflict with path settings in `routing.json` + +## 関連ドキュメント / Related Documentation + +- [../README.md](../README.md) - Mockディレクトリの説明 +- [../../src/assets/README.md](../../src/assets/README.md) - インライン画像 diff --git a/template/package.json b/template/package.json index 01c67d3..399b10c 100644 --- a/template/package.json +++ b/template/package.json @@ -10,34 +10,38 @@ "preview:macos": "npx @next2d/builder --platform macos --preview", "preview:windows": "npx @next2d/builder --platform windows --preview", "preview:linux": "npx @next2d/builder --platform linux --preview", - "build:steam:windows": "npx @next2d/builder --platform steam:windows --env prd", - "build:steam:macos": "npx @next2d/builder --platform steam:macos --env prd", - "build:steam:linux": "npx @next2d/builder --platform steam:linux --env prd", - "build:web": "npx @next2d/builder --platform web --env prd", + "open:ios": "npx @next2d/builder --platform ios --open", + "open:android": "npx @next2d/builder --platform android --open", + "build:steam:windows": "npx @next2d/builder --platform steam:windows", + "build:steam:macos": "npx @next2d/builder --platform steam:macos", + "build:steam:linux": "npx @next2d/builder --platform steam:linux", + "build:web": "npx @next2d/builder --platform web", + "build:ios": "npx @next2d/builder --platform ios --build", + "build:android": "npx @next2d/builder --platform android --build", "build": "npx @next2d/builder", "test": "npx vitest", "generate": "npx @next2d/view-generator" }, "devDependencies": { - "@capacitor/android": "^7.4.4", - "@capacitor/cli": "^7.4.4", - "@capacitor/core": "^7.4.4", - "@capacitor/ios": "^7.4.4", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "^9.39.0", - "@next2d/vite-plugin-next2d-auto-loader": "^3.1.9", - "@types/node": "^24.9.2", - "@typescript-eslint/eslint-plugin": "^8.46.2", - "@typescript-eslint/parser": "^8.46.2", - "@vitest/web-worker": "^4.0.6", - "eslint": "^9.39.0", + "@capacitor/android": "^8.0.2", + "@capacitor/cli": "^8.0.2", + "@capacitor/core": "^8.0.2", + "@capacitor/ios": "^8.0.2", + "@eslint/eslintrc": "^3.3.3", + "@eslint/js": "^9.39.2", + "@next2d/vite-plugin-next2d-auto-loader": "^3.1.12", + "@types/node": "^25.2.0", + "@typescript-eslint/eslint-plugin": "^8.54.0", + "@typescript-eslint/parser": "^8.54.0", + "@vitest/web-worker": "^4.0.18", + "eslint": "^9.39.2", "eslint-plugin-unused-imports": "^4.3.0", - "globals": "^16.4.0", - "jsdom": "^27.1.0", + "globals": "^17.3.0", + "jsdom": "^28.0.0", "typescript": "^5.9.3", - "vite": "^7.1.12", - "vite-tsconfig-paths": "^5.1.4", - "vitest": "^4.0.6", + "vite": "^7.3.1", + "vite-tsconfig-paths": "^6.0.5", + "vitest": "^4.0.18", "vitest-webgl-canvas-mock": "^1.1.0" }, "peerDependencies": { diff --git a/template/specs/commands.md b/template/specs/commands.md new file mode 100644 index 0000000..33934e6 --- /dev/null +++ b/template/specs/commands.md @@ -0,0 +1,49 @@ +# CLI Commands Reference + +## Setup + +```bash +npm install # 依存パッケージのインストール +``` + +## Development + +```bash +npm start # 開発サーバー起動 (http://localhost:5173) +npm run generate # routing.jsonからView/ViewModelクラスを自動生成 +``` + +## Testing + +```bash +npm test # 全テスト実行 (Vitest) +npm test -- --watch # ウォッチモード +npm test -- --coverage # カバレッジレポート +``` + +## Build + +| Command | Platform | Output | +|---------|----------|--------| +| `npm run build:web -- --env prd` | Web (HTML) | `dist/web/prd/` | +| `npm run build:steam:windows -- --env prd` | Windows (Steam) | `dist/steam/windows/` | +| `npm run build:steam:macos -- --env prd` | macOS (Steam) | `dist/steam/macos/` | +| `npm run build:steam:linux -- --env prd` | Linux (Steam) | `dist/steam/linux/` | +| `npm run build:ios -- --env prd` | iOS | Xcode project | +| `npm run build:android -- --env prd` | Android | Android Studio project | + +## Platform Emulators + +```bash +npm run preview:windows -- --env prd # Windows +npm run preview:macos -- --env prd # macOS +npm run preview:linux -- --env prd # Linux +npm run preview:ios -- --env prd # iOS +npm run preview:android -- --env prd # Android +``` + +`--env` オプション: `local`, `dev`, `stg`, `prd` + +## Environment Configuration + +環境ごとの設定は`src/config/config.json`で管理。`--env`で指定した環境名の設定値と`all`の設定値がマージされる。 diff --git a/template/specs/config.md b/template/specs/config.md new file mode 100644 index 0000000..72b4e5a --- /dev/null +++ b/template/specs/config.md @@ -0,0 +1,198 @@ +# Configuration Files + +設定ファイルは `src/config/` ディレクトリに配置。 + +## stage.json + +表示領域(Stage)の設定。 + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `width` | number | 240 | 表示領域の幅 | +| `height` | number | 240 | 表示領域の高さ | +| `fps` | number | 60 | 描画回数/秒 (1-60) | +| `options` | object | null | オプション設定 | + +### Stage Options + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `options.fullScreen` | boolean | false | 画面全体に描画 | +| `options.tagId` | string | null | 描画先のエレメントID | +| `options.bgColor` | string | "transparent" | 背景色 (16進数) | + +### Example + +```json +{ + "width": 240, + "height": 240, + "fps": 60, + "options": { + "fullScreen": true + } +} +``` + +--- + +## config.json + +環境別の設定ファイル。`local`, `dev`, `stg`, `prd`, `all` に分離。 + +### Structure + +```json +{ + "local": { "api": { "endPoint": "/" }, "content": { "endPoint": "/" } }, + "dev": { "api": { "endPoint": "/" }, "content": { "endPoint": "/" } }, + "stg": { "api": { "endPoint": "/" }, "content": { "endPoint": "/" } }, + "prd": { "api": { "endPoint": "https://..." }, "content": { "endPoint": "https://..." } }, + "all": { /* 全環境共通 */ } +} +``` + +### `all` Properties (全環境共通) + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `defaultTop` | string | "top" | ページトップのView名 | +| `spa` | boolean | true | SPA (URLでシーン制御) | +| `loading.callback` | string | "Loading" | ローディング画面のコールバッククラス。start/end関数が呼ばれる | +| `gotoView.callback` | string/array | ["callback.Background"] | 画面遷移完了後のコールバッククラス。execute関数がasync/awaitで呼ばれる | + +### `platform` Property + +ビルド時の`--platform`値がセットされる。値: `macos`, `windows`, `linux`, `ios`, `android`, `web` + +### Config Access in Code + +```typescript +import { config } from "@/config/Config"; + +const endpoint = config.api.endPoint; +const stageWidth = config.stage.width; +``` + +--- + +## routing.json + +ルーティング設定。トッププロパティは英数字・スラッシュ。スラッシュをキーにCamelCaseでViewクラスにアクセス。 + +### Routing Example + +```json +{ + "quest/list": { + "requests": [] + } +} +``` + +→ `https://example.com/quest/list` でアクセス可能。`QuestListView`クラスがセットされる。 + +### Cluster Pattern (共通リクエストの再利用) + +`@`プレフィックスで共通リクエスト群を定義し、他のルートから参照: + +```json +{ + "@sample": { + "requests": [ + { + "type": "content", + "path": "{{ content.endPoint }}content/sample.json", + "name": "MainContent", + "cache": true + } + ] + }, + "top": { + "requests": [ + { "type": "cluster", "path": "@sample" }, + { "type": "json", "path": "{{ api.endPoint }}api/top.json", "name": "TopText" } + ] + } +} +``` + +### Second Level Properties + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `private` | boolean | false | true時、URLアクセスするとTopViewが読み込まれる | +| `requests` | array | null | Viewバインド前に実行するリクエスト群 | + +### Request Properties + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `type` | string | "content" | `json`, `content`, `custom`, `cluster` | +| `path` | string | "" | `{{***}}`でconfig変数を参照可能。`@`プレフィックスでcluster参照 | +| `name` | string | "" | responseのキー名。`app.getResponse().get("key")` | +| `cache` | boolean | false | キャッシュ有効。`app.getCache().get("key")` | +| `callback` | string/array | null | リクエスト完了後のコールバッククラス。execute関数が呼ばれる | +| `class` | string | "" | custom type時のリクエスト実行クラス | +| `access` | string | "public" | custom type時の関数アクセス (`public`/`static`) | +| `method` | string | "" | custom type時の関数名 | + +### Request Types + +- **`json`**: URLからJSONを取得 +- **`content`**: Animation Toolコンテンツを取得 +- **`custom`**: 指定クラスのメソッドを実行 +- **`cluster`**: `@`プレフィックスの共通リクエスト群を参照 + +### Data Access + +```typescript +// responseデータ (画面遷移で初期化される) +const data = app.getResponse().get("HomeText"); + +// cacheデータ (画面遷移しても保持される) +const cached = app.getCache().get("MainContent"); +``` + +--- + +## Static Files + +### mock/ Directory + +ローカル開発用モックデータ。`http://localhost:5173/***`でアクセス可能。`routing.json`のパスと重複しないよう注意。 + +``` +mock/ +├── api/ # APIモック (JSON) +├── content/ # Animation Toolコンテンツモック +└── img/ # 画像モック +``` + +### file/ Directory + +Animation Toolで作成した`.n2d`ファイルを格納。バージョン管理可能。 + +### assets/ Directory + +ビルド時にバンドルに含める静的アセット。 + +```typescript +// 画像インポート +import logoImage from "@/assets/logo.png?inline"; + +// JSONインポート +import animation from "@/assets/animation.json"; +``` + +| 項目 | assets | mock | +|------|--------|------| +| 用途 | バンドルに含める | 開発サーバーで配信 | +| アクセス | importで取得 | URL経由でfetch | +| ビルド | バンドルに含まれる | 含まれない | + +--- + +## @types/ Directory + +グローバルな型定義ファイル (.d.ts)。`Window`インターフェースの拡張等。アプリケーション固有のインターフェースは`src/interface/`に配置。 diff --git a/template/specs/interface.md b/template/specs/interface.md new file mode 100644 index 0000000..c64db36 --- /dev/null +++ b/template/specs/interface.md @@ -0,0 +1,133 @@ +# Interface Definitions + +TypeScriptインターフェース定義。Clean Architecture原則に従い、各層の依存関係を抽象化。 + +## Rules + +- 命名規則: `I` プレフィックスを使用 (例: `IDraggable`, `ITextField`) +- 必要最小限のプロパティのみ定義 +- `any`型を禁止、常に明示的な型を使用 +- JSDocコメントを追加 + +## Interface Categories + +### 1. UI関連 (コンポーネントの振る舞い) + +```typescript +// IDraggable.ts - ドラッグ可能なオブジェクト +export interface IDraggable { + startDrag(): void; + stopDrag(): void; +} +// 使用: HomeBtnMolecule, HomeContent + +// ITextField.ts - テキストフィールドの基本プロパティ +export interface ITextField { + width: number; + x: number; +} +// 使用: TextAtom, CenterTextFieldUseCase + +// ITextFieldProps.ts - テキストフィールドの詳細プロパティ設定 +// 使用: TextAtomのコンストラクタ + +// ITextFieldType.ts - テキストフィールドタイプ +// ITextFieldAutoSize.ts - テキストフィールドオートサイズ +// ITextFormatAlign.ts - テキストフォーマットアライン +// ITextFormatObject.ts - テキストフォーマットスタイル設定 +``` + +### 2. データ転送オブジェクト (DTO) + +```typescript +// IHomeTextResponse.ts - APIレスポンス型 +export interface IHomeTextResponse { + word: string; +} +// 使用: HomeTextRepository.get()の戻り値型 +``` + +### 3. 画面遷移関連 + +```typescript +// IViewName.ts - 利用可能な画面名 (Union型) +export type ViewName = "top" | "home"; +// 使用: NavigateToViewUseCase +// 新画面追加時はこの型にも追加が必要 +``` + +### 4. 設定関連 + +- `IConfig.ts` - アプリケーション全体設定 +- `IStage.ts` - ステージ設定 (`stage.json`の型) +- `IRouting.ts` - ルーティング設定 +- `IGotoView.ts` - 画面遷移オプション +- `IRequest.ts` / `IRequestType.ts` - HTTPリクエスト設定 +- `IOptions.ts` - オプション設定 + +## Interface Template + +```typescript +/** + * @description [インターフェースの説明] + * [Interface description] + * + * @interface + */ +export interface IYourInterface +{ + /** + * @description [プロパティの説明] + * [Property description] + * + * @type {type} + */ + propertyName: type; + + /** + * @description [メソッドの説明] + * [Method description] + * + * @param {ParamType} paramName + * @return {ReturnType} + * @method + */ + methodName(paramName: ParamType): ReturnType; +} +``` + +## Best Practices + +```typescript +// OK: 必要最小限 +export interface ITextField { + width: number; + x: number; +} + +// NG: 不要なプロパティ +export interface ITextField { + width: number; + height: number; // 使用しない + x: number; + y: number; // 使用しない +} + +// OK: 型の再利用 +export interface IPosition { x: number; y: number; } +export interface ITextField extends IPosition { width: number; } + +// OK: 明示的型 +export interface IHomeTextResponse { word: string; } + +// NG: any型 +export interface IHomeTextResponse { word: any; } +``` + +## Adding New Interface Steps + +1. 目的を明確にする(どの層の依存を抽象化するか) +2. `I`プレフィックスの命名規則に従う +3. 必要最小限のプロパティ/メソッドのみ定義 +4. JSDocコメントを追加 +5. 使用箇所を明記 diff --git a/template/specs/model.md b/template/specs/model.md new file mode 100644 index 0000000..24ed778 --- /dev/null +++ b/template/specs/model.md @@ -0,0 +1,374 @@ +# Model Layer (Application / Domain / Infrastructure) + +Model層はビジネスロジックとデータアクセスを担当。Clean Architectureに基づき3層で構成。 + +## Directory Structure + +``` +model/ +├── application/ # UseCase (ビジネスロジック) +│ └── {screen}/ +│ └── usecase/ +│ └── {Action}UseCase.ts +├── domain/ # コアビジネスロジック +│ └── {feature}/ +│ ├── {Feature}.ts +│ └── service/ +│ └── {Feature}{Action}Service.ts +└── infrastructure/ # Repository (データアクセス) + └── repository/ + └── {Resource}Repository.ts +``` + +## Layer Dependencies + +``` +Application → Domain (uses) +Application → Infrastructure (calls) +Domain → 依存なし (最も安定) +``` + +--- + +## Application Layer (UseCase) + +### Rules + +- 1つのユーザーアクションに対して1つのUseCaseクラスを作成 +- エントリーポイントは `execute` メソッドに統一 +- インターフェースに依存し、具象クラスに依存しない +- 画面ごとにディレクトリを作成: `application/{screen}/usecase/` + +### UseCase Template + +```typescript +import type { IYourInterface } from "@/interface/IYourInterface"; + +/** + * @description [UseCaseの説明] + * [UseCase description] + * + * @class + */ +export class YourUseCase +{ + /** + * @description [処理の説明] + * [Process description] + * + * @param {IYourInterface} param + * @return {void} + * @method + * @public + */ + execute (param: IYourInterface): void + { + // ビジネスロジックを実装 + } +} +``` + +### UseCase with Repository + +```typescript +import { YourRepository } from "@/model/infrastructure/repository/YourRepository"; +import type { IYourResponse } from "@/interface/IYourResponse"; + +export class FetchDataUseCase +{ + async execute (): Promise + { + try { + const data = await YourRepository.get(); + // ビジネスロジック: データの加工・検証 + return data; + } catch (error) { + console.error('Failed to fetch data:', error); + throw error; + } + } +} +``` + +### UseCase Composition (複数UseCaseの組み合わせ) + +```typescript +export class InitializeScreenUseCase +{ + private readonly fetchUseCase: FetchDataUseCase; + private readonly centerUseCase: CenterTextFieldUseCase; + + constructor () + { + this.fetchUseCase = new FetchDataUseCase(); + this.centerUseCase = new CenterTextFieldUseCase(); + } + + async execute (textField: ITextField): Promise + { + const data = await this.fetchUseCase.execute(); + this.centerUseCase.execute(textField, stageWidth); + } +} +``` + +### UseCase Anti-Patterns + +```typescript +// NG: 複数の責務 +export class DragUseCase { + start(target: IDraggable): void { ... } + stop(target: IDraggable): void { ... } + validate(target: IDraggable): boolean { ... } +} + +// OK: 単一の責務 +export class StartDragUseCase { + execute(target: IDraggable): void { target.startDrag(); } +} +export class StopDragUseCase { + execute(target: IDraggable): void { target.stopDrag(); } +} + +// NG: 具象クラスに依存 +execute(target: HomeBtnMolecule): void { ... } + +// OK: インターフェースに依存 +execute(target: IDraggable): void { ... } +``` + +### UseCase Test Template + +```typescript +import { StartDragUseCase } from "./StartDragUseCase"; +import type { IDraggable } from "@/interface/IDraggable"; + +describe('StartDragUseCase', () => { + test('should call startDrag on target', () => { + const mockDraggable: IDraggable = { + startDrag: vi.fn(), + stopDrag: vi.fn() + }; + + const useCase = new StartDragUseCase(); + useCase.execute(mockDraggable); + + expect(mockDraggable.startDrag).toHaveBeenCalled(); + }); +}); +``` + +--- + +## Domain Layer + +### Rules + +- アプリケーションのコアビジネスルールを実装 +- 可能な限りフレームワーク非依存(※Next2D描画機能の使用は許容) +- 純粋関数を心がけ、副作用を最小化 +- 可能な限り不変オブジェクトを使用 + +### Domain Service (Functional Style) + +```typescript +/** + * @description [サービスの説明] + * [Service description] + * + * @param {ParamType} param + * @return {ReturnType} + */ +export const execute = (param: ParamType): ReturnType => +{ + // ビジネスルールの実装 + return result; +}; +``` + +### Domain Class (Class-based Style) + +```typescript +import { Shape, stage } from "@next2d/display"; +import { Event } from "@next2d/events"; + +/** + * @description [ドメインクラスの説明] + * [Domain class description] + * + * @class + */ +export class YourDomainClass +{ + public readonly shape: Shape; + + constructor () + { + this.shape = new Shape(); + } + + execute (): void + { + // コアビジネスロジック + } +} +``` + +### Domain Callback Pattern + +`config.json`の`gotoView.callback`で設定されたクラスは、画面遷移完了後に`execute()`が呼び出される。 + +```typescript +// config.json: "gotoView": { "callback": ["domain.callback.Background"] } +// → model/domain/callback/Background.ts の execute() が呼ばれる + +export class Background +{ + execute (): void + { + const context = app.getContext(); + const view = context.view; + if (!view) return; + view.addChildAt(this.shape, 0); + } +} +``` + +### Domain Directory Extensions (将来の拡張) + +``` +domain/ +├── callback/ # コールバック処理 +├── service/ # ドメインサービス +├── entity/ # エンティティ (ID持ち) +└── value-object/ # 値オブジェクト +``` + +--- + +## Infrastructure Layer (Repository) + +### Rules + +- 外部システムとの連携(API、DB等)を担当 +- `any`型を避け、明示的な型定義を使用 +- すべての外部アクセスでtry-catchを実装 +- エンドポイントは`config`から取得(ハードコーディング禁止) +- シンプルな場合は静的メソッド、状態を持つ場合はインスタンスメソッド + +### Repository Template + +```typescript +import type { IYourResponse } from "@/interface/IYourResponse"; +import { config } from "@/config/Config"; + +/** + * @description [Repositoryの説明] + * [Repository description] + * + * @class + */ +export class YourRepository +{ + /** + * @description [処理の説明] + * [Process description] + * + * @param {string} id + * @return {Promise} + * @static + * @throws {Error} [エラーの説明] + */ + static async get (id: string): Promise + { + try { + const response = await fetch( + `${config.api.endPoint}api/your-endpoint/${id}` + ); + + if (!response.ok) { + throw new Error( + `HTTP error! status: ${response.status}` + ); + } + + return await response.json() as IYourResponse; + + } catch (error) { + console.error('Failed to fetch data:', error); + throw error; + } + } +} +``` + +### Repository with Cache + +```typescript +export class CachedRepository +{ + private static cache: Map = new Map(); + private static readonly CACHE_TTL = 60000; + + static async get (id: string): Promise + { + const cached = this.cache.get(id); + const now = Date.now(); + + if (cached && (now - cached.timestamp) < this.CACHE_TTL) { + return cached.data; + } + + const response = await fetch(`${config.api.endPoint}api/${id}`); + const data = await response.json(); + this.cache.set(id, { data, timestamp: now }); + + return data; + } +} +``` + +### Repository Anti-Patterns + +```typescript +// NG: any型 +static async get(): Promise { ... } + +// OK: 明示的型定義 +static async get(): Promise { ... } + +// NG: エラーハンドリングなし +static async get(): Promise { + const response = await fetch(...); + return await response.json(); +} + +// NG: ハードコーディング +const response = await fetch('https://example.com/api/data.json'); + +// OK: configから取得 +const response = await fetch(`${config.api.endPoint}api/data.json`); +``` + +### routing.json Custom Request Pattern + +`routing.json`でRepositoryを直接呼び出すことも可能: + +```json +{ + "home": { + "requests": [ + { + "type": "custom", + "class": "infrastructure.repository.HomeTextRepository", + "access": "static", + "method": "get", + "name": "HomeText", + "cache": true + } + ] + } +} +``` + +取得したデータは `app.getResponse().get("HomeText")` でアクセス可能。 diff --git a/template/specs/overview.md b/template/specs/overview.md new file mode 100644 index 0000000..aa80d80 --- /dev/null +++ b/template/specs/overview.md @@ -0,0 +1,94 @@ +# Next2D Framework TypeScript Template - Overview + +## Project Summary + +Next2D Frameworkを使用したTypeScriptプロジェクトテンプレート。MVVM + Clean Architecture + Atomic Designを採用。 + +- **レンダリングエンジン**: Next2D Player +- **フレームワーク**: Next2D Framework +- **言語**: TypeScript +- **ビルドツール**: Vite +- **テスト**: Vitest +- **パッケージマネージャ**: npm + +## Requirements + +- Node.js 22.x以上 +- npm 10.x以上 +- iOS: Xcode 14以上 (iOS/Androidビルド時のみ) +- Android: Android Studio, JDK 21以上 (iOS/Androidビルド時のみ) + +## Architecture + +**MVVM + Clean Architecture + Atomic Design** の5層構成: + +``` +View Layer (view/, ui/) + └─ depends on ─→ Interface Layer (interface/) + ↑ +Application Layer (model/application/) + ├─ depends on ─→ Interface Layer + ├─ depends on ─→ Domain Layer (model/domain/) + └─ calls ──────→ Infrastructure Layer (model/infrastructure/) +``` + +### Layer Dependencies (依存関係の方向) + +- **View層** → Interface経由でApplication層を使用 +- **Application層** → Interface経由でDomain層・Infrastructure層を使用 +- **Domain層** → 何にも依存しない(最も安定、純粋なビジネスロジック) +- **Infrastructure層** → Interface層を実装 + +### Key Design Patterns + +1. **MVVM**: View(表示) / ViewModel(橋渡し) / Model(ビジネスロジック+データアクセス) +2. **UseCase Pattern**: ユーザーアクションごとに専用のUseCaseクラスを作成 +3. **Dependency Inversion**: 具象クラスではなくインターフェースに依存 +4. **Repository Pattern**: データアクセスを抽象化 +5. **Atomic Design**: Atom → Molecule → Organism → Template → Page + +## Directory Structure + +``` +src/ +├── config/ # 設定ファイル (stage.json, config.json, routing.json) +├── interface/ # TypeScriptインターフェース定義 +├── model/ +│ ├── application/ # UseCase (ビジネスロジック実装) +│ │ └── {screen}/usecase/ +│ ├── domain/ # コアビジネスロジック +│ │ └── {feature}/service/ +│ └── infrastructure/ # Repository (データアクセス) +│ └── repository/ +├── ui/ +│ ├── animation/ # アニメーション定義 +│ │ └── {screen}/ +│ ├── component/ +│ │ ├── atom/ # 最小コンポーネント (Button, Text等) +│ │ ├── molecule/ # 複合コンポーネント +│ │ ├── organism/ # 複数Moleculeの組み合わせ (拡張用) +│ │ ├── page/ # ページコンポーネント +│ │ │ └── {screen}/ +│ │ └── template/ # ページテンプレート (拡張用) +│ └── content/ # Animation Tool生成コンテンツ +├── view/ # View & ViewModel +│ └── {screen}/ +│ ├── {Screen}View.ts +│ └── {Screen}ViewModel.ts +└── assets/ # 静的ファイル (画像, JSON) + +@types/ # グローバル型定義 (.d.ts) +electron/ # Electron設定 (デスクトップビルド用) +file/ # Animation Tool n2dファイル +mock/ # 開発用モックデータ (API, Content, 画像) +``` + +## Best Practices (全体共通) + +1. **インターフェース優先**: 常に具象クラスではなくインターフェースに依存 +2. **単一責任の原則**: 各クラスは1つの責務のみを持つ +3. **型安全性**: `any`型を避け、明示的な型定義を使用 +4. **テスタブル**: 各層を独立してテスト可能にする +5. **JSDoc**: 処理内容を日英両方で明記 +6. **executeメソッド**: UseCaseのエントリーポイントを統一 +7. **エラーハンドリング**: Infrastructure層で適切に処理 diff --git a/template/specs/ui.md b/template/specs/ui.md new file mode 100644 index 0000000..1bbcee0 --- /dev/null +++ b/template/specs/ui.md @@ -0,0 +1,328 @@ +# UI Layer (Components / Animation / Content) + +## Directory Structure + +``` +ui/ +├── animation/ # アニメーション定義 +│ └── {screen}/ +│ └── {Component}{Action}Animation.ts +├── component/ +│ ├── atom/ # 最小単位 (Button, Text等) +│ ├── molecule/ # Atomの組み合わせ +│ ├── organism/ # 複数Moleculeの組み合わせ (拡張用) +│ ├── page/ # ページコンポーネント +│ │ └── {screen}/ +│ └── template/ # ページテンプレート (拡張用) +└── content/ # Animation Tool生成コンテンツ +``` + +## Rules (共通) + +- 各コンポーネントは単一の責務のみ +- ビジネスロジックやデータアクセスに直接依存しない +- データはViewModelから引数で受け取る +- インターフェースを実装して抽象化する + +--- + +## Atomic Design Hierarchy + +### Atom (原子) - 最小単位 + +最も基本的なUI要素。これ以上分割できない。 + +```typescript +// ButtonAtom: ボタンの基本機能 +import { Sprite } from "@next2d/display"; + +export class ButtonAtom extends Sprite +{ + constructor () + { + super(); + this.buttonMode = true; + } +} +``` + +```typescript +// TextAtom: テキスト表示の基本機能 +import { TextField } from "@next2d/text"; +import type { ITextField } from "@/interface/ITextField"; +import type { ITextFormatObject } from "@/interface/ITextFormatObject"; + +export class TextAtom extends TextField implements ITextField +{ + constructor ( + text: string = "", + props: any | null = null, + format_object: ITextFormatObject | null = null + ) { + super(); + // プロパティ設定、フォーマット設定 + } +} +``` + +### Molecule (分子) - Atomの組み合わせ + +複数のAtomを組み合わせた、特定の用途向けコンポーネント。 + +```typescript +import { ButtonAtom } from "../atom/ButtonAtom"; +import { HomeContent } from "@/ui/content/HomeContent"; +import type { IDraggable } from "@/interface/IDraggable"; + +export class HomeBtnMolecule extends ButtonAtom implements IDraggable +{ + private readonly homeContent: HomeContent; + + constructor () + { + super(); + this.homeContent = new HomeContent(); + this.addChild(this.homeContent); + } + + // IDraggableメソッド(startDrag/stopDrag)はMovieClipContentの親クラスから継承 +} +``` + +```typescript +import { ButtonAtom } from "../atom/ButtonAtom"; +import { TextAtom } from "../atom/TextAtom"; + +export class TopBtnMolecule extends ButtonAtom +{ + constructor (text: string) // ViewModelからテキストを受け取る + { + super(); + const textField = new TextAtom(text, { autoSize: "center" }); + this.addChild(textField); + } + + playEntrance (callback: () => void): void + { + // アニメーション再生 + } +} +``` + +### Organism (有機体) - 拡張用 + +複数のMoleculeを組み合わせた大きな機能単位。必要に応じて実装。 + +### Page (ページ) + +画面全体を構成するコンポーネント。ViewからPageを配置し、PageがMolecule/Atomを組み合わせて画面構築。 + +### Template (テンプレート) - 拡張用 + +ページのレイアウト構造を定義。必要に応じて実装。 + +## Component Creation Templates + +### New Atom + +```typescript +import { Sprite } from "@next2d/display"; + +export class YourAtom extends Sprite +{ + constructor (props: any = null) + { + super(); + if (props) { + Object.assign(this, props); + } + } +} +``` + +### New Molecule + +```typescript +import { ButtonAtom } from "../atom/ButtonAtom"; +import { TextAtom } from "../atom/TextAtom"; + +export class YourMolecule extends ButtonAtom +{ + constructor () + { + super(); + const text = new TextAtom("Click me"); + this.addChild(text); + } +} +``` + +## Anti-Patterns + +```typescript +// NG: コンポーネント内でデータ取得 +export class BadAtom extends TextField { + async fetchDataFromAPI() { ... } // NG: データ取得は別層の責務 +} + +// NG: 直接APIアクセス +constructor() { + const data = await Repository.get(); // NG +} + +// OK: ViewModelからデータを受け取る +constructor(text: string) { + this.textField = new TextAtom(text); // OK +} +``` + +--- + +## Animation + +アニメーションロジックをコンポーネントから分離し、再利用性と保守性を向上。 + +### Naming Convention + +`{Component}{Action}Animation.ts` (例: `TopBtnShowAnimation.ts`) + +### Animation Types + +- **Show Animation**: 画面表示時のアニメーション +- **Exit Animation**: 画面遷移時のアニメーション +- **Interaction Animation**: ユーザー操作に対するアニメーション + +### Animation Class Template + +```typescript +import type { Sprite } from "@next2d/display"; +import { Tween, Easing, type Job } from "@next2d/ui"; +import { Event } from "@next2d/events"; + +/** + * @description [アニメーションの説明] + * [Animation description] + * + * @class + * @public + */ +export class YourAnimation +{ + private readonly _job: Job; + + /** + * @param {Sprite} sprite - アニメーション対象 + * @param {() => void} callback - 完了時コールバック + * @constructor + * @public + */ + constructor ( + sprite: Sprite, + callback?: () => void + ) { + // 初期状態設定 + sprite.alpha = 0; + + // Tween設定: (対象, 開始値, 終了値, 秒数, 遅延秒数, イージング) + this._job = Tween.add(sprite, + { "alpha": 0 }, + { "alpha": 1 }, + 0.5, 1, Easing.outQuad + ); + + if (callback) { + this._job.addEventListener(Event.COMPLETE, callback); + } + } + + /** + * @description アニメーション開始 + * Start animation + * + * @method + * @public + */ + start (): void + { + this._job.start(); + } +} +``` + +### Component-Animation Coordination + +```typescript +// component/molecule/TopBtnMolecule.ts +import { TopBtnShowAnimation } from "@/ui/animation/top/TopBtnShowAnimation"; + +export class TopBtnMolecule extends ButtonAtom { + playShow(callback: () => void): void { + new TopBtnShowAnimation(this, callback).start(); + } +} +``` + +--- + +## Content (Animation Tool) + +Animation Toolで作成されたコンテンツをTypeScriptクラスとしてラップ。 + +### Content Template + +```typescript +import { MovieClipContent } from "@next2d/framework"; + +/** + * @description [コンテンツの説明] + * [Content description] + * + * @class + * @extends {MovieClipContent} + */ +export class YourContent extends MovieClipContent +{ + /** + * @description Animation Toolのシンボル名を返す + * Returns the Animation Tool symbol name + * + * @return {string} + * @readonly + */ + get namespace (): string + { + return "YourSymbolName"; // Animation Toolで設定した名前と一致させる + } +} +``` + +### Content with Interface + +```typescript +import { MovieClipContent } from "@next2d/framework"; +import type { IDraggable } from "@/interface/IDraggable"; + +export class HomeContent extends MovieClipContent implements IDraggable +{ + get namespace (): string + { + return "HomeContent"; + } + + // IDraggableメソッド(startDrag/stopDrag)は + // MovieClipContentの親クラス(MovieClip)から継承 +} +``` + +### Content Creation Steps + +1. Animation Toolでシンボルを作成 +2. `.n2d`ファイルを`file/`ディレクトリに配置 +3. Contentクラスを作成 (`namespace`はシンボル名と一致させる) +4. Molecule等のコンポーネントで使用 + +### Content Rules + +- クラス名とシンボル名を一致させる +- アニメーションの制御のみを担当 +- 必要な機能はインターフェースで定義 diff --git a/template/specs/view-viewmodel.md b/template/specs/view-viewmodel.md new file mode 100644 index 0000000..50e7705 --- /dev/null +++ b/template/specs/view-viewmodel.md @@ -0,0 +1,216 @@ +# View / ViewModel (MVVM Pattern) + +## Rules + +- 1画面にView + ViewModelをワンセット作成 +- ディレクトリ名はキャメルケースの最初のブロック (例: `questList` → `view/quest/`) +- Viewは表示構造のみ担当、ビジネスロジックはViewModelに委譲 +- イベントは必ずViewModelに委譲(View内で完結させない) +- ViewModelはインターフェースに依存し、具象クラスに依存しない + +## Lifecycle (実行順序) + +``` +1. ViewModel インスタンス生成 +2. ViewModel.initialize() ← ViewModelが先 +3. View インスタンス生成 (ViewModelを注入) +4. View.initialize() ← UIコンポーネントの構築 +5. View.onEnter() ← 画面表示時の処理 + (ユーザー操作) +6. View.onExit() ← 画面非表示時の処理 +``` + +### View Lifecycle Methods + +| Method | Timing | Purpose | Do | Don't | +|--------|--------|---------|-----|-------| +| `initialize()` | View生成直後、表示前 | UIコンポーネントの生成・配置・イベントリスナー登録 | addChild, addEventListener | API呼び出し、重い処理 | +| `onEnter()` | initialize完了後、画面表示直前 | 入場アニメーション、データ取得、タイマー開始 | アニメーション再生、fetchInitialData | UIコンポーネント生成 | +| `onExit()` | 別画面遷移前 | アニメーション停止、タイマークリア、リソース解放 | clearInterval, 状態リセット | 新リソース作成 | + +### ViewModel Lifecycle Methods + +| Method | Timing | Purpose | View参照 | +|--------|--------|---------|---------| +| `constructor()` | インスタンス生成時 | UseCaseの生成 | 不可 | +| `initialize()` | Viewの`initialize()`より前 | 初期データ取得、状態初期化 | 不可 | +| イベントハンドラ | ユーザー操作時 | ビジネスロジック実行 | 可能 | + +## View Class Template + +```typescript +import type { {Screen}ViewModel } from "./{Screen}ViewModel"; +import { View } from "@next2d/framework"; + +/** + * @class + * @extends {View} + */ +export class {Screen}View extends View +{ + /** + * @param {{Screen}ViewModel} vm + * @constructor + * @public + */ + constructor ( + private readonly vm: {Screen}ViewModel + ) { + super(); + } + + /** + * @description 画面の初期化 - UIコンポーネントの構築 + * Initialize - Build UI components + * + * @return {Promise} + * @method + * @override + * @public + */ + async initialize (): Promise + { + // UIコンポーネントの作成と配置 + // イベントリスナーの登録 (ViewModelのメソッドに接続) + } + + /** + * @description 画面表示時の処理 + * On screen shown + * + * @return {Promise} + * @method + * @override + * @public + */ + async onEnter (): Promise + { + // 入場アニメーション、データ取得 + } + + /** + * @description 画面非表示時の処理 + * On screen hidden + * + * @return {Promise} + * @method + * @override + * @public + */ + async onExit (): Promise + { + // タイマークリア、リソース解放 + } +} +``` + +## ViewModel Class Template + +```typescript +import { ViewModel } from "@next2d/framework"; +import { YourUseCase } from "@/model/application/{screen}/usecase/YourUseCase"; + +/** + * @class + * @extends {ViewModel} + */ +export class {Screen}ViewModel extends ViewModel +{ + private readonly yourUseCase: YourUseCase; + + constructor () + { + super(); + this.yourUseCase = new YourUseCase(); + } + + /** + * @description ViewModelの初期化 (Viewのinitialize()より前に呼ばれる) + * Initialize ViewModel (called before View's initialize()) + * + * @return {Promise} + * @method + * @override + * @public + */ + async initialize (): Promise + { + // 初期データ取得、状態初期化 + // ※ この時点ではViewは未生成のためUI操作不可 + } + + /** + * @description イベントハンドラ + * Event handler + * + * @param {PointerEvent} event + * @return {void} + * @method + * @public + */ + yourEventHandler (event: PointerEvent): void + { + // インターフェースを通じてターゲットを取得 + const target = event.currentTarget as unknown as IYourInterface; + this.yourUseCase.execute(target); + } +} +``` + +## View-ViewModel Coordination Pattern + +ViewModelの`initialize()`で事前取得したデータをViewで使用するパターン: + +```typescript +// ViewModel: 事前にデータ取得 +async initialize(): Promise { + const data = await HomeTextRepository.get(); + this.homeText = data.word; +} + +getHomeText(): string { + return this.homeText; +} + +// View: ViewModelから取得済みデータを使用 +async initialize(): Promise { + // vm.initialize()は既に完了している + const text = this.vm.getHomeText(); + const textField = new TextAtom(text); + this.addChild(textField); +} +``` + +## Code Generation + +```bash +npm run generate +``` + +`routing.json`のトッププロパティ値を分解し、`view`ディレクトリ直下に対象ディレクトリがなければ作成。View/ViewModelが存在しない場合のみ新規クラスを生成。 + +## Anti-Patterns + +```typescript +// NG: Viewでビジネスロジック +class BadView extends View { + async initialize() { + btn.addEventListener(PointerEvent.POINTER_DOWN, async () => { + const data = await Repository.get(); // NG + this.processData(data); // NG + }); + } +} + +// NG: ViewModelで具象クラスに依存 +homeContentPointerDownEvent(event: PointerEvent): void { + const target = event.currentTarget as HomeBtnMolecule; // NG + target.startDrag(); +} + +// OK: ViewModelでインターフェースに依存 +homeContentPointerDownEvent(event: PointerEvent): void { + const target = event.currentTarget as unknown as IDraggable; // OK + this.startDragUseCase.execute(target); +} +``` diff --git a/template/src/assets/README.md b/template/src/assets/README.md index 8da25f2..e4f6bea 100644 --- a/template/src/assets/README.md +++ b/template/src/assets/README.md @@ -2,4 +2,66 @@ インラインで利用したい画像・JSONなど静的ファイルを格納するディレクトリです。 -This directory stores static files such as images and JSON to be used inline. \ No newline at end of file +This directory stores static files such as images and JSON to be used inline. + +## 概要 / Overview + +ビルド時にバンドルに含めたい静的アセットを配置します。Viteのインポート機能を利用して、画像やJSONデータをインラインで使用できます。 + +Place static assets that you want to include in the bundle at build time. You can use images and JSON data inline using Vite's import functionality. + +## 使用例 / Usage + +### 画像のインポート / Importing Images + +```typescript +import logoImage from "@/assets/logo.png?inline"; + +// logoImageはビルド時に解決されたURLになります +// logoImage will be a resolved URL at build time +const shape = new Shape(); +await shape.load(logoImage); +``` + +### JSONのインポート / Importing JSON + +```typescript +import animation from "@/assets/animation.json"; + +// AnimationToolで書き出したJSONは直接インポートできます +// JSON exported from AnimationTool can be imported directly. +const loader = new Loader(); +await loader.loadJSON(animation); +``` + +## ディレクトリ構造の例 / Example Directory Structure + +``` +assets/ +├── images/ +│ ├── logo.png +│ └── icons/ +│ └── home.svg +└── json/ + └── animation.json +``` + +## assetsとmockの使い分け / Difference between assets and mock + +| 項目 / Item | assets | mock | +|-------------|--------|------| +| **用途 / Purpose** | バンドルに含める | 開発サーバーで配信 | +| **アクセス方法 / Access** | importで取得 | URL経由でfetch | +| **ビルド / Build** | バンドルに含まれる | 含まれない | +| **使用場面 / Use case** | アプリ内で直接使用 | API模擬・外部リソース | + +## 対応フォーマット / Supported Formats + +- **画像 / Images**: PNG, JPG, SVG, WebP, GIF +- **データ / Data**: JSON(Animation Tool) +- **その他 / Others**: Viteがサポートする形式 + +## 関連ドキュメント / Related Documentation + +- [../mock/README.md](../../mock/README.md) - 開発用モックデータ +- [Vite Asset Handling](https://vitejs.dev/guide/assets.html) - Viteのアセット処理 \ No newline at end of file diff --git a/template/src/index.ts b/template/src/index.ts index cfec6c0..e39ac4d 100644 --- a/template/src/index.ts +++ b/template/src/index.ts @@ -1,6 +1,5 @@ "use strict"; -import type { IConfig } from "@/interface/IConfig"; import { app } from "@next2d/framework"; import { config } from "@/config/Config"; import { packages } from "@/Packages"; @@ -16,7 +15,7 @@ const boot = async (event: Event | null = null): Promise => event.target.removeEventListener("DOMContentLoaded", boot); } - await app.initialize(config as IConfig, packages).run(); + await app.initialize(config, packages).run(); await app.gotoView(); }; diff --git a/template/src/interface/IConfig.ts b/template/src/interface/IConfig.ts deleted file mode 100644 index 72f65a4..0000000 --- a/template/src/interface/IConfig.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { IStage } from "./IStage"; -import type { IRouting } from "./IRouting"; -import type { IGotoView } from "./IGotoView"; - -interface IBaseConfig { - [key: string]: any -} - -export interface IConfig extends IBaseConfig { - platform: string; - stage: IStage; - spa: boolean; - defaultTop?: string; - gotoView?: IGotoView; - routing?: { - [key: string]: IRouting - }; - loading?: { - callback: string; - }; -} \ No newline at end of file diff --git a/template/src/interface/IDraggable.ts b/template/src/interface/IDraggable.ts new file mode 100644 index 0000000..31b894e --- /dev/null +++ b/template/src/interface/IDraggable.ts @@ -0,0 +1,25 @@ +/** + * @description ドラッグ可能なオブジェクトのインターフェース + * Interface for draggable objects + * + * @interface + */ +export interface IDraggable { + /** + * @description ドラッグを開始する + * Start dragging + * + * @return {void} + * @method + */ + startDrag(): void; + + /** + * @description ドラッグを停止する + * Stop dragging + * + * @return {void} + * @method + */ + stopDrag(): void; +} diff --git a/template/src/interface/IGotoView.ts b/template/src/interface/IGotoView.ts deleted file mode 100644 index d94f966..0000000 --- a/template/src/interface/IGotoView.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface IGotoView { - callback: string | string[]; -} \ No newline at end of file diff --git a/template/src/interface/IHomeTextResponse.ts b/template/src/interface/IHomeTextResponse.ts new file mode 100644 index 0000000..1dd883c --- /dev/null +++ b/template/src/interface/IHomeTextResponse.ts @@ -0,0 +1,15 @@ +/** + * @description Home画面のテキストレスポンス + * Home screen text response + * + * @interface + */ +export interface IHomeTextResponse { + /** + * @description 表示するテキスト + * Text to display + * + * @type {string} + */ + word: string; +} diff --git a/template/src/interface/IRequest.ts b/template/src/interface/IRequest.ts deleted file mode 100644 index 44cedd3..0000000 --- a/template/src/interface/IRequest.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { IRequestType } from "./IRequestType"; - -export interface IRequest { - type: IRequestType; - path?: string; - name?: string; - cache?: boolean; - callback?: string | string[any]; - class?: string; - access?: string; - method?: string; - headers?: HeadersInit; - body?: any; -} \ No newline at end of file diff --git a/template/src/interface/IRequestType.ts b/template/src/interface/IRequestType.ts deleted file mode 100644 index 936eecb..0000000 --- a/template/src/interface/IRequestType.ts +++ /dev/null @@ -1 +0,0 @@ -export type IRequestType = "json" | "content" | "custom" | "cluster"; \ No newline at end of file diff --git a/template/src/interface/IRouting.ts b/template/src/interface/IRouting.ts deleted file mode 100644 index 24bce51..0000000 --- a/template/src/interface/IRouting.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { IRequest } from "./IRequest"; - -export interface IRouting { - private?: boolean; - requests?: IRequest[]; - redirect?: string; -} \ No newline at end of file diff --git a/template/src/interface/IStage.ts b/template/src/interface/IStage.ts deleted file mode 100644 index 4215be4..0000000 --- a/template/src/interface/IStage.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { IOptions } from "./IOptions"; - -export interface IStage { - width: number; - height: number; - fps: number; - options?: IOptions; -} \ No newline at end of file diff --git a/template/src/interface/ITextField.ts b/template/src/interface/ITextField.ts new file mode 100644 index 0000000..90af491 --- /dev/null +++ b/template/src/interface/ITextField.ts @@ -0,0 +1,23 @@ +/** + * @description テキストフィールドのインターフェース + * Interface for text field + * + * @interface + */ +export interface ITextField { + /** + * @description テキストフィールドの幅 + * Width of the text field + * + * @type {number} + */ + width: number; + + /** + * @description テキストフィールドのX座標 + * X coordinate of the text field + * + * @type {number} + */ + x: number; +} diff --git a/template/src/interface/ITextFieldAutoSize.ts b/template/src/interface/ITextFieldAutoSize.ts new file mode 100644 index 0000000..e38ed29 --- /dev/null +++ b/template/src/interface/ITextFieldAutoSize.ts @@ -0,0 +1 @@ +export type ITextFieldAutoSize = "center" | "left" | "none" | "right"; \ No newline at end of file diff --git a/template/src/interface/ITextFieldProps.ts b/template/src/interface/ITextFieldProps.ts new file mode 100644 index 0000000..63ed748 --- /dev/null +++ b/template/src/interface/ITextFieldProps.ts @@ -0,0 +1,16 @@ +import type { ITextFieldAutoSize } from "./ITextFieldAutoSize"; +import type { ITextFieldType } from "./ITextFieldType"; + +export interface ITextFieldProps { + selectable?: boolean; + mouseEnabled?: boolean; + wordWrap?: boolean; + multiline?: boolean; + maxChars?: number; + background?: boolean; + backgroundColor?: number; + border?: boolean; + borderColor?: number; + autoSize?: ITextFieldAutoSize; + type?: ITextFieldType; +} \ No newline at end of file diff --git a/template/src/interface/ITextFieldType.ts b/template/src/interface/ITextFieldType.ts new file mode 100644 index 0000000..3f92a04 --- /dev/null +++ b/template/src/interface/ITextFieldType.ts @@ -0,0 +1 @@ +export type ITextFieldType = "input" | "static"; \ No newline at end of file diff --git a/template/src/interface/ITextFormatAlign.ts b/template/src/interface/ITextFormatAlign.ts new file mode 100644 index 0000000..aa48dea --- /dev/null +++ b/template/src/interface/ITextFormatAlign.ts @@ -0,0 +1 @@ +export type ITextFormatAlign = "center" | "left" | "right"; \ No newline at end of file diff --git a/template/src/interface/ITextFormatObject.ts b/template/src/interface/ITextFormatObject.ts new file mode 100644 index 0000000..a52ffa8 --- /dev/null +++ b/template/src/interface/ITextFormatObject.ts @@ -0,0 +1,15 @@ +import type { ITextFormatAlign } from "./ITextFormatAlign"; + +export interface ITextFormatObject { + align?: ITextFormatAlign | null; + bold?: boolean | null; + color?: number | null; + font?: string | null; + italic?: boolean | null; + leading?: number | null; + leftMargin?: number | null; + letterSpacing?: number | null; + rightMargin?: number | null; + size?: number | null; + underline?: boolean | null; +} \ No newline at end of file diff --git a/template/src/interface/IViewName.ts b/template/src/interface/IViewName.ts new file mode 100644 index 0000000..c8b4cf3 --- /dev/null +++ b/template/src/interface/IViewName.ts @@ -0,0 +1,7 @@ +/** + * @description 画面名の型定義 + * Type definition for view names + * + * @type + */ +export type ViewName = "top" | "home"; diff --git a/template/src/interface/README.md b/template/src/interface/README.md new file mode 100644 index 0000000..814e6e4 --- /dev/null +++ b/template/src/interface/README.md @@ -0,0 +1,285 @@ +# Interface + +TypeScriptのインターフェース定義を格納するディレクトリです。クリーンアーキテクチャの原則に従い、各層の依存関係を抽象化します。 + +Directory for storing TypeScript interface definitions. Following Clean Architecture principles, this abstracts dependencies between layers. + +## 役割 / Role + +インターフェースは以下の目的で使用されます: + +Interfaces are used for the following purposes: + +1. **依存性の逆転** - 上位層が下位層の具象クラスに直接依存しない +2. **テスタビリティ** - モックやスタブの作成が容易 +3. **疎結合** - 実装の変更が他の層に影響しにくい +4. **型安全性** - TypeScriptの型システムを最大限活用 + +1. **Dependency Inversion** - Higher layers don't depend directly on concrete classes in lower layers +2. **Testability** - Easy to create mocks and stubs +3. **Loose Coupling** - Implementation changes are less likely to affect other layers +4. **Type Safety** - Maximize use of TypeScript's type system + +## インターフェースの分類 / Interface Categories + +### 1. UI関連インターフェース / UI-related Interfaces + +UIコンポーネントの振る舞いを定義します。 + +Defines the behavior of UI components. + +#### IDraggable.ts +ドラッグ可能なオブジェクトのインターフェースです。 + +Interface for draggable objects. + +```typescript +export interface IDraggable { + startDrag(): void; + stopDrag(): void; +} +``` + +**使用例 / Usage:** +- `HomeBtnMolecule` - ボタンコンポーネント +- `HomeContent` - アニメーションコンテンツ + +#### ITextField.ts +テキストフィールドの基本プロパティを定義します。 + +Defines basic properties for text fields. + +```typescript +export interface ITextField { + width: number; + x: number; +} +``` + +**使用例 / Usage:** +- `TextAtom` - テキストアトムコンポーネント +- `CenterTextFieldUseCase` - テキスト中央揃えユースケース + +#### ITextFieldProps.ts +テキストフィールドの詳細なプロパティ設定を定義します。 + +Defines detailed property settings for text fields. + +**使用例 / Usage:** +- `TextAtom`のコンストラクタで使用 + +#### ITextFormatObject.ts +テキストフォーマットのスタイル設定を定義します。 + +Defines style settings for text formatting. + +**使用例 / Usage:** +- `TextAtom`のテキストスタイル設定 + +### 2. データ転送オブジェクト (DTO) / Data Transfer Objects + +APIレスポンスやデータ構造を型定義します。 + +Type definitions for API responses and data structures. + +#### IHomeTextResponse.ts +Home画面のテキストAPIレスポンスの型です。 + +Type definition for Home screen text API response. + +```typescript +export interface IHomeTextResponse { + word: string; +} +``` + +**使用例 / Usage:** +- `HomeTextRepository.get()` の戻り値型 +- API通信の型安全性を確保 + +### 3. 設定関連インターフェース / Configuration Interfaces + +アプリケーション設定の型定義です。 + +Type definitions for application configuration. + +#### IConfig.ts +アプリケーション全体の設定を定義します。 + +Defines the overall application configuration. + +**使用例 / Usage:** +- `config/Config.ts` で使用 +- アプリケーションの初期化時に使用 + +#### IStage.ts +ステージ(画面領域)の設定を定義します。 + +Defines stage (screen area) configuration. + +**使用例 / Usage:** +- `config/stage.json` の型定義 + +#### IRouting.ts +ルーティング設定の型定義です。 + +Type definition for routing configuration. + +**使用例 / Usage:** +- `config/routing.json` の型定義 + +#### IGotoView.ts +画面遷移のオプション設定を定義します。 + +Defines options for view navigation. + +**使用例 / Usage:** +- `app.gotoView()` のパラメータ型 + +#### IRequest.ts / IRequestType.ts +HTTPリクエストの設定を定義します。 + +Defines HTTP request configuration. + +**使用例 / Usage:** +- `routing.json` の `requests` 配列の型定義 + +### 4. 画面遷移関連 / View Navigation + +#### IViewName.ts +利用可能な画面名をUnion型で定義します。 + +Defines available view names as a Union type. + +```typescript +export type ViewName = "top" | "home"; +``` + +**使用例 / Usage:** +- `NavigateToViewUseCase` - 画面遷移時の型安全性を確保 +- 新しい画面を追加した場合は、この型にも追加が必要 + +## ベストプラクティス / Best Practices + +### 1. インターフェースの命名規則 / Interface Naming Convention + +```typescript +// ✅ 良い例: "I" プレフィックスを使用 +export interface IDraggable { ... } +export interface ITextField { ... } + +// ❌ 悪い例: プレフィックスなし +export interface Draggable { ... } +``` + +### 2. 最小限のプロパティ定義 / Minimal Property Definition + +必要最小限のプロパティのみを定義します。 + +Define only the minimum required properties. + +```typescript +// ✅ 良い例: 必要なプロパティのみ +export interface ITextField { + width: number; + x: number; +} + +// ❌ 悪い例: 不要なプロパティも含む +export interface ITextField { + width: number; + height: number; // 使用しない + x: number; + y: number; // 使用しない +} +``` + +### 3. 型の再利用 / Type Reusability + +共通の型は別のインターフェースとして定義します。 + +Define common types as separate interfaces. + +```typescript +// ✅ 良い例 +export interface IPosition { + x: number; + y: number; +} + +export interface ITextField extends IPosition { + width: number; +} +``` + +### 4. `any` 型の禁止 / Avoid `any` Type + +常に明示的な型を使用します。 + +Always use explicit types. + +```typescript +// ✅ 良い例 +export interface IHomeTextResponse { + word: string; +} + +// ❌ 悪い例 +export interface IHomeTextResponse { + word: any; +} +``` + +## 新しいインターフェースの追加 / Adding New Interfaces + +新しいインターフェースを追加する際は、以下の手順に従ってください。 + +Follow these steps when adding new interfaces: + +1. **目的を明確にする** - どの層の依存を抽象化するか +2. **命名規則に従う** - "I" プレフィックスを使用 +3. **最小限に保つ** - 必要なプロパティ/メソッドのみ +4. **ドキュメントを記述** - JSDocコメントを追加 +5. **使用例を確認** - 実際に使用される場所を明記 + +1. **Clarify Purpose** - Which layer's dependency will be abstracted +2. **Follow Naming Convention** - Use "I" prefix +3. **Keep Minimal** - Only necessary properties/methods +4. **Write Documentation** - Add JSDoc comments +5. **Verify Usage** - Document where it will be used + +### テンプレート / Template + +```typescript +/** + * @description [インターフェースの説明] + * [Interface description] + * + * @interface + */ +export interface IYourInterface { + /** + * @description [プロパティの説明] + * [Property description] + * + * @type {type} + */ + propertyName: type; + + /** + * @description [メソッドの説明] + * [Method description] + * + * @param {ParamType} paramName + * @return {ReturnType} + * @method + */ + methodName(paramName: ParamType): ReturnType; +} +``` + +## 関連ドキュメント / Related Documentation + +- [ARCHITECTURE.md](../../ARCHITECTURE.md) - アーキテクチャ全体の説明 +- [model/README.md](../model/README.md) - モデル層の説明 +- [view/README.md](../view/README.md) - ビュー層の説明 diff --git a/template/src/model/README.md b/template/src/model/README.md index 9c1b869..6be37b1 100644 --- a/template/src/model/README.md +++ b/template/src/model/README.md @@ -1,116 +1,440 @@ # Model -アプリケーションのドメインを隔離するためのディレクトリです。このディレクトリ構成は一例であり、アプリケーションの機能性、保守性など、特性に合わせてモデリングを行ってください。 +アプリケーションのビジネスロジックとデータアクセスを担当するディレクトリです。クリーンアーキテクチャに基づき、Application、Domain、Infrastructureの3層で構成されています。 -This directory is used to isolate the domain of the application. The directory structure provided is just one example; please model it according to the specific characteristics of your application, such as its functionality and maintainability. +This directory is responsible for business logic and data access. Based on Clean Architecture, it consists of three layers: Application, Domain, and Infrastructure. -## Example of directory structure +## 📁 現在のディレクトリ構造 / Current Directory Structure -```sh -project -└── src - └── model - ├── application - │ └── content - ├── domain - │ ├── callback - │ └── event - │ ├── top - │ └── home - ├── infrastructure - │ └── repository - └── ui - └── component - ├── atom - └── template - ├── top - └── home -``` - -各ディレクトリの役割を記載していきます。 -(このディレクトリ構成は一例であり、アプリケーションの機能性、保守性など、特性に合わせてモデリングを行ってください。) - -We will describe the role of each directory. -(This directory structure is just an example; please model it according to the specific characteristics of your application, such as its functionality and maintainability.) - -### Application +``` +model/ +├── application/ # アプリケーション層 +│ ├── home/ +│ │ └── usecase/ +│ │ ├── StartDragUseCase.ts +│ │ ├── StopDragUseCase.ts +│ │ └── CenterTextFieldUseCase.ts +│ └── top/ +│ └── usecase/ +│ └── NavigateToViewUseCase.ts +├── domain/ # ドメイン層 +│ └── callback/ +│ ├── Background.ts # コールバッククラス本体 +│ └── Background/ +│ └── service/ +│ ├── BackgroundDrawService.ts +│ └── BackgroundChangeScaleService.ts +└── infrastructure/ # インフラ層 + └── repository/ + └── HomeTextRepository.ts +``` -```sh -application -└── content +## 🎨 アーキテクチャ概要 / Architecture Overview + +```mermaid +graph TB + subgraph Application["⚙️ Application Layer"] + direction TB + UseCase["UseCases"] + UseCaseDesc["ビジネスロジックの実装
Business logic implementation"] + end + + subgraph Domain["💎 Domain Layer"] + direction TB + DomainLogic["Domain Logic"] + DomainDesc["コアビジネスルール
Core business rules"] + end + + subgraph Infrastructure["🔧 Infrastructure Layer"] + direction TB + Repository["Repositories"] + RepoDesc["外部データアクセス
External data access"] + end + + Application -->|uses| Domain + Application -->|calls| Infrastructure + + classDef appStyle fill:#f3e5f5,stroke:#4a148c,stroke-width:2px + classDef domainStyle fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px + classDef infraStyle fill:#fce4ec,stroke:#880e4f,stroke-width:2px + + class Application,UseCase appStyle + class Domain,DomainLogic domainStyle + class Infrastructure,Repository infraStyle ``` -`content` ディレクトリにはAnimation Toolで作成された `DisplayObject` を動的に生成するためのクラスが格納されてます。動的に生成された `DisplayObject` は起動時に `initialize` 関数が実行されます。 `service`、 `usecase` ディレクトリを作成して、`domain` へのアクセスを行う責務を担う事も良いかもしれません。 +## ⚙️ Application Layer + +### 役割 / Role + +- ユーザーのアクションに対応するビジネスロジックを実装 +- 各ユーザーアクションごとにUseCaseクラスを作成 +- インターフェースを通じてDomainとInfrastructureにアクセス -The `content` directory contains classes for dynamically generating `DisplayObject`s created by the Animation Tool. Dynamically generated `DisplayObject`s execute their `initialize` function upon startup. It might also be a good idea to create `service` and `usecase` directories to handle the responsibility of accessing the `domain`. +Implements business logic corresponding to user actions. Creates a UseCase class for each user action. Accesses Domain and Infrastructure through interfaces. -#### Example of cooperation with Animation Tool +### ディレクトリ構造 / Directory Structure + +``` +application/ +├── home/ # Home画面 +│ └── usecase/ +│ ├── StartDragUseCase.ts +│ ├── StopDragUseCase.ts +│ └── CenterTextFieldUseCase.ts +└── top/ # Top画面 + └── usecase/ + └── NavigateToViewUseCase.ts +``` + +### 実装例 / Implementation Example + +#### StartDragUseCase.ts + +```typescript +import type { IDraggable } from "@/interface/IDraggable"; + +export class StartDragUseCase { + /** + * @description ドラッグ可能なオブジェクトのドラッグを開始 + * Start dragging a draggable object + */ + execute(target: IDraggable): void { + target.startDrag(); + } +} +``` + +#### NavigateToViewUseCase.ts + +```typescript +import { app } from "@next2d/framework"; + +export class NavigateToViewUseCase { + /** + * @description 指定された画面に遷移 + * Navigate to the specified view + */ + async execute(viewName: string): Promise { + await app.gotoView(viewName); + } +} +``` + +### 特徴 / Features + +- ✅ **単一責任** - 1つのUseCaseは1つの責務のみ +- ✅ **インターフェース指向** - 抽象に依存、具象に依存しない +- ✅ **再利用可能** - 異なるViewModelから呼び出し可能 +- ✅ **テスタブル** - 独立してユニットテスト可能 + +詳細は [application/README.md](./application/README.md) を参照してください。 + +See [application/README.md](./application/README.md) for details. + +## 💎 Domain Layer + +### 役割 / Role + +- アプリケーションのコアとなるビジネスルールを実装 +- フレームワークや外部ライブラリに依存しない純粋なロジック +- アプリケーション全体で共通して使用される処理 + +Implements the core business rules of the application. Pure logic that doesn't depend on frameworks or external libraries. Commonly used processes throughout the application. + +### ディレクトリ構造 / Directory Structure + +``` +domain/ +└── callback/ + └── Background/ + ├── Background.ts # グラデーション背景 + └── service/ + ├── BackgroundDrawService.ts # 描画サービス + └── BackgroundChangeScaleService.ts # スケール変更 +``` + +### 実装例 / Implementation Example + +#### Background.ts + +```typescript +import { Shape, stage } from "@next2d/display"; +import { Event } from "@next2d/events"; + +/** + * @description グラデーション背景 + * Gradient background + */ +export class Background { + public readonly shape: Shape; + + constructor() { + this.shape = new Shape(); + + // リサイズイベントをリスン + stage.addEventListener(Event.RESIZE, (): void => { + backgroundDrawService(this); + backgroundChangeScaleService(this); + }); + } + + execute(): void { + const context = app.getContext(); + const view = context.view; + if (!view) return; + + // 背景を最背面に配置 + view.addChildAt(this.shape, 0); + } +} +``` -`namespace` にAnimation Toolのシンボルに設定した名前を追記する事で動的生成が可能になります。 -Dynamic generation is enabled by appending the name set for the Animation Tool symbol in the `namespace` field. +#### BackgroundDrawService.ts -```javascript -import { MovieClipContent } from "@next2d/framework"; +```typescript +import type { Background } from "../Background"; +import { config } from "@/config/Config"; +import { Matrix } from "@next2d/geom"; /** - * @see file/sample.n2d - * @class - * @extends {MovieClipContent} + * @description 背景のグラデーション描画を実行 + * Execute background gradient drawing */ -export class TopContent extends MovieClipContent -{ +export const execute = (background: Background): void => { + const width = config.stage.width; + const height = config.stage.height; + + const matrix = new Matrix(); + matrix.createGradientBox(height, width, Math.PI / 2, 0, 0); + + background.shape.graphics + .clear() + .beginGradientFill( + "linear", + ["#1461A0", "#ffffff"], + [0.6, 1], + [0, 255], + matrix + ) + .drawRect(0, 0, width, height) + .endFill(); +}; +``` + +### 特徴 / Features + +- ✅ **フレームワーク非依存** - 可能な限り純粋なTypeScript +- ✅ **再利用可能** - アプリケーション全体で利用 +- ✅ **安定性** - 外部の変更に影響されにくい +- ✅ **テスタブル** - 外部依存が最小限 + +詳細は [domain/README.md](./domain/README.md) を参照してください。 + +See [domain/README.md](./domain/README.md) for details. + +## 🔧 Infrastructure Layer + +### 役割 / Role + +- 外部システムとの連携(API、データベース等) +- データアクセスの実装 +- エラーハンドリングと型安全性の保証 + +Integrates with external systems (APIs, databases, etc.). Implements data access. Ensures error handling and type safety. + +### ディレクトリ構造 / Directory Structure + +``` +infrastructure/ +└── repository/ + └── HomeTextRepository.ts # Home画面テキストデータ +``` + +### 実装例 / Implementation Example + +#### HomeTextRepository.ts + +```typescript +import type { IHomeTextResponse } from "@/interface/IHomeTextResponse"; +import { config } from "@/config/Config"; + +export class HomeTextRepository { /** - * @return {string} - * @readonly - * @public + * @description Home画面のテキストデータを取得 + * Get text data for Home screen */ - get namespace () - { - // Animation Toolのsymbolで設定した名前を追記 - // Append the name assigned to the Animation Tool symbol. - return "TopContent"; + static async get(): Promise { + try { + const response = await fetch( + `${config.api.endPoint}api/home.json` + ); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return await response.json() as IHomeTextResponse; + } catch (error) { + console.error('Failed to fetch home text:', error); + throw error; + } } } ``` -### Domain +### 特徴 / Features -```sh -domain -├── callback -└── event - ├── top - └── home +- ✅ **型安全性** - `any`型を避け、明示的な型定義 +- ✅ **エラーハンドリング** - すべての外部アクセスでtry-catch +- ✅ **設定の外部化** - エンドポイントは`config`から取得 +- ✅ **テスタブル** - モックに差し替え可能 + +詳細は [infrastructure/README.md](./infrastructure/README.md) を参照してください。 + +See [infrastructure/README.md](./infrastructure/README.md) for details. + +## 🔄 レイヤー間の関係 / Layer Relationships + +```mermaid +sequenceDiagram + participant VM as ViewModel + participant UC as UseCase
(Application) + participant DL as Domain Logic
(Domain) + participant Repo as Repository
(Infrastructure) + participant API as External API + + VM->>UC: 1. ビジネスロジック実行
Execute business logic + activate UC + UC->>DL: 2. ドメインロジック使用
Use domain logic + activate DL + DL-->>UC: 3. 結果返却
Return result + deactivate DL + UC->>Repo: 4. データ取得
Fetch data + activate Repo + Repo->>API: 5. API呼び出し
Call API + activate API + API-->>Repo: 6. レスポンス
Response + deactivate API + Repo-->>UC: 7. データ返却
Return data + deactivate Repo + UC-->>VM: 8. 処理完了
Complete + deactivate UC ``` -アプリケーションの固有ロジックを格納するディレクトリで、プロジェクトの核心になる層です。このテンプレートでは `callback` で、背景に全画面のグラデーション描画を行なっています。 `event` ディレクトリは各ページのイベント処理関数が管理されています。 `event` ディレクトリのクラスがユーザーからの `InputUseCase` の責務を担っています。 +## 📋 設計原則 / Design Principles + +### 1. 依存関係の方向 / Dependency Direction -This directory stores the application-specific logic and is the core layer of the project. -The `callback` generates the background for all screens, and the `event` directory handles events for each page. -The classes in the `event` directory are responsible for `InputUseCase` from User. +```mermaid +graph LR + View["View Layer"] --> Application["Application Layer"] + Application --> Domain["Domain Layer"] + Application --> Infrastructure["Infrastructure Layer"] + + style Domain fill:#e8f5e9,stroke:#1b5e20 + style Application fill:#f3e5f5,stroke:#4a148c + style Infrastructure fill:#fce4ec,stroke:#880e4f + style View fill:#e3f2fd,stroke:#0d47a1 +``` -### Infrastructure +- **Application層** は **Domain層** と **Infrastructure層** に依存 +- **Domain層** は何にも依存しない(最も安定) +- **Infrastructure層** は **Interface層** を実装 + +### 2. インターフェース駆動 / Interface-Driven + +すべての層間通信はインターフェースを経由: + +All inter-layer communication goes through interfaces: + +```typescript +// ✅ 良い例: インターフェースに依存 +import type { IDraggable } from "@/interface/IDraggable"; +export class StartDragUseCase { + execute(target: IDraggable): void { ... } +} + +// ❌ 悪い例: 具象クラスに依存 +import { HomeBtnMolecule } from "@/ui/component/molecule/HomeBtnMolecule"; +export class StartDragUseCase { + execute(target: HomeBtnMolecule): void { ... } +} +``` + +### 3. 単一責任の原則 / Single Responsibility Principle + +各クラスは1つの明確な責務のみを持ちます。 + +Each class has one clear responsibility. + +```typescript +// ✅ 良い例: 単一の責務 +export class StartDragUseCase { + execute(target: IDraggable): void { + target.startDrag(); + } +} + +export class StopDragUseCase { + execute(target: IDraggable): void { + target.stopDrag(); + } +} +``` + +## 🆕 新しい機能の追加方法 / Adding New Features + +### 1. UseCase(Application層)の追加 ```sh -infrastructure -└── repository +# 1. ディレクトリ作成 +model/application/{screen-name}/usecase/ + +# 2. UseCaseファイル作成 +model/application/{screen-name}/usecase/YourUseCase.ts + +# 3. インターフェース定義(必要に応じて) +interface/IYourInterface.ts ``` -外部へのアクセスロジックを格納するディレクトリです。データベースからの情報であれば `entity` ディレクトリを作成して可変可能オブジェクトとして運用し、可変想定がないオブジェクトなどはデータ転送オブジェクト(DTO)として `dto` ディレクトリにそれぞれ責務を分散させるのが良いかもしれません。 +### 2. Domain Logicの追加 + +```sh +# 1. ディレクトリ作成 +model/domain/{feature-name}/ -This directory stores logic for external access. For data retrieved from a database, you might consider creating an `entity` directory to manage mutable objects, while objects that are not meant to change can have their responsibilities distributed into a `dto` directory as data transfer objects (DTOs). +# 2. ドメインロジック作成 +model/domain/{feature-name}/YourDomainLogic.ts +model/domain/{feature-name}/service/YourService.ts +``` -### UI(User Interface) +### 3. Repositoryの追加 ```sh -ui -└── component - ├── atom - └── template - ├── top - └── home +# 1. インターフェース定義 +interface/IYourResponse.ts + +# 2. Repository作成 +model/infrastructure/repository/YourRepository.ts ``` -アトミックデザインを意識したディレクトリ構成になってます。`atom` ディレクトリに最小の表示要素を作成して、`template` ディレクトリで各atomの要素を呼び出しページのレイアウトを作成してます。今回は `template` 内のクラスが `UseCase` の責務も担っています。`application` ディレクトリへのアクセスは `template` 内のクラスに制限する想定です。 +## ✅ ベストプラクティス / Best Practices + +1. **インターフェース優先** - 常にインターフェースに依存 +2. **1クラス1責務** - UseCaseは単一の目的のみ +3. **executeメソッド** - UseCaseのエントリーポイントを統一 +4. **エラーハンドリング** - Infrastructure層で適切に処理 +5. **型安全性** - `any`型を避ける +6. **ドキュメント** - JSDocで処理内容を明記 +7. **テスト** - 各層を独立してテスト可能に + +## 🔗 関連ドキュメント / Related Documentation -The directory structure is designed with atomic design in mind. Minimal display elements are created in the `atom` directory, and in the `template` directory, these atom elements are assembled to build the page layout. In this case, the classes in the `template` directory also carry the responsibilities of the `UseCase`. Access to the `application` directory is intended to be restricted to classes within the `template` directory. \ No newline at end of file +- [../ARCHITECTURE.md](../../ARCHITECTURE.md) - アーキテクチャ全体の説明 +- [application/README.md](./application/README.md) - Application層の詳細 +- [domain/README.md](./domain/README.md) - Domain層の詳細 +- [infrastructure/README.md](./infrastructure/README.md) - Infrastructure層の詳細 +- [../interface/README.md](../interface/README.md) - インターフェース定義 +- [../view/README.md](../view/README.md) - View層の説明 +- [../ui/README.md](../ui/README.md) - UIコンポーネント \ No newline at end of file diff --git a/template/src/model/application/README.md b/template/src/model/application/README.md new file mode 100644 index 0000000..6185855 --- /dev/null +++ b/template/src/model/application/README.md @@ -0,0 +1,416 @@ +# Application Layer + +アプリケーション層のディレクトリです。ビジネスロジックを実装するUseCaseを格納します。 + +Directory for the Application layer. Stores UseCases that implement business logic. + +## 役割 / Role + +Application層は、ユーザーのアクションに対応するビジネスロジックを提供します。この層は以下の責務を持ちます: + +The Application layer provides business logic corresponding to user actions. This layer has the following responsibilities: + +- ✅ **ビジネスロジックの実装** - UseCaseとして定義 +- ✅ **ドメイン層の調整** - 複数のドメインロジックを組み合わせる +- ✅ **トランザクション管理** - 一連の処理の整合性を保証 +- ✅ **外部サービスとの連携** - Repository経由でデータアクセス +- ❌ **UI操作** - View層の責務 +- ❌ **永続化の詳細** - Infrastructure層の責務 + +## ディレクトリ構造 / Directory Structure + +``` +application/ +├── home/ +│ └── usecase/ +│ ├── StartDragUseCase.ts +│ ├── StopDragUseCase.ts +│ └── CenterTextFieldUseCase.ts +└── top/ + └── usecase/ + └── NavigateToViewUseCase.ts +``` + +各画面ごとにディレクトリを作成し、その中に `usecase` ディレクトリを配置します。 + +Create a directory for each screen, and place the `usecase` directory within it. + +## UseCase Pattern + +UseCaseは、1つのユーザーアクションに対して1つのクラスを作成します。 + +Create one UseCase class for each user action. + +### UseCaseの特徴 / UseCase Characteristics + +1. **単一責任** - 1つの明確な目的を持つ +2. **再利用可能** - 異なるViewModelから呼び出せる +3. **テスタブル** - 独立してテスト可能 +4. **インターフェース指向** - 抽象に依存する + +### Example: StartDragUseCase + +```typescript +import type { IDraggable } from "@/interface/IDraggable"; + +/** + * @description ドラッグ開始のユースケース + * Use case for starting drag + * + * @class + */ +export class StartDragUseCase +{ + /** + * @description ドラッグ可能なオブジェクトのドラッグを開始する + * Start dragging a draggable object + * + * @param {IDraggable} target + * @return {void} + * @method + * @public + */ + execute (target: IDraggable): void + { + // ビジネスルールを実装 + // 例: ドラッグ可能かチェック、ログ記録など + + target.startDrag(); + } +} +``` + +### Example: NavigateToViewUseCase + +```typescript +import type { ViewName } from "@/interface/IViewName"; +import { app } from "@next2d/framework"; + +/** + * @description 画面遷移のユースケース + * Use case for navigating to a view + * + * @class + */ +export class NavigateToViewUseCase +{ + /** + * @description 指定された画面に遷移する + * Navigate to the specified view + * + * @param {ViewName} viewName + * @return {Promise} + * @method + * @public + */ + async execute (viewName: ViewName): Promise + { + // ビジネスルール: 遷移前の検証など + // 例: 未保存データのチェック、権限確認など + + await app.gotoView(viewName); + } +} +``` + +**ポイント / Key Points:** +- `ViewName` 型を使用することで、存在しない画面名を指定するとコンパイルエラーになる +- 型安全な画面遷移を実現 + +### Example: CenterTextFieldUseCase + +```typescript +import type { ITextField } from "@/interface/ITextField"; + +/** + * @description テキストフィールド中央揃えのユースケース + * Use case for centering text field + * + * @class + */ +export class CenterTextFieldUseCase +{ + /** + * @description テキストフィールドを画面中央に配置する + * Center the text field on the screen + * + * @param {ITextField} textField + * @param {number} stageWidth - ステージの幅 / Stage width + * @return {void} + * @method + * @public + */ + execute (textField: ITextField, stageWidth: number): void + { + // ビジネスロジック: 中央配置の計算 + textField.x = (stageWidth - textField.width) / 2; + } +} +``` + +**ポイント / Key Points:** +- `config` に直接依存せず、`stageWidth` を引数で受け取る +- テスタビリティが向上(任意の幅でテスト可能) + +## UseCaseの設計原則 / UseCase Design Principles + +### 1. インターフェースに依存 / Depend on Interfaces + +具象クラスではなく、インターフェースに依存します。 + +Depend on interfaces, not concrete classes. + +```typescript +// ✅ 良い例: インターフェースに依存 +export class StartDragUseCase { + execute(target: IDraggable): void { + target.startDrag(); + } +} + +// ❌ 悪い例: 具象クラスに依存 +export class StartDragUseCase { + execute(target: HomeBtnMolecule): void { // NG: UIコンポーネントに依存 + target.startDrag(); + } +} +``` + +### 2. 単一責任の原則 / Single Responsibility Principle + +1つのUseCaseは1つの責務のみを持ちます。 + +One UseCase has only one responsibility. + +```typescript +// ✅ 良い例: 単一の責務 +export class StartDragUseCase { + execute(target: IDraggable): void { + target.startDrag(); + } +} + +export class StopDragUseCase { + execute(target: IDraggable): void { + target.stopDrag(); + } +} + +// ❌ 悪い例: 複数の責務 +export class DragUseCase { + start(target: IDraggable): void { ... } + stop(target: IDraggable): void { ... } + validate(target: IDraggable): boolean { ... } + log(message: string): void { ... } // NG: 責務が多すぎる +} +``` + +### 3. 副作用の明示 / Explicit Side Effects + +副作用(状態変更、外部API呼び出しなど)を明確にします。 + +Make side effects (state changes, external API calls, etc.) explicit. + +```typescript +// ✅ 良い例: 非同期処理を明示 +export class FetchDataUseCase { + async execute(id: string): Promise { // async/await + const data = await Repository.get(id); + return data; + } +} + +// ✅ 良い例: 同期処理 +export class ValidateInputUseCase { + execute(input: string): boolean { // 同期 + return input.length > 0; + } +} +``` + +### 4. エラーハンドリング / Error Handling + +適切にエラーを処理し、上位層に伝播させます。 + +Handle errors appropriately and propagate to upper layers. + +```typescript +export class FetchUserDataUseCase { + async execute(userId: string): Promise { + try { + // Repositoryでもエラーハンドリングされているが + // UseCase層でも必要に応じて処理 + const data = await UserRepository.get(userId); + + // ビジネスルールのバリデーション + if (!this.validateUserData(data)) { + throw new Error('Invalid user data'); + } + + return data; + } catch (error) { + // ログ記録 + console.error('Failed to fetch user data:', error); + + // 上位層にエラーを伝播 + throw error; + } + } + + private validateUserData(data: UserData): boolean { + // バリデーションロジック + return data !== null && data.id !== undefined; + } +} +``` + +## UseCase と Repository の連携 / UseCase and Repository Collaboration + +UseCaseは、データアクセスが必要な場合はRepositoryを使用します。 + +UseCases use Repositories when data access is needed. + +```typescript +import { HomeTextRepository } from "@/model/infrastructure/repository/HomeTextRepository"; +import type { IHomeTextResponse } from "@/interface/IHomeTextResponse"; + +export class FetchHomeTextUseCase { + /** + * @description Home画面のテキストを取得 + * Fetch text for Home screen + * + * @return {Promise} + * @method + * @public + */ + async execute(): Promise { + try { + // Repositoryでデータ取得 + const data = await HomeTextRepository.get(); + + // ビジネスロジック: データの加工や検証 + // 例: キャッシュの確認、デフォルト値の設定など + + return data; + } catch (error) { + console.error('Failed to fetch home text:', error); + + // フォールバック: デフォルト値を返す + return { word: 'Hello, World!' }; + } + } +} +``` + +## 複数のUseCaseの組み合わせ / Combining Multiple UseCases + +複雑な処理は、複数のUseCaseを組み合わせて実装します。 + +Implement complex processes by combining multiple UseCases. + +```typescript +export class InitializeHomeScreenUseCase { + private readonly fetchTextUseCase: FetchHomeTextUseCase; + private readonly centerTextUseCase: CenterTextFieldUseCase; + + constructor() { + this.fetchTextUseCase = new FetchHomeTextUseCase(); + this.centerTextUseCase = new CenterTextFieldUseCase(); + } + + async execute(textField: ITextField): Promise { + // 1. データ取得 + const data = await this.fetchTextUseCase.execute(); + + // 2. テキスト設定(ViewModelで実施) + + // 3. 中央配置 + this.centerTextUseCase.execute(textField); + } +} +``` + +## テスト / Testing + +UseCaseは独立してテスト可能です。 + +UseCases can be tested independently. + +```typescript +import { StartDragUseCase } from "./StartDragUseCase"; +import type { IDraggable } from "@/interface/IDraggable"; + +describe('StartDragUseCase', () => { + test('should call startDrag on target', () => { + // モックオブジェクトを作成 + const mockDraggable: IDraggable = { + startDrag: jest.fn(), + stopDrag: jest.fn() + }; + + // UseCaseを実行 + const useCase = new StartDragUseCase(); + useCase.execute(mockDraggable); + + // startDragが呼ばれたか検証 + expect(mockDraggable.startDrag).toHaveBeenCalled(); + }); +}); +``` + +## 新しいUseCaseの作成 / Creating New UseCases + +### 手順 / Steps + +1. **ユーザーアクションを特定** - どのような操作か +2. **インターフェースを定義** - 必要に応じて `interface/` に追加 +3. **UseCaseクラスを作成** - `execute` メソッドを実装 +4. **ViewModelで使用** - コンストラクタで依存性注入 +5. **テストを作成** - ユニットテストを追加 + +### テンプレート / Template + +```typescript +import type { IYourInterface } from "@/interface/IYourInterface"; + +/** + * @description [UseCaseの説明] + * [UseCase description] + * + * @class + */ +export class YourUseCase +{ + /** + * @description [処理の説明] + * [Process description] + * + * @param {ParamType} param + * @return {ReturnType} + * @method + * @public + */ + execute (param: ParamType): ReturnType + { + // ビジネスロジックを実装 + + return result; + } +} +``` + +## ベストプラクティス / Best Practices + +1. **1クラス1責務** - 各UseCaseは明確な1つの目的を持つ +2. **executeメソッド** - UseCaseのエントリーポイントは `execute` に統一 +3. **インターフェース優先** - 具象クラスへの依存を避ける +4. **テスタブル** - 依存をモックに差し替え可能にする +5. **ドキュメント** - JSDocで処理内容を明記 + +## 関連ドキュメント / Related Documentation + +- [ARCHITECTURE.md](../../../ARCHITECTURE.md) - アーキテクチャ全体の説明 +- [../domain/README.md](../domain/README.md) - Domain層の説明 +- [../infrastructure/README.md](../infrastructure/README.md) - Infrastructure層の説明 +- [../../interface/README.md](../../interface/README.md) - インターフェース定義 +- [../../view/README.md](../../view/README.md) - View層の説明 diff --git a/template/src/model/application/home/usecase/CenterTextFieldUseCase.test.ts b/template/src/model/application/home/usecase/CenterTextFieldUseCase.test.ts new file mode 100644 index 0000000..ddeb22e --- /dev/null +++ b/template/src/model/application/home/usecase/CenterTextFieldUseCase.test.ts @@ -0,0 +1,108 @@ +import { describe, it, expect } from "vitest"; +import { CenterTextFieldUseCase } from "./CenterTextFieldUseCase"; +import type { ITextField } from "@/interface/ITextField"; + +/** + * @description CenterTextFieldUseCase のテスト + * Tests for CenterTextFieldUseCase + */ +describe("CenterTextFieldUseCase", () => { + /** + * @description execute メソッドのテスト + * Test for execute method + */ + describe("execute", () => { + it("テキストフィールドが中央に配置されること", () => { + const textField: ITextField = { + width: 200, + x: 0 + }; + + const useCase = new CenterTextFieldUseCase(); + useCase.execute(textField, 800); + + // 中央位置: (800 - 200) / 2 = 300 + expect(textField.x).toBe(300); + }); + + it("幅が異なる場合でも正しく中央に配置されること", () => { + const textField: ITextField = { + width: 100, + x: 0 + }; + + const useCase = new CenterTextFieldUseCase(); + useCase.execute(textField, 500); + + // 中央位置: (500 - 100) / 2 = 200 + expect(textField.x).toBe(200); + }); + + it("テキストフィールドの幅がステージ幅と同じ場合、x は 0 になること", () => { + const textField: ITextField = { + width: 800, + x: 100 + }; + + const useCase = new CenterTextFieldUseCase(); + useCase.execute(textField, 800); + + // 中央位置: (800 - 800) / 2 = 0 + expect(textField.x).toBe(0); + }); + + it("テキストフィールドの幅がステージ幅より大きい場合、x は負の値になること", () => { + const textField: ITextField = { + width: 1000, + x: 0 + }; + + const useCase = new CenterTextFieldUseCase(); + useCase.execute(textField, 800); + + // 中央位置: (800 - 1000) / 2 = -100 + expect(textField.x).toBe(-100); + }); + + it("小数点を含む計算結果が正しいこと", () => { + const textField: ITextField = { + width: 101, + x: 0 + }; + + const useCase = new CenterTextFieldUseCase(); + useCase.execute(textField, 800); + + // 中央位置: (800 - 101) / 2 = 349.5 + expect(textField.x).toBe(349.5); + }); + + it("既存の x 座標が上書きされること", () => { + const textField: ITextField = { + width: 200, + x: 999 + }; + + const useCase = new CenterTextFieldUseCase(); + useCase.execute(textField, 800); + + expect(textField.x).toBe(300); + }); + }); + + /** + * @description インスタンス生成のテスト + * Test for instance creation + */ + describe("Instance Creation / インスタンス生成", () => { + it("インスタンスが正常に生成されること", () => { + const useCase = new CenterTextFieldUseCase(); + expect(useCase).toBeInstanceOf(CenterTextFieldUseCase); + }); + + it("execute メソッドを持つこと", () => { + const useCase = new CenterTextFieldUseCase(); + expect(typeof useCase.execute).toBe("function"); + }); + }); +}); diff --git a/template/src/model/application/home/usecase/CenterTextFieldUseCase.ts b/template/src/model/application/home/usecase/CenterTextFieldUseCase.ts new file mode 100644 index 0000000..2be55cd --- /dev/null +++ b/template/src/model/application/home/usecase/CenterTextFieldUseCase.ts @@ -0,0 +1,24 @@ +import type { ITextField } from "@/interface/ITextField"; + +/** + * @description テキストフィールド中央揃えのユースケース + * Use case for centering text field + * + * @class + */ +export class CenterTextFieldUseCase { + /** + * @description テキストフィールドを画面中央に配置する + * Center the text field on the screen + * + * @param {ITextField} textField + * @param {number} stageWidth - ステージの幅 / Stage width + * @return {void} + * @method + * @public + */ + execute (textField: ITextField, stageWidth: number): void + { + textField.x = (stageWidth - textField.width) / 2; + } +} diff --git a/template/src/model/application/home/usecase/StartDragUseCase.test.ts b/template/src/model/application/home/usecase/StartDragUseCase.test.ts new file mode 100644 index 0000000..ec5bec0 --- /dev/null +++ b/template/src/model/application/home/usecase/StartDragUseCase.test.ts @@ -0,0 +1,70 @@ +import { describe, it, expect, vi } from "vitest"; +import { StartDragUseCase } from "./StartDragUseCase"; +import type { IDraggable } from "@/interface/IDraggable"; + +/** + * @description StartDragUseCase のテスト + * Tests for StartDragUseCase + */ +describe("StartDragUseCase", () => { + /** + * @description execute メソッドのテスト + * Test for execute method + */ + describe("execute", () => { + it("target の startDrag メソッドが呼び出されること", () => { + const mockDraggable: IDraggable = { + startDrag: vi.fn(), + stopDrag: vi.fn() + }; + + const useCase = new StartDragUseCase(); + useCase.execute(mockDraggable); + + expect(mockDraggable.startDrag).toHaveBeenCalled(); + expect(mockDraggable.startDrag).toHaveBeenCalledTimes(1); + }); + + it("stopDrag メソッドは呼び出されないこと", () => { + const mockDraggable: IDraggable = { + startDrag: vi.fn(), + stopDrag: vi.fn() + }; + + const useCase = new StartDragUseCase(); + useCase.execute(mockDraggable); + + expect(mockDraggable.stopDrag).not.toHaveBeenCalled(); + }); + + it("複数回呼び出した場合、その都度 startDrag が呼び出されること", () => { + const mockDraggable: IDraggable = { + startDrag: vi.fn(), + stopDrag: vi.fn() + }; + + const useCase = new StartDragUseCase(); + useCase.execute(mockDraggable); + useCase.execute(mockDraggable); + useCase.execute(mockDraggable); + + expect(mockDraggable.startDrag).toHaveBeenCalledTimes(3); + }); + }); + + /** + * @description インスタンス生成のテスト + * Test for instance creation + */ + describe("Instance Creation / インスタンス生成", () => { + it("インスタンスが正常に生成されること", () => { + const useCase = new StartDragUseCase(); + expect(useCase).toBeInstanceOf(StartDragUseCase); + }); + + it("execute メソッドを持つこと", () => { + const useCase = new StartDragUseCase(); + expect(typeof useCase.execute).toBe("function"); + }); + }); +}); diff --git a/template/src/model/application/home/usecase/StartDragUseCase.ts b/template/src/model/application/home/usecase/StartDragUseCase.ts new file mode 100644 index 0000000..9db4570 --- /dev/null +++ b/template/src/model/application/home/usecase/StartDragUseCase.ts @@ -0,0 +1,23 @@ +import type { IDraggable } from "@/interface/IDraggable"; + +/** + * @description ドラッグ開始のユースケース + * Use case for starting drag + * + * @class + */ +export class StartDragUseCase { + /** + * @description ドラッグ可能なオブジェクトのドラッグを開始する + * Start dragging a draggable object + * + * @param {IDraggable} target + * @return {void} + * @method + * @public + */ + execute (target: IDraggable): void + { + target.startDrag(); + } +} diff --git a/template/src/model/application/home/usecase/StopDragUseCase.test.ts b/template/src/model/application/home/usecase/StopDragUseCase.test.ts new file mode 100644 index 0000000..b28ab5b --- /dev/null +++ b/template/src/model/application/home/usecase/StopDragUseCase.test.ts @@ -0,0 +1,70 @@ +import { describe, it, expect, vi } from "vitest"; +import { StopDragUseCase } from "./StopDragUseCase"; +import type { IDraggable } from "@/interface/IDraggable"; + +/** + * @description StopDragUseCase のテスト + * Tests for StopDragUseCase + */ +describe("StopDragUseCase", () => { + /** + * @description execute メソッドのテスト + * Test for execute method + */ + describe("execute", () => { + it("target の stopDrag メソッドが呼び出されること", () => { + const mockDraggable: IDraggable = { + startDrag: vi.fn(), + stopDrag: vi.fn() + }; + + const useCase = new StopDragUseCase(); + useCase.execute(mockDraggable); + + expect(mockDraggable.stopDrag).toHaveBeenCalled(); + expect(mockDraggable.stopDrag).toHaveBeenCalledTimes(1); + }); + + it("startDrag メソッドは呼び出されないこと", () => { + const mockDraggable: IDraggable = { + startDrag: vi.fn(), + stopDrag: vi.fn() + }; + + const useCase = new StopDragUseCase(); + useCase.execute(mockDraggable); + + expect(mockDraggable.startDrag).not.toHaveBeenCalled(); + }); + + it("複数回呼び出した場合、その都度 stopDrag が呼び出されること", () => { + const mockDraggable: IDraggable = { + startDrag: vi.fn(), + stopDrag: vi.fn() + }; + + const useCase = new StopDragUseCase(); + useCase.execute(mockDraggable); + useCase.execute(mockDraggable); + useCase.execute(mockDraggable); + + expect(mockDraggable.stopDrag).toHaveBeenCalledTimes(3); + }); + }); + + /** + * @description インスタンス生成のテスト + * Test for instance creation + */ + describe("Instance Creation / インスタンス生成", () => { + it("インスタンスが正常に生成されること", () => { + const useCase = new StopDragUseCase(); + expect(useCase).toBeInstanceOf(StopDragUseCase); + }); + + it("execute メソッドを持つこと", () => { + const useCase = new StopDragUseCase(); + expect(typeof useCase.execute).toBe("function"); + }); + }); +}); diff --git a/template/src/model/application/home/usecase/StopDragUseCase.ts b/template/src/model/application/home/usecase/StopDragUseCase.ts new file mode 100644 index 0000000..9065b54 --- /dev/null +++ b/template/src/model/application/home/usecase/StopDragUseCase.ts @@ -0,0 +1,23 @@ +import type { IDraggable } from "@/interface/IDraggable"; + +/** + * @description ドラッグ停止のユースケース + * Use case for stopping drag + * + * @class + */ +export class StopDragUseCase { + /** + * @description ドラッグ可能なオブジェクトのドラッグを停止する + * Stop dragging a draggable object + * + * @param {IDraggable} target + * @return {void} + * @method + * @public + */ + execute (target: IDraggable): void + { + target.stopDrag(); + } +} diff --git a/template/src/model/application/top/usecase/NavigateToViewUseCase.test.ts b/template/src/model/application/top/usecase/NavigateToViewUseCase.test.ts new file mode 100644 index 0000000..6f1fc39 --- /dev/null +++ b/template/src/model/application/top/usecase/NavigateToViewUseCase.test.ts @@ -0,0 +1,69 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { NavigateToViewUseCase } from "./NavigateToViewUseCase"; +import type { ViewName } from "@/interface/IViewName"; + +// @next2d/framework モジュールをモック +vi.mock("@next2d/framework", () => ({ + app: { + gotoView: vi.fn().mockResolvedValue(undefined) + } +})); + +import { app } from "@next2d/framework"; + +/** + * @description NavigateToViewUseCase のテスト + * Tests for NavigateToViewUseCase + */ +describe("NavigateToViewUseCase", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + /** + * @description execute メソッドのテスト + * Test for execute method + */ + describe("execute", () => { + it("app.gotoView が指定された viewName で呼び出されること", async () => { + const useCase = new NavigateToViewUseCase(); + const viewName: ViewName = "home"; + + await useCase.execute(viewName); + + expect(app.gotoView).toHaveBeenCalledWith("home"); + expect(app.gotoView).toHaveBeenCalledTimes(1); + }); + + it("'top' ビューに遷移できること", async () => { + const useCase = new NavigateToViewUseCase(); + const viewName: ViewName = "top"; + + await useCase.execute(viewName); + + expect(app.gotoView).toHaveBeenCalledWith("top"); + }); + + it("非同期処理が正常に完了すること", async () => { + const useCase = new NavigateToViewUseCase(); + + await expect(useCase.execute("home")).resolves.toBeUndefined(); + }); + }); + + /** + * @description インスタンス生成のテスト + * Test for instance creation + */ + describe("Instance Creation / インスタンス生成", () => { + it("インスタンスが正常に生成されること", () => { + const useCase = new NavigateToViewUseCase(); + expect(useCase).toBeInstanceOf(NavigateToViewUseCase); + }); + + it("execute メソッドを持つこと", () => { + const useCase = new NavigateToViewUseCase(); + expect(typeof useCase.execute).toBe("function"); + }); + }); +}); diff --git a/template/src/model/application/top/usecase/NavigateToViewUseCase.ts b/template/src/model/application/top/usecase/NavigateToViewUseCase.ts new file mode 100644 index 0000000..f77a203 --- /dev/null +++ b/template/src/model/application/top/usecase/NavigateToViewUseCase.ts @@ -0,0 +1,24 @@ +import type { ViewName } from "@/interface/IViewName"; +import { app } from "@next2d/framework"; + +/** + * @description 画面遷移のユースケース + * Use case for navigating to a view + * + * @class + */ +export class NavigateToViewUseCase { + /** + * @description 指定された画面に遷移する + * Navigate to the specified view + * + * @param {ViewName} viewName + * @return {Promise} + * @method + * @public + */ + async execute (viewName: ViewName): Promise + { + await app.gotoView(viewName); + } +} diff --git a/template/src/model/domain/README.md b/template/src/model/domain/README.md new file mode 100644 index 0000000..03a9f88 --- /dev/null +++ b/template/src/model/domain/README.md @@ -0,0 +1,564 @@ +# Domain Layer + +ドメイン層のディレクトリです。アプリケーションのコアとなるビジネスロジックを実装します。 + +Directory for the Domain layer. Implements the core business logic of the application. + +## 役割 / Role + +Domain層は、アプリケーションの核心となるビジネスルールを保持します。この層は以下の特徴を持ちます: + +The Domain layer holds the core business rules of the application. This layer has the following characteristics: + +- ✅ **純粋なビジネスロジック** - フレームワークに依存しない(※注: 一部Next2D固有機能を使用) +- ✅ **再利用可能なロジック** - アプリケーション全体で利用される +- ✅ **ドメイン知識の表現** - ビジネスルールを明確に表現 +- ✅ **安定性** - 外部の変更に影響されにくい +- ❌ **UI依存** - View層に依存しない +- ❌ **永続化の詳細** - Infrastructure層に依存しない +- ❌ **フレームワーク依存** - 特定のフレームワークに依存しない + +## ディレクトリ構造 / Directory Structure + +``` +domain/ +└── callback/ + └── Background/ + ├── Background.ts + └── service/ + ├── BackgroundDrawService.ts + └── BackgroundChangeScaleService.ts +``` + +将来的に以下のような拡張も可能です: + +Future extensions are possible, such as: + +``` +domain/ +├── callback/ # コールバック処理 +│ └── Background/ +├── service/ # ドメインサービス +│ ├── ValidationService.ts +│ └── CalculationService.ts +├── entity/ # エンティティ +│ └── User.ts +└── value-object/ # 値オブジェクト + └── Email.ts +``` + +## ドメインの概念 / Domain Concepts + +### 1. Callback - コールバック処理 + +アプリケーション全体で使用される共通処理です。 + +Common processes used throughout the application. + +#### Example: Background + +```typescript +import { config } from "@/config/Config"; +import { app } from "@next2d/framework"; +import { Shape, stage } from "@next2d/display"; +import { Event } from "@next2d/events"; + +/** + * @description グラデーション背景 + * Gradient background + * + * @class + */ +export class Background +{ + public readonly shape: Shape; + + constructor () + { + this.shape = new Shape(); + + // リサイズイベントをリスン + stage.addEventListener(Event.RESIZE, (): void => + { + backgroundDrawService(this); + backgroundChangeScaleService(this); + }); + } + + /** + * @description 背景のShapeを表示されるviewにセット + * Set the background shape to the view to be displayed + * + * @return {void} + * @method + * @public + */ + execute (): void + { + const context = app.getContext(); + const view = context.view; + if (!view) { + return; + } + + const shape = this.shape; + if (config.stage.width !== shape.width + || config.stage.height !== shape.height + ) { + backgroundDrawService(this); + backgroundChangeScaleService(this); + } + + view.addChildAt(shape, 0); + } +} +``` + +### 2. Domain Service - ドメインサービス + +複数のエンティティにまたがるビジネスロジックを実装します。 + +Implements business logic that spans multiple entities. + +#### Example: BackgroundDrawService + +```typescript +import type { Background } from "../../Background"; +import { config } from "@/config/Config"; +import { Matrix } from "@next2d/geom"; + +/** + * @description 背景のグラデーション描画を実行 + * Execute background gradient drawing + * + * @param {Background} background + * @return {void} + * @method + * @protected + */ +export const execute = (background: Background): void => +{ + const width = config.stage.width; + const height = config.stage.height; + + const matrix = new Matrix(); + matrix.createGradientBox(height, width, Math.PI / 2, 0, 0); + + background + .shape + .graphics + .clear() + .beginGradientFill( + "linear", + ["#1461A0", "#ffffff"], + [0.6, 1], + [0, 255], + matrix + ) + .drawRect(0, 0, width, height) + .endFill(); +}; +``` + +#### Example: BackgroundChangeScaleService + +```typescript +import type { Background } from "../../Background"; +import { config } from "@/config/Config"; +import { stage } from "@next2d/display"; + +/** + * @description 表示範囲に合わせてShapeを拡大・縮小 + * Scale the shape to fit the display area + * + * @param {Background} background + * @return {void} + * @method + * @protected + */ +export const execute = (background: Background): void => +{ + const width = config.stage.width; + const height = config.stage.height; + const scale = stage.rendererScale; + + const shape = background.shape; + const tx = (stage.rendererWidth - stage.stageWidth * scale) / 2; + if (tx) { + shape.scaleX = (width + tx * 2 / scale) / width; + shape.x = -tx / scale; + } + + const ty = (stage.rendererHeight - stage.stageHeight * scale) / 2; + if (ty) { + shape.scaleY = (height + ty * 2 / scale) / height; + shape.y = -ty / scale; + } +}; +``` + +## ドメイン層の設計原則 / Domain Layer Design Principles + +### 1. フレームワーク非依存 / Framework Independence + +可能な限り、特定のフレームワークに依存しない実装を心がけます。 + +Strive for implementation that doesn't depend on specific frameworks as much as possible. + +```typescript +// ✅ 良い例: ビジネスロジックのみ +export class ValidationService { + static validate(input: string): boolean { + // 純粋なロジック + return input.length >= 3 && input.length <= 50; + } +} + +// ⚠️ 注意: Next2D固有の機能を使う場合は明確に +// このプロジェクトではNext2Dの描画機能(Shape, stage等)を +// Domain層で使用することを許容しています。 +// これはNext2Dフレームワーク固有の設計判断であり、 +// 純粋なクリーンアーキテクチャからは逸脱していますが、 +// 描画ロジックの再利用性を優先した設計です。 +export class Background { + // Next2Dのshapeを使用(このプロジェクトでは許容) + public readonly shape: Shape; +} +``` + +### 2. ビジネスルールの表現 / Express Business Rules + +ビジネスルールを明確に表現します。 + +Express business rules clearly. + +```typescript +// ✅ 良い例: ビジネスルールが明確 +export class User { + constructor( + public readonly id: string, + public readonly name: string, + public readonly age: number + ) { + // ビジネスルール: 年齢は0以上150以下 + if (age < 0 || age > 150) { + throw new Error('Invalid age'); + } + } + + // ビジネスルール: 成人判定 + isAdult(): boolean { + return this.age >= 18; + } +} + +// ❌ 悪い例: ビジネスルールが不明確 +export class User { + id: string; + name: string; + age: number; + + check(): boolean { // 何をチェックするのか不明 + return this.age >= 18; + } +} +``` + +### 3. 副作用の最小化 / Minimize Side Effects + +純粋関数を心がけ、副作用を最小限に抑えます。 + +Strive for pure functions and minimize side effects. + +```typescript +// ✅ 良い例: 純粋関数 +export class Calculator { + static add(a: number, b: number): number { + return a + b; // 副作用なし + } + + static multiply(a: number, b: number): number { + return a * b; // 副作用なし + } +} + +// ⚠️ 副作用がある場合は明示 +export class Background { + execute(): void { + // 副作用: DOMを操作 + // この場合は、メソッド名や説明で明確にする + const view = app.getContext().view; + view.addChildAt(this.shape, 0); + } +} +``` + +### 4. 不変性 / Immutability + +可能な限り、不変なオブジェクトを使用します。 + +Use immutable objects as much as possible. + +```typescript +// ✅ 良い例: 不変オブジェクト +export class Point { + constructor( + public readonly x: number, + public readonly y: number + ) {} + + // 新しいインスタンスを返す + move(dx: number, dy: number): Point { + return new Point(this.x + dx, this.y + dy); + } +} + +// ❌ 悪い例: 可変オブジェクト +export class MutablePoint { + constructor( + public x: number, // 変更可能 + public y: number + ) {} + + // 自身を変更 + move(dx: number, dy: number): void { + this.x += dx; + this.y += dy; + } +} +``` + +## ドメインサービスの実装パターン / Domain Service Implementation Patterns + +### 関数型スタイル / Functional Style + +単純なロジックは関数としてexportします。 + +Export simple logic as functions. + +```typescript +/** + * @description バリデーション処理 + * Validation process + * + * @param {string} input + * @return {boolean} + */ +export const validateInput = (input: string): boolean => +{ + return input.length >= 3 && input.length <= 50; +}; + +/** + * @description 計算処理 + * Calculation process + * + * @param {number} a + * @param {number} b + * @return {number} + */ +export const calculate = (a: number, b: number): number => +{ + return (a + b) * 2; +}; +``` + +### クラス型スタイル / Class-based Style + +複雑なロジックや状態を持つ場合はクラスを使用します。 + +Use classes for complex logic or when holding state. + +```typescript +/** + * @description 複雑な計算を行うサービス + * Service for complex calculations + * + * @class + */ +export class CalculationService +{ + private cache: Map = new Map(); + + /** + * @description 計算を実行(キャッシュあり) + * Execute calculation (with cache) + * + * @param {number} a + * @param {number} b + * @return {number} + */ + calculate(a: number, b: number): number + { + const key = `${a}-${b}`; + + if (this.cache.has(key)) { + return this.cache.get(key)!; + } + + const result = this.performCalculation(a, b); + this.cache.set(key, result); + + return result; + } + + private performCalculation(a: number, b: number): number + { + // 複雑な計算ロジック + return (a + b) * 2; + } +} +``` + +## エンティティと値オブジェクト / Entities and Value Objects + +### Entity - エンティティ + +一意の識別子を持つオブジェクトです。 + +Objects with unique identifiers. + +```typescript +/** + * @description ユーザーエンティティ + * User entity + * + * @class + */ +export class User +{ + constructor( + public readonly id: string, + public readonly name: string, + public readonly email: string + ) { + this.validate(); + } + + private validate(): void + { + if (!this.id) { + throw new Error('User ID is required'); + } + if (!this.email.includes('@')) { + throw new Error('Invalid email format'); + } + } + + // エンティティは同一性をIDで判定 + equals(other: User): boolean + { + return this.id === other.id; + } +} +``` + +### Value Object - 値オブジェクト + +属性の値によって識別されるオブジェクトです。 + +Objects identified by their attribute values. + +```typescript +/** + * @description メールアドレス値オブジェクト + * Email address value object + * + * @class + */ +export class Email +{ + private readonly value: string; + + constructor(email: string) + { + this.validate(email); + this.value = email; + } + + private validate(email: string): void + { + if (!email.includes('@')) { + throw new Error('Invalid email format'); + } + } + + getValue(): string + { + return this.value; + } + + // 値オブジェクトは値で同一性を判定 + equals(other: Email): boolean + { + return this.value === other.getValue(); + } +} +``` + +## テスト / Testing + +ドメイン層は最もテストしやすい層です。 + +The Domain layer is the easiest layer to test. + +```typescript +import { validateInput } from "./ValidationService"; + +describe('ValidationService', () => { + test('should return true for valid input', () => { + expect(validateInput('abc')).toBe(true); + expect(validateInput('test')).toBe(true); + }); + + test('should return false for invalid input', () => { + expect(validateInput('ab')).toBe(false); // 短すぎる + expect(validateInput('a'.repeat(51))).toBe(false); // 長すぎる + }); +}); +``` + +## 新しいドメインロジックの作成 / Creating New Domain Logic + +### 手順 / Steps + +1. **ビジネスルールを特定** - どのようなルールか明確にする +2. **適切な場所を選択** - service/entity/value-object +3. **インターフェースを定義** - 必要に応じて +4. **実装** - 純粋なビジネスロジックを記述 +5. **テスト** - ユニットテストを作成 + +### テンプレート / Template + +```typescript +/** + * @description [ドメインロジックの説明] + * [Domain logic description] + * + * @param {ParamType} param + * @return {ReturnType} + * @method + * @public + */ +export const execute = (param: ParamType): ReturnType => +{ + // ビジネスルールの実装 + + return result; +}; +``` + +## ベストプラクティス / Best Practices + +1. **フレームワーク非依存** - 可能な限り純粋なTypeScriptで実装 +2. **ビジネスルール優先** - 技術的な詳細よりもビジネスルールを優先 +3. **テスタブル** - 外部依存を最小限に抑える +4. **不変性** - 可能な限り不変なオブジェクトを使用 +5. **明確な命名** - ビジネス用語を使用した分かりやすい命名 + +## 関連ドキュメント / Related Documentation + +- [ARCHITECTURE.md](../../../ARCHITECTURE.md) - アーキテクチャ全体の説明 +- [../application/README.md](../application/README.md) - Application層の説明 +- [../infrastructure/README.md](../infrastructure/README.md) - Infrastructure層の説明 +- [../../interface/README.md](../../interface/README.md) - インターフェース定義 diff --git a/template/src/model/domain/callback/Background.test.ts b/template/src/model/domain/callback/Background.test.ts new file mode 100644 index 0000000..7a8c00a --- /dev/null +++ b/template/src/model/domain/callback/Background.test.ts @@ -0,0 +1,133 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; + +// @next2d/display モジュールをモック +vi.mock("@next2d/display", () => { + return { + Shape: class MockShape { + graphics = { + clear: vi.fn().mockReturnThis(), + beginGradientFill: vi.fn().mockReturnThis(), + drawRect: vi.fn().mockReturnThis(), + endFill: vi.fn().mockReturnThis() + }; + width = 0; + height = 0; + scaleX = 1; + scaleY = 1; + x = 0; + y = 0; + }, + stage: { + addEventListener: vi.fn(), + rendererScale: 1, + rendererWidth: 240, + rendererHeight: 240, + stageWidth: 240, + stageHeight: 240 + } + }; +}); + +// @next2d/events モジュールをモック +vi.mock("@next2d/events", () => ({ + Event: { + RESIZE: "resize" + } +})); + +// @next2d/framework モジュールをモック +vi.mock("@next2d/framework", () => ({ + app: { + getContext: vi.fn().mockReturnValue({ + view: { + addChildAt: vi.fn() + } + }) + } +})); + +import { Background } from "./Background"; +import { stage } from "@next2d/display"; +import { app } from "@next2d/framework"; + +/** + * @description Background のテスト + * Tests for Background + */ +describe("Background", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + /** + * @description コンストラクタのテスト + * Test for constructor + */ + describe("Constructor / コンストラクタ", () => { + it("インスタンスが正常に生成されること", () => { + const background = new Background(); + expect(background).toBeInstanceOf(Background); + }); + + it("shape プロパティが初期化されること", () => { + const background = new Background(); + expect(background.shape).toBeDefined(); + }); + + it("stage に RESIZE イベントリスナーが登録されること", () => { + new Background(); + expect(stage.addEventListener).toHaveBeenCalled(); + }); + }); + + /** + * @description execute メソッドのテスト + * Test for execute method + */ + describe("execute", () => { + it("execute メソッドが存在すること", () => { + const background = new Background(); + expect(typeof background.execute).toBe("function"); + }); + + it("view が存在する場合、shape が追加されること", () => { + const background = new Background(); + const mockView = { + addChildAt: vi.fn() + }; + + vi.mocked(app.getContext).mockReturnValue({ + view: mockView + } as any); + + background.execute(); + + expect(app.getContext).toHaveBeenCalled(); + }); + + it("view が存在しない場合、早期リターンすること", () => { + const background = new Background(); + + vi.mocked(app.getContext).mockReturnValue({ + view: null + } as any); + + // エラーなく実行されること + expect(() => background.execute()).not.toThrow(); + }); + }); + + /** + * @description shape プロパティのテスト + * Test for shape property + */ + describe("shape Property / shape プロパティ", () => { + it("shape が readonly であること", () => { + const background = new Background(); + const originalShape = background.shape; + + // TypeScriptの型システムで readonly が保証されている + expect(background.shape).toBe(originalShape); + }); + }); +}); diff --git a/template/src/model/domain/callback/Background.ts b/template/src/model/domain/callback/Background.ts index 38e1dda..105181c 100644 --- a/template/src/model/domain/callback/Background.ts +++ b/template/src/model/domain/callback/Background.ts @@ -11,8 +11,7 @@ import { execute as backgroundChangeScaleService } from "./Background/service/Ba * * @class */ -export class Background -{ +export class Background { /** * @type {Shape} * @public diff --git a/template/src/model/domain/callback/Background/service/BackgroundChangeScaleService.test.ts b/template/src/model/domain/callback/Background/service/BackgroundChangeScaleService.test.ts new file mode 100644 index 0000000..b3e7349 --- /dev/null +++ b/template/src/model/domain/callback/Background/service/BackgroundChangeScaleService.test.ts @@ -0,0 +1,114 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { execute } from "./BackgroundChangeScaleService"; + +// @next2d/display モジュールをモック +vi.mock("@next2d/display", () => ({ + stage: { + rendererScale: 1, + rendererWidth: 240, + rendererHeight: 240, + stageWidth: 240, + stageHeight: 240 + } +})); + +/** + * @description BackgroundChangeScaleService のテスト + * Tests for BackgroundChangeScaleService + */ +describe("BackgroundChangeScaleService", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + /** + * @description execute 関数のテスト + * Test for execute function + */ + describe("execute", () => { + it("execute 関数が存在すること", () => { + expect(typeof execute).toBe("function"); + }); + + it("エラーなく実行されること", () => { + const mockBackground = { + shape: { + scaleX: 1, + scaleY: 1, + x: 0, + y: 0 + } + }; + + expect(() => execute(mockBackground as any)).not.toThrow(); + }); + + it("shape のプロパティにアクセスすること", () => { + const mockShape = { + scaleX: 1, + scaleY: 1, + x: 0, + y: 0 + }; + + const mockBackground = { + shape: mockShape + }; + + execute(mockBackground as any); + + // shape へのアクセスが行われること(プロパティが変更される可能性がある) + expect(mockBackground.shape).toBeDefined(); + }); + }); + + /** + * @description スケール計算のテスト + * Test for scale calculation + */ + describe("Scale Calculation / スケール計算", () => { + it("tx が 0 の場合、scaleX と x は変更されないこと", () => { + const mockShape = { + scaleX: 1, + scaleY: 1, + x: 0, + y: 0 + }; + + const mockBackground = { + shape: mockShape + }; + + const originalScaleX = mockShape.scaleX; + const originalX = mockShape.x; + + execute(mockBackground as any); + + // デフォルトのモック設定では tx = 0 のため変更されない + expect(mockShape.scaleX).toBe(originalScaleX); + expect(mockShape.x).toBe(originalX); + }); + + it("ty が 0 の場合、scaleY と y は変更されないこと", () => { + const mockShape = { + scaleX: 1, + scaleY: 1, + x: 0, + y: 0 + }; + + const mockBackground = { + shape: mockShape + }; + + const originalScaleY = mockShape.scaleY; + const originalY = mockShape.y; + + execute(mockBackground as any); + + // デフォルトのモック設定では ty = 0 のため変更されない + expect(mockShape.scaleY).toBe(originalScaleY); + expect(mockShape.y).toBe(originalY); + }); + }); +}); diff --git a/template/src/model/domain/callback/Background/service/BackgroundChangeScaleService.ts b/template/src/model/domain/callback/Background/service/BackgroundChangeScaleService.ts index 4e706bb..7d5fe66 100644 --- a/template/src/model/domain/callback/Background/service/BackgroundChangeScaleService.ts +++ b/template/src/model/domain/callback/Background/service/BackgroundChangeScaleService.ts @@ -11,8 +11,7 @@ import { stage } from "@next2d/display"; * @method * @protected */ -export const execute = (background: Background): void => -{ +export const execute = (background: Background): void => { const width = config.stage.width; const height = config.stage.height; const scale = stage.rendererScale; diff --git a/template/src/model/domain/callback/Background/service/BackgroundDrawService.test.ts b/template/src/model/domain/callback/Background/service/BackgroundDrawService.test.ts new file mode 100644 index 0000000..886b9f9 --- /dev/null +++ b/template/src/model/domain/callback/Background/service/BackgroundDrawService.test.ts @@ -0,0 +1,94 @@ +import { describe, it, expect, vi } from "vitest"; + +// @next2d/geom モジュールをモック +vi.mock("@next2d/geom", () => { + return { + Matrix: class MockMatrix { + createGradientBox = vi.fn(); + } + }; +}); + +import { execute } from "./BackgroundDrawService"; + +/** + * @description BackgroundDrawService のテスト + * Tests for BackgroundDrawService + */ +describe("BackgroundDrawService", () => { + /** + * @description execute 関数のテスト + * Test for execute function + */ + describe("execute", () => { + it("execute 関数が存在すること", () => { + expect(typeof execute).toBe("function"); + }); + + it("background.shape.graphics のメソッドが呼び出されること", () => { + const mockGraphics = { + clear: vi.fn().mockReturnThis(), + beginGradientFill: vi.fn().mockReturnThis(), + drawRect: vi.fn().mockReturnThis(), + endFill: vi.fn().mockReturnThis() + }; + + const mockBackground = { + shape: { + graphics: mockGraphics + } + }; + + execute(mockBackground as any); + + expect(mockGraphics.clear).toHaveBeenCalled(); + expect(mockGraphics.beginGradientFill).toHaveBeenCalled(); + expect(mockGraphics.drawRect).toHaveBeenCalled(); + expect(mockGraphics.endFill).toHaveBeenCalled(); + }); + + it("beginGradientFill が正しいパラメータで呼び出されること", () => { + const mockGraphics = { + clear: vi.fn().mockReturnThis(), + beginGradientFill: vi.fn().mockReturnThis(), + drawRect: vi.fn().mockReturnThis(), + endFill: vi.fn().mockReturnThis() + }; + + const mockBackground = { + shape: { + graphics: mockGraphics + } + }; + + execute(mockBackground as any); + + // linear グラデーションが使用されること + expect(mockGraphics.beginGradientFill).toHaveBeenCalledWith( + "linear", + expect.any(Array), + expect.any(Array), + expect.any(Array), + expect.any(Object) + ); + }); + + it("メソッドチェーンが正しく動作すること", () => { + const mockGraphics = { + clear: vi.fn().mockReturnThis(), + beginGradientFill: vi.fn().mockReturnThis(), + drawRect: vi.fn().mockReturnThis(), + endFill: vi.fn().mockReturnThis() + }; + + const mockBackground = { + shape: { + graphics: mockGraphics + } + }; + + // エラーなく実行されること + expect(() => execute(mockBackground as any)).not.toThrow(); + }); + }); +}); diff --git a/template/src/model/domain/callback/Background/service/BackgroundDrawService.ts b/template/src/model/domain/callback/Background/service/BackgroundDrawService.ts index 2656c00..dbd384c 100644 --- a/template/src/model/domain/callback/Background/service/BackgroundDrawService.ts +++ b/template/src/model/domain/callback/Background/service/BackgroundDrawService.ts @@ -11,13 +11,12 @@ import { Matrix } from "@next2d/geom"; * @method * @protected */ -export const execute = (background: Background): void => -{ +export const execute = (background: Background): void => { const width = config.stage.width; const height = config.stage.height; const matrix = new Matrix(); - matrix.createGradientBox(width, height, Math.PI / 2); + matrix.createGradientBox(height, width, Math.PI / 2, 0, 0); background .shape diff --git a/template/src/model/domain/event/home/HomeButtonPointerDownEvent.ts b/template/src/model/domain/event/home/HomeButtonPointerDownEvent.ts deleted file mode 100644 index 38e7f20..0000000 --- a/template/src/model/domain/event/home/HomeButtonPointerDownEvent.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { Event } from "@next2d/events"; -import type { Sprite } from "@next2d/display"; - -/** - * @description Home画面のキャラクターの移動開始処理 - * Processes the start of character movement on the Home screen. - * - * @return {void} - * @method - * @public - */ -export const execute = (event: Event): void => -{ - const sprite = event.currentTarget as Sprite; - sprite.startDrag(); -}; \ No newline at end of file diff --git a/template/src/model/domain/event/home/HomeButtonPointerUpEvent.ts b/template/src/model/domain/event/home/HomeButtonPointerUpEvent.ts deleted file mode 100644 index 266eae8..0000000 --- a/template/src/model/domain/event/home/HomeButtonPointerUpEvent.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { Event } from "@next2d/events"; -import type { Sprite } from "@next2d/display"; - -/** - * @description Home画面のキャラクターの移動処理を終了 - * Terminates the process of moving the character on the Home screen. - * - * @param {Event} event - * @method - * @public - */ -export const execute = (event: Event): void => -{ - const sprite = event.currentTarget as Sprite; - sprite.stopDrag(); -}; \ No newline at end of file diff --git a/template/src/model/domain/event/top/TopButtonPointerUpEvent.ts b/template/src/model/domain/event/top/TopButtonPointerUpEvent.ts deleted file mode 100644 index b3fdf79..0000000 --- a/template/src/model/domain/event/top/TopButtonPointerUpEvent.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { app } from "@next2d/framework"; - -/** - * @description トップページのボタンがポインターアップされた時の実行関数 - * Execution function when the top button is mouse up - * - * @return {Promise} - * @method - * @protected - */ -export const execute = async (): Promise => -{ - await app.gotoView("home"); -}; diff --git a/template/src/model/infrastructure/README.md b/template/src/model/infrastructure/README.md new file mode 100644 index 0000000..40a2a92 --- /dev/null +++ b/template/src/model/infrastructure/README.md @@ -0,0 +1,438 @@ +# Infrastructure Layer + +インフラストラクチャ層のディレクトリです。外部システムとの連携やデータ永続化の詳細を実装します。 + +Directory for the Infrastructure layer. Implements details of integration with external systems and data persistence. + +## 役割 / Role + +Infrastructure層は、アプリケーションの外部とのやり取りを担当します: + +The Infrastructure layer is responsible for interactions with the outside of the application: + +- ✅ **データアクセス** - API通信、データベースアクセス +- ✅ **外部サービス連携** - サードパーティAPIの呼び出し +- ✅ **永続化の実装** - ストレージへの保存/読み込み +- ✅ **エラーハンドリング** - 外部システムのエラーを適切に処理 +- ❌ **ビジネスロジック** - Application/Domain層の責務 +- ❌ **UI操作** - View層の責務 + +## ディレクトリ構造 / Directory Structure + +``` +infrastructure/ +└── repository/ + └── HomeTextRepository.ts +``` + +将来的に以下のような拡張も可能です: + +Future extensions are possible, such as: + +``` +infrastructure/ +├── repository/ # データアクセス層 +│ ├── HomeTextRepository.ts +│ ├── UserRepository.ts +│ └── ConfigRepository.ts +├── entity/ # DBエンティティ +│ └── UserEntity.ts +├── dto/ # データ転送 +│ └── ApiResponseDto.ts +└── external/ # 外部サービス + └── AnalyticsService.ts +``` + +## Repository Pattern + +Repositoryパターンは、データアクセスの詳細を抽象化し、Application層から隔離します。 + +The Repository pattern abstracts data access details and isolates them from the Application layer. + +### Repository の特徴 / Repository Characteristics + +1. **データソースの抽象化** - APIやDBなどの詳細を隠蔽 +2. **統一されたインターフェース** - 一貫したデータアクセス方法 +3. **テスタビリティ** - モックに差し替え可能 +4. **エラーハンドリング** - 外部システムのエラーを適切に処理 + +### Example: HomeTextRepository + +```typescript +import type { IHomeTextResponse } from "@/interface/IHomeTextResponse"; +import { config } from "@/config/Config"; + +/** + * @description Home画面のテキストデータを管理するRepository + * Repository for managing Home screen text data + * + * @class + */ +export class HomeTextRepository +{ + /** + * @description Home画面のテキストデータを取得 + * Get text data for Home screen + * + * @return {Promise} + * @static + * @throws {Error} Failed to fetch home text + */ + static async get (): Promise + { + try { + // APIエンドポイントへリクエスト + const response = await fetch( + `${config.api.endPoint}api/home.json` + ); + + // HTTPエラーチェック + if (!response.ok) { + throw new Error( + `HTTP error! status: ${response.status}` + ); + } + + // レスポンスをパース + return await response.json() as IHomeTextResponse; + + } catch (error) { + // エラーログ + console.error('Failed to fetch home text:', error); + + // エラーを上位層に伝播 + throw error; + } + } +} +``` + +## Repository の設計原則 / Repository Design Principles + +### 1. 型安全性 / Type Safety + +`any` 型を避け、明示的な型定義を使用します。 + +Avoid `any` type and use explicit type definitions. + +```typescript +// ✅ 良い例: 明示的な型定義 +static async get(): Promise { + const response = await fetch(...); + return await response.json() as IHomeTextResponse; +} + +// ❌ 悪い例: any型 +static async get(): Promise { // NG + const response = await fetch(...); + return await response.json(); +} +``` + +### 2. エラーハンドリング / Error Handling + +すべての外部アクセスでエラーハンドリングを実装します。 + +Implement error handling for all external access. + +```typescript +// ✅ 良い例: 適切なエラーハンドリング +static async get(): Promise { + try { + const response = await fetch(...); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return await response.json(); + } catch (error) { + console.error('Failed to fetch:', error); + throw error; // 上位層に伝播 + } +} + +// ❌ 悪い例: エラーハンドリングなし +static async get(): Promise { + const response = await fetch(...); // エラーが握りつぶされる + return await response.json(); +} +``` + +### 3. 設定の外部化 / Externalize Configuration + +エンドポイントなどの設定は `config` から取得します。 + +Retrieve settings such as endpoints from `config`. + +```typescript +// ✅ 良い例: 設定を外部化 +static async get(): Promise { + const response = await fetch( + `${config.api.endPoint}api/data.json` + ); + return await response.json(); +} + +// ❌ 悪い例: ハードコーディング +static async get(): Promise { + const response = await fetch( + 'https://example.com/api/data.json' // NG + ); + return await response.json(); +} +``` + +### 4. 静的メソッド vs インスタンスメソッド / Static vs Instance Methods + +シンプルな場合は静的メソッド、状態を持つ場合はインスタンスメソッドを使用します。 + +Use static methods for simple cases, instance methods when holding state. + +```typescript +// ✅ 静的メソッド: ステートレスな場合 +export class SimpleRepository { + static async get(id: string): Promise { + const response = await fetch(`/api/${id}`); + return await response.json(); + } +} + +// ✅ インスタンスメソッド: 状態を持つ場合 +export class CachedRepository { + private cache: Map = new Map(); + + async get(id: string): Promise { + if (this.cache.has(id)) { + return this.cache.get(id)!; + } + + const response = await fetch(`/api/${id}`); + const data = await response.json(); + this.cache.set(id, data); + + return data; + } +} +``` + +## 高度なエラーハンドリング / Advanced Error Handling + +### リトライ機能 / Retry Functionality + +```typescript +export class RobustRepository { + private static readonly MAX_RETRIES = 3; + private static readonly RETRY_DELAY = 1000; + + static async get(id: string): Promise { + let lastError: Error | null = null; + + for (let i = 0; i < this.MAX_RETRIES; i++) { + try { + const response = await fetch(`/api/${id}`); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + + return await response.json(); + } catch (error) { + lastError = error as Error; + console.warn(`Retry ${i + 1}/${this.MAX_RETRIES}:`, error); + + if (i < this.MAX_RETRIES - 1) { + await this.sleep(this.RETRY_DELAY); + } + } + } + + throw new Error(`Failed after ${this.MAX_RETRIES} retries: ${lastError}`); + } + + private static sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); + } +} +``` + +### タイムアウト処理 / Timeout Handling + +```typescript +export class TimeoutRepository { + private static readonly TIMEOUT = 5000; // 5秒 + + static async get(id: string): Promise { + const controller = new AbortController(); + const timeoutId = setTimeout( + () => controller.abort(), + this.TIMEOUT + ); + + try { + const response = await fetch(`/api/${id}`, { + signal: controller.signal + }); + + clearTimeout(timeoutId); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + + return await response.json(); + } catch (error) { + clearTimeout(timeoutId); + + if (error instanceof Error && error.name === 'AbortError') { + throw new Error(`Request timeout after ${this.TIMEOUT}ms`); + } + + throw error; + } + } +} +``` + +## キャッシング戦略 / Caching Strategy + +### メモリキャッシュ / Memory Cache + +```typescript +export class CachedRepository { + private static cache: Map = new Map(); + + private static readonly CACHE_TTL = 60000; // 1分 + + static async get(id: string): Promise { + // キャッシュチェック + const cached = this.cache.get(id); + const now = Date.now(); + + if (cached && (now - cached.timestamp) < this.CACHE_TTL) { + console.log('Cache hit:', id); + return cached.data; + } + + // APIから取得 + console.log('Cache miss:', id); + const response = await fetch(`/api/${id}`); + const data = await response.json(); + + // キャッシュに保存 + this.cache.set(id, { + data, + timestamp: now + }); + + return data; + } + + static clearCache(): void { + this.cache.clear(); + } +} +``` + +## モック実装 / Mock Implementation + +テスト用のモックRepositoryを作成できます。 + +You can create mock Repositories for testing. + +```typescript +// テスト用モック +export class MockHomeTextRepository { + static async get(): Promise { + // モックデータを返す + return { + word: 'Mock Data' + }; + } +} + +// テストコード +import { MockHomeTextRepository } from './MockHomeTextRepository'; + +describe('HomeViewModel', () => { + test('should display mock data', async () => { + // Repositoryをモックに差し替え + const data = await MockHomeTextRepository.get(); + expect(data.word).toBe('Mock Data'); + }); +}); +``` + +## 新しいRepositoryの作成 / Creating New Repositories + +### 手順 / Steps + +1. **データ構造を定義** - `interface/` にレスポンス型を追加 +2. **Repositoryクラスを作成** - `infrastructure/repository/` に配置 +3. **エラーハンドリング実装** - try-catchで適切に処理 +4. **型定義を追加** - `any` を避け明示的な型を使用 +5. **UseCaseから使用** - Application層で呼び出し + +### テンプレート / Template + +```typescript +import type { IYourResponse } from "@/interface/IYourResponse"; +import { config } from "@/config/Config"; + +/** + * @description [Repositoryの説明] + * [Repository description] + * + * @class + */ +export class YourRepository +{ + /** + * @description [処理の説明] + * [Process description] + * + * @param {string} id + * @return {Promise} + * @static + * @throws {Error} [エラーの説明] + */ + static async get (id: string): Promise + { + try { + const response = await fetch( + `${config.api.endPoint}api/your-endpoint/${id}` + ); + + if (!response.ok) { + throw new Error( + `HTTP error! status: ${response.status}` + ); + } + + return await response.json() as IYourResponse; + + } catch (error) { + console.error('Failed to fetch data:', error); + throw error; + } + } +} +``` + +## ベストプラクティス / Best Practices + +1. **型安全性** - `any` 型を避け、明示的な型定義を使用 +2. **エラーハンドリング** - すべての外部アクセスでtry-catchを実装 +3. **設定の外部化** - エンドポイントなどは `config` から取得 +4. **ログ出力** - エラー時は詳細なログを出力 +5. **単一責任** - 1つのRepositoryは1つのデータソースを担当 + +## 関連ドキュメント / Related Documentation + +- [ARCHITECTURE.md](../../../ARCHITECTURE.md) - アーキテクチャ全体の説明 +- [../application/README.md](../application/README.md) - Application層の説明 +- [../../interface/README.md](../../interface/README.md) - インターフェース定義 +- [../../config/README.md](../../config/README.md) - 設定ファイル diff --git a/template/src/model/infrastructure/repository/HomeTextRepository.test.ts b/template/src/model/infrastructure/repository/HomeTextRepository.test.ts new file mode 100644 index 0000000..26903cc --- /dev/null +++ b/template/src/model/infrastructure/repository/HomeTextRepository.test.ts @@ -0,0 +1,87 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { HomeTextRepository } from "./HomeTextRepository"; +import type { IHomeTextResponse } from "@/interface/IHomeTextResponse"; + +/** + * @description HomeTextRepository のテスト + * Tests for HomeTextRepository + */ +describe("HomeTextRepository", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + /** + * @description get メソッドのテスト + * Test for get method + */ + describe("get", () => { + it("正常にデータを取得できること", async () => { + const mockResponse: IHomeTextResponse = { word: "Hello, Next2D!" }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: vi.fn().mockResolvedValue(mockResponse) + }); + + const result = await HomeTextRepository.get(); + + expect(result).toEqual(mockResponse); + expect(result.word).toBe("Hello, Next2D!"); + }); + + it("fetch が正しい URL で呼び出されること", async () => { + const mockResponse: IHomeTextResponse = { word: "Test" }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: vi.fn().mockResolvedValue(mockResponse) + }); + + await HomeTextRepository.get(); + + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining("api/home.json") + ); + }); + + it("HTTP エラー時に Error をスローすること", async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: false, + status: 404 + }); + + await expect(HomeTextRepository.get()).rejects.toThrow("HTTP error! status: 404"); + }); + + it("500 エラー時に適切なメッセージでスローすること", async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: false, + status: 500 + }); + + await expect(HomeTextRepository.get()).rejects.toThrow("HTTP error! status: 500"); + }); + + it("ネットワークエラー時に Error をスローすること", async () => { + global.fetch = vi.fn().mockRejectedValue(new Error("Network error")); + + await expect(HomeTextRepository.get()).rejects.toThrow("Network error"); + }); + + it("静的メソッドであること", () => { + expect(typeof HomeTextRepository.get).toBe("function"); + }); + }); + + /** + * @description クラス構造のテスト + * Test for class structure + */ + describe("Class Structure / クラス構造", () => { + it("get が静的メソッドとして定義されていること", () => { + expect(HomeTextRepository.get).toBeDefined(); + expect(typeof HomeTextRepository.get).toBe("function"); + }); + }); +}); diff --git a/template/src/model/infrastructure/repository/HomeTextRepository.ts b/template/src/model/infrastructure/repository/HomeTextRepository.ts index 5f75b26..e92382f 100644 --- a/template/src/model/infrastructure/repository/HomeTextRepository.ts +++ b/template/src/model/infrastructure/repository/HomeTextRepository.ts @@ -1,17 +1,31 @@ +import type { IHomeTextResponse } from "@/interface/IHomeTextResponse"; import { config } from "@/config/Config"; /** * @class */ -export class HomeTextRepository -{ +export class HomeTextRepository { /** - * @return {Promise} + * @description Home画面のテキストデータを取得 + * Get text data for Home screen + * + * @return {Promise} * @static + * @throws {Error} Failed to fetch home text */ - static async get (): Promise + static async get (): Promise { - const response = await fetch(`${config.api.endPoint}api/home.json`); - return await response.json(); + try { + const response = await fetch(`${config.api.endPoint}api/home.json`); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return await response.json() as IHomeTextResponse; + } catch (error) { + console.error("Failed to fetch home text:", error); + throw error; + } } } \ No newline at end of file diff --git a/template/src/model/ui/component/atom/ButtonComponent.ts b/template/src/model/ui/component/atom/ButtonComponent.ts deleted file mode 100644 index 9483160..0000000 --- a/template/src/model/ui/component/atom/ButtonComponent.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { MovieClip } from "@next2d/display"; - -/** - * @description 指定したコンテンツをボタンモードに設定します。 - * Sets the specified content to button mode. - * - * @param {D} content - * @return {D} - * @method - * @public - */ -export const execute = (content: D | null = null): D => -{ - const button = content || new MovieClip(); - button.buttonMode = true; - - return button as D; -}; \ No newline at end of file diff --git a/template/src/model/ui/component/atom/TextComponent.ts b/template/src/model/ui/component/atom/TextComponent.ts deleted file mode 100644 index e205213..0000000 --- a/template/src/model/ui/component/atom/TextComponent.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { TextField } from "@next2d/text"; - -/** - * @description 指定したテキストフィールドを生成します。 - * Generates the specified text field. - * - * @param {string} text - * @param {object} [props=null] - * @param {object} [format=null] - * @return {TextField} - * @method - * @public - */ -export const execute = ( - text: string = "", - props: any = null, - format: any = null -): TextField => { - - const textField = new TextField(); - - if (props) { - - const keys: string[] = Object.keys(props); - for (let idx = 0; idx < keys.length; idx++) { - - const name = keys[idx]; - - if (!(name in textField)) { - continue; - } - - // @ts-ignore - textField[name] = props[name]; - } - } - - if (format) { - - const textFormat = textField.defaultTextFormat; - - const keys: string[] = Object.keys(format); - for (let idx = 0; idx < keys.length; idx++) { - - const name = keys[idx]; - - if (!(name in textFormat)) { - continue; - } - - // @ts-ignore - textFormat[name] = format[name]; - } - - textField.defaultTextFormat = textFormat; - } - - textField.text = text; - - return textField; -}; \ No newline at end of file diff --git a/template/src/model/ui/component/template/home/HomeButtonTemplate.ts b/template/src/model/ui/component/template/home/HomeButtonTemplate.ts deleted file mode 100644 index 848ca1a..0000000 --- a/template/src/model/ui/component/template/home/HomeButtonTemplate.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { config } from "@/config/Config"; -import { execute as buttonComponent } from "@/model/ui/component/atom/ButtonComponent"; -import { HomeContent } from "@/model/application/content/HomeContent"; -import { PointerEvent } from "@next2d/events"; -import { execute as homeButtonPointerDownEvent } from "@/model/domain/event/home/HomeButtonPointerDownEvent"; -import { execute as homeButtonPointerUpEvent } from "@/model/domain/event/home/HomeButtonPointerUpEvent"; - -/** - * @description Home画面のキャラクターを生成 - * Generate characters for the Home screen - * - * @return {HomeContent} - * @method - * @public - */ -export const execute = (): HomeContent => -{ - const homeContent = buttonComponent(new HomeContent()); - - homeContent.x = config.stage.width / 2 - 4; - homeContent.y = config.stage.height / 2; - - homeContent.scaleX = 2; - homeContent.scaleY = 2; - - homeContent.addEventListener(PointerEvent.POINTER_DOWN, homeButtonPointerDownEvent); - homeContent.addEventListener(PointerEvent.POINTER_UP, homeButtonPointerUpEvent); - - return homeContent; -}; \ No newline at end of file diff --git a/template/src/model/ui/component/template/home/HomeTextTemplate.ts b/template/src/model/ui/component/template/home/HomeTextTemplate.ts deleted file mode 100644 index bdb40c9..0000000 --- a/template/src/model/ui/component/template/home/HomeTextTemplate.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { TextField } from "@next2d/text"; -import type { HomeContent } from "@/model/application/content/HomeContent"; -import { config } from "@/config/Config"; -import { execute as textComponent } from "@/model/ui/component/atom/TextComponent"; -import { app } from "@next2d/framework"; -import { Event } from "@next2d/events"; - -/** - * @description Home画面のTextFieldを作成 - * Create a TextField for the Home screen - * - * @return {TextField} - * @method - * @public - */ -export const execute = (home_content: HomeContent): TextField => -{ - const response = app.getResponse(); - - // Hello, World. - const text = response.has("HomeText") ? response.get("HomeText").word : ""; - const textField = textComponent(text, { - "autoSize": "center", - "type": "input" - }); - - textField.addEventListener(Event.CHANGE, () => - { - textField.x = config.stage.width / 2 - textField.width / 2; - }); - - textField.x = config.stage.width / 2 - textField.width / 2; - textField.y = home_content.y + home_content.height / 2 + textField.height; - - return textField; -}; \ No newline at end of file diff --git a/template/src/model/ui/component/template/top/TopButtonTemplate.ts b/template/src/model/ui/component/template/top/TopButtonTemplate.ts deleted file mode 100644 index 4184949..0000000 --- a/template/src/model/ui/component/template/top/TopButtonTemplate.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { TopContent } from "@/model/application/content/TopContent"; -import type { MovieClip } from "@next2d/display"; -import { config } from "@/config/Config"; -import { execute as buttonComponent } from "@/model/ui/component/atom/ButtonComponent"; -import { execute as topButtonPointerUpEvent } from "@/model/domain/event/top/TopButtonPointerUpEvent"; -import { execute as textComponent } from "@/model/ui/component/atom/TextComponent"; -import { app } from "@next2d/framework"; -import { PointerEvent } from "@next2d/events"; - -/** - * @description Topページのボタンを生成 - * Generate Top page button - * - * @return {MovieClip} - * @method - * @public - */ -export const execute = (top_content: TopContent): D => -{ - const response = app.getResponse(); - - const text = response.has("TopText") ? response.get("TopText").word : ""; - const textField = textComponent(text, { - "autoSize": "center" - }); - - textField.x = config.stage.width / 2 - textField.width / 2; - textField.y = top_content.y + top_content.height / 2 + textField.height; - - const button = buttonComponent(); - button.addChild(textField); - - /** - * ドメイン層から専用のイベントを起動 - * Launch dedicated events from the domain layer - */ - button.addEventListener(PointerEvent.POINTER_UP, topButtonPointerUpEvent); - - return button as D; -}; \ No newline at end of file diff --git a/template/src/model/ui/component/template/top/TopContentTemplate.ts b/template/src/model/ui/component/template/top/TopContentTemplate.ts deleted file mode 100644 index cfb18fc..0000000 --- a/template/src/model/ui/component/template/top/TopContentTemplate.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { TopContent } from "@/model/application/content/TopContent"; -import { config } from "@/config/Config"; - -/** - * @description Topページのログ画像をAnimationToolのJSONから作成 - * Top page log image created from AnimationTool JSON - * - * @return {TopContent} - * @method - * @public - */ -export const execute = (): TopContent => -{ - /** - * ロゴアニメーションをAnimation ToolのJSONから生成 - * Logo animation generated from Animation Tool's JSON - */ - const topContent = new TopContent(); - - topContent.x = config.stage.width / 2; - topContent.y = config.stage.height / 2; - - return topContent; -}; \ No newline at end of file diff --git a/template/src/ui/README.md b/template/src/ui/README.md new file mode 100644 index 0000000..da522d2 --- /dev/null +++ b/template/src/ui/README.md @@ -0,0 +1,324 @@ +# UI Components + +UIコンポーネントを格納するディレクトリです。アトミックデザインの概念に基づいて構成されています。 + +Directory for storing UI components, structured based on Atomic Design principles. + +## ディレクトリ構造 / Directory Structure + +``` +ui/ +├── animation/ # アニメーション定義 +├── component/ +│ ├── atom/ # 最小単位 +│ ├── molecule/ # 複合コンポーネント +│ ├── organism/ # 複数Moleculeの組み合わせ(将来の拡張用) +│ ├── page/ # ページコンポーネント +│ └── template/ # ページテンプレート(将来の拡張用) +└── content/ # Animation Tool +``` + +## アトミックデザインの階層 / Atomic Design Hierarchy + +### 1. Atom (原子) - 最小単位のコンポーネント + +最も基本的なUI要素です。これ以上分割できない最小のコンポーネントです。 + +The most basic UI elements. The smallest components that cannot be divided further. + +#### component/atom/ButtonAtom.ts +ボタンの基本機能を提供します。 + +Provides basic button functionality. + +```typescript +export class ButtonAtom extends Sprite { + constructor() { + super(); + this.buttonMode = true; // ボタンモード有効化 + } +} +``` + +**特徴 / Features:** +- マウスカーソルがポインター型に変更される +- ボタンとしての基本的な振る舞い + +#### component/atom/TextAtom.ts +テキスト表示の基本機能を提供します。 + +Provides basic text display functionality. + +```typescript +export class TextAtom extends TextField implements ITextField { + constructor( + text: string = "", + props: any | null = null, + format_object: ITextFormatObject | null = null + ) { + // プロパティ設定、フォーマット設定 + } +} +``` + +**特徴 / Features:** +- 柔軟なテキストフォーマット設定 +- プロパティの動的設定 +- `ITextField` インターフェースを実装 + +### 2. Molecule (分子) - Atomを組み合わせたコンポーネント + +複数のAtomを組み合わせて、より複雑な機能を持つコンポーネントです。 + +Components with more complex functionality, combining multiple Atoms. + +#### component/molecule/HomeBtnMolecule.ts +Home画面のボタンコンポーネントです。 + +Button component for the Home screen. + +```typescript +export class HomeBtnMolecule extends ButtonAtom implements IDraggable { + private readonly homeContent: HomeContent; + + constructor() { + super(); + this.homeContent = new HomeContent(); + this.addChild(this.homeContent); + } + // IDraggableメソッド(startDrag/stopDrag)はMovieClipContentの親クラスから継承 +} +``` + +**特徴 / Features:** +- `ButtonAtom` を継承 +- `IDraggable` インターフェースを実装(メソッドは`MovieClipContent`親クラスから継承) +- ドラッグ&ドロップ機能を提供 + +#### component/molecule/TopBtnMolecule.ts +Top画面のボタンコンポーネントです。 + +Button component for the Top screen. + +```typescript +export class TopBtnMolecule extends ButtonAtom { + constructor(text: string) { + super(); + // ViewModelから渡されたテキストを表示 + const textField = new TextAtom(text, { autoSize: "center" }); + this.addChild(textField); + } + + playEntrance(callback: () => void): void { + // アニメーション再生 + } +} +``` + +**特徴 / Features:** +- テキストはViewModelから引数で受け取る(データ取得はViewModelの責務) +- 入場アニメーション機能 + +### 3. Content - Animation Tool生成コンテンツ + +Animation Toolで作成されたコンテンツです。 + +Content created with the Animation Tool. + +#### content/HomeContent.ts +Home画面用のアニメーションコンテンツです。 + +Animation content for the Home screen. + +```typescript +export class HomeContent extends MovieClipContent implements IDraggable { + get namespace(): string { + return "HomeContent"; // Animation Toolのシンボル名 + } +} +``` + +**特徴 / Features:** +- `MovieClipContent` を継承 +- `IDraggable` インターフェースを実装 +- Animation Tool (`file/sample.n2d`) と連携 + +#### content/TopContent.ts +Top画面用のアニメーションコンテンツです。 + +Animation content for the Top screen. + +```typescript +export class TopContent extends MovieClipContent { + get namespace(): string { + return "TopContent"; + } +} +``` + +### 4. Animation - アニメーション定義 + +コンポーネントのアニメーションロジックを定義します。 + +Defines animation logic for components. + +#### animation/top/TopBtnEntranceAnimation.ts +Topボタンの入場アニメーションです。 + +Entrance animation for the Top button. + +**特徴 / Features:** +- コンポーネントとアニメーションロジックを分離 +- 再利用可能なアニメーション定義 + +## 設計原則 / Design Principles + +### 1. 単一責任の原則 / Single Responsibility Principle + +各コンポーネントは1つの責務のみを持ちます。 + +Each component has only one responsibility. + +```typescript +// ✅ 良い例: 表示のみを担当 +export class TextAtom extends TextField { ... } + +// ❌ 悪い例: 表示とビジネスロジックを混在 +export class TextAtom extends TextField { + fetchDataFromAPI() { ... } // NG: データ取得は別層の責務 +} +``` + +### 2. インターフェース指向 / Interface-Oriented + +抽象に依存し、具象に依存しません。 + +Depend on abstractions, not concretions. + +```typescript +// ✅ 良い例: インターフェースを実装し、内部のContentに委譲 +export class HomeBtnMolecule extends ButtonAtom implements IDraggable { + private readonly homeContent: HomeContent; + // IDraggableメソッド(startDrag/stopDrag)はMovieClipContentの親クラスから継承 +} +``` + +### 3. 再利用性 / Reusability + +Atomは汎用的に、Moleculeは特定の用途に設計します。 + +Atoms are designed generically, Molecules for specific purposes. + +```typescript +// Atom: 汎用的 +export class ButtonAtom extends Sprite { ... } + +// Molecule: 特定の用途 +export class HomeBtnMolecule extends ButtonAtom { ... } +export class TopBtnMolecule extends ButtonAtom { ... } +``` + +### 4. 疎結合 / Loose Coupling + +ビジネスロジックやデータアクセスに直接依存しません。 + +Don't directly depend on business logic or data access. + +```typescript +// ✅ 良い例: ViewModelからデータを受け取る +constructor(text: string) { + this.textField = new TextAtom(text); +} + +// ❌ 悪い例: 直接APIアクセス +constructor() { + const data = await Repository.get(); // NG +} +``` + +## コンポーネントの作成ガイドライン / Component Creation Guidelines + +### Atomの作成 / Creating Atoms + +1. **基本クラスを継承** - `Sprite`, `TextField` など +2. **最小限の機能** - 1つの明確な役割 +3. **プロパティの設定** - コンストラクタで柔軟に設定可能に +4. **インターフェース実装** - 必要に応じて抽象化 + +```typescript +import { Sprite } from "@next2d/display"; + +export class YourAtom extends Sprite { + constructor(props: any = null) { + super(); + + // プロパティ設定 + if (props) { + Object.assign(this, props); + } + } +} +``` + +### Moleculeの作成 / Creating Molecules + +1. **Atomを組み合わせる** - 複数のAtomを子要素として追加 +2. **特定の用途** - 画面固有の機能を実装 +3. **インターフェース実装** - ビジネスロジック層との連携 +4. **イベント処理** - 必要に応じてイベントリスナーを設定 + +```typescript +import { ButtonAtom } from "../atom/ButtonAtom"; +import { TextAtom } from "../atom/TextAtom"; + +export class YourMolecule extends ButtonAtom { + constructor() { + super(); + + const text = new TextAtom("Click me"); + this.addChild(text); + } +} +``` + +### Contentの作成 / Creating Contents + +1. **Animation Toolと連携** - `namespace` でシンボル名を指定 +2. **MovieClipContentを継承** - フレームアニメーション機能 +3. **インターフェース実装** - 必要に応じて機能を追加 + +```typescript +import { MovieClipContent } from "@next2d/framework"; + +export class YourContent extends MovieClipContent { + get namespace(): string { + return "YourSymbolName"; // Animation Toolのシンボル名 + } +} +``` + +## テスト / Testing + +UIコンポーネントのテストはインターフェースを利用します。 + +UI component testing utilizes interfaces. + +```typescript +import { IDraggable } from "@/interface/IDraggable"; + +describe('HomeBtnMolecule', () => { + test('implements IDraggable', () => { + const btn = new HomeBtnMolecule(); + + expect(btn.startDrag).toBeDefined(); + expect(btn.stopDrag).toBeDefined(); + }); +}); +``` + +## 関連ドキュメント / Related Documentation + +- [ARCHITECTURE.md](../../ARCHITECTURE.md) - アーキテクチャ全体の説明 +- [interface/README.md](../interface/README.md) - インターフェース定義 +- [view/README.md](../view/README.md) - View層の説明 +- [Animation Tool Documentation](https://next2d.app/docs/animation-tool/) - Animation Toolの使い方 diff --git a/template/src/ui/animation/README.md b/template/src/ui/animation/README.md new file mode 100644 index 0000000..40df209 --- /dev/null +++ b/template/src/ui/animation/README.md @@ -0,0 +1,269 @@ +# Animation Definitions + +コンポーネントのアニメーションロジックを格納するディレクトリです。 + +Directory for storing animation logic for components. + +## 概要 / Overview + +アニメーション定義をコンポーネントから分離することで、コードの再利用性と保守性を向上させます。 + +Separating animation definitions from components improves code reusability and maintainability. + +## ディレクトリ構造 / Directory Structure + +``` +animation/ +└── top/ + └── TopBtnShowAnimation.ts +``` + +画面ごとにサブディレクトリを作成し、その中にアニメーション定義ファイルを配置します。 + +Create subdirectories for each screen and place animation definition files within them. + +## アニメーションの種類 / Animation Types + +### 登場アニメーション / Show Animation + +画面表示時のアニメーションです。 + +Animation when the screen is displayed. + +### 退場アニメーション / Exit Animation + +画面遷移時のアニメーションです。 + +Animation during screen transitions. + +### インタラクションアニメーション / Interaction Animation + +ユーザー操作に対するアニメーションです。 + +Animation in response to user actions. + +## 実装例 / Implementation Example + +### TopBtnShowAnimation.ts + +```typescript +import type { TopBtnMolecule } from "@/ui/component/molecule/TopBtnMolecule"; +import { Tween, Easing, type Job } from "@next2d/ui"; +import { Event } from "@next2d/events"; + +/** + * @description Topボタンの登場アニメーション + * Top Button Entrance Animation + * + * @class + * @public + */ +export class TopBtnShowAnimation { + + private readonly _job: Job; + + /** + * @param {TopBtnMolecule} sprite + * @param {() => void} callback + * @constructor + * @public + */ + constructor( + sprite: TopBtnMolecule, + callback: () => void + ) { + + // アニメーションの初期値に設定 + sprite.alpha = 0; + + this._job = Tween.add(sprite, + { + "alpha": 0 + }, + { + "alpha": 1 + }, 0.5, 1, Easing.inQuad + ); + + // 終了アニメーションが完了したら、完了イベントを発行 + this._job.addEventListener(Event.COMPLETE, (): void => + { + callback(); + }); + } + + /** + * @description アニメーション開始 + * Start animation + * + * @method + * @public + */ + start(): void { + this._job.start(); + } +} +``` + +## 設計原則 / Design Principles + +### 1. コンポーネントとの分離 / Separation from Components + +アニメーションロジックをコンポーネントから分離します。 + +Separate animation logic from components. + +```typescript +// ✅ 良い例: アニメーションを別ファイルに分離 +// animation/top/TopBtnShowAnimation.ts +export class TopBtnShowAnimation { ... } + +// component/molecule/TopBtnMolecule.ts +import { TopBtnShowAnimation } from "@/ui/animation/top/TopBtnShowAnimation"; + +export class TopBtnMolecule extends ButtonAtom { + playShow(callback: () => void): void { + new TopBtnShowAnimation(this, callback).start(); + } +} +``` + +### 2. 再利用性 / Reusability + +同じアニメーションを複数のコンポーネントで使用できるようにします。 + +Make the same animation usable across multiple components. + +```typescript +// ✅ 良い例: 汎用的なフェードインアニメーションクラス +import { Tween, Easing, type Job } from "@next2d/ui"; +import type { Sprite } from "@next2d/display"; +import { Event } from "@next2d/events"; + +export class FadeInAnimation { + private readonly _job: Job; + + constructor(target: Sprite, callback?: () => void) { + target.alpha = 0; + this._job = Tween.add(target, { "alpha": 0 }, { "alpha": 1 }, 0.3, 1, Easing.linear); + if (callback) { + this._job.addEventListener(Event.COMPLETE, callback); + } + } + + start(): void { + this._job.start(); + } +} +``` + +### 3. コールバック対応 / Callback Support + +アニメーション完了時のコールバックをサポートします。 + +Support callbacks for when animation completes. + +```typescript +import { Tween, Easing, type Job } from "@next2d/ui"; +import { Event } from "@next2d/events"; + +export class ExampleAnimation { + private readonly _job: Job; + + constructor(target: Sprite, callback?: () => void) { + this._job = Tween.add(target, { "alpha": 0 }, { "alpha": 1 }, 0.5, 1, Easing.outQuad); + + if (callback) { + this._job.addEventListener(Event.COMPLETE, callback); + } + } + + start(): void { + this._job.start(); + } +} +``` + +## 新しいアニメーションの追加 / Adding New Animations + +### 手順 / Steps + +1. 対象画面のディレクトリを確認(なければ作成) +2. アニメーション関数を作成 +3. コンポーネントから呼び出し +4. JSDocコメントを追加 + +### テンプレート / Template + +```typescript +import type { Sprite } from "@next2d/display"; +import { Tween, Easing, type Job } from "@next2d/ui"; +import { Event } from "@next2d/events"; + +/** + * @description [アニメーションの説明] + * [Animation description] + * + * @class + * @public + */ +export class YourAnimation { + + private readonly _job: Job; + + /** + * @param {Sprite} sprite - アニメーション対象 + * @param {() => void} callback - 完了時コールバック + * @constructor + * @public + */ + constructor( + sprite: Sprite, + callback?: () => void + ) { + + // 初期状態設定 + sprite.alpha = 0; + + // アニメーション設定 + this._job = Tween.add(sprite, + { + "alpha": 0 + }, + { + "alpha": 1 + }, 0.5, 1, Easing.outQuad + ); + + // 完了時コールバック + if (callback) { + this._job.addEventListener(Event.COMPLETE, callback); + } + } + + /** + * @description アニメーション開始 + * Start animation + * + * @method + * @public + */ + start(): void { + this._job.start(); + } +} +``` + +## ベストプラクティス / Best Practices + +1. **分離** - アニメーションロジックをコンポーネントから分離 +2. **命名** - `{Component}{Action}Animation.ts` の形式で命名(例: TopBtnShowAnimation.ts) +3. **コールバック** - Event.COMPLETEで完了時の処理をサポート +4. **再利用** - 汎用的なアニメーションクラスは共通化 +5. **クラスベース** - Jobインスタンスを保持し、start()メソッドで開始 + +## 関連ドキュメント / Related Documentation + +- [../component/README.md](../component/README.md) - UIコンポーネント +- [../README.md](../README.md) - UI全体の説明 +- [Next2D Tween Documentation](https://next2d.app/docs/tween/) - Tweenの使い方 diff --git a/template/src/ui/animation/top/TopBtnShowAnimation.ts b/template/src/ui/animation/top/TopBtnShowAnimation.ts new file mode 100644 index 0000000..4642639 --- /dev/null +++ b/template/src/ui/animation/top/TopBtnShowAnimation.ts @@ -0,0 +1,56 @@ +import type { TopBtnMolecule } from "@/ui/component/molecule/TopBtnMolecule"; +import { Tween, Easing, type Job } from "@next2d/ui"; +import { Event } from "@next2d/events"; + +/** + * @description Topボタンの登場アニメーション + * Top Button Entrance Animation + * + * @class + * @public + */ +export class TopBtnShowAnimation { + + private readonly _job: Job; + + /** + * @param {TopBtnMolecule} sprite + * @param {() => void} callback + * @constructor + * @public + */ + constructor( + sprite: TopBtnMolecule, + callback: () => void + ) { + + // アニメーションの初期値に設定 + sprite.alpha = 0; + + this._job = Tween.add(sprite, + { + "alpha": 0 + }, + { + "alpha": 1 + }, 0.5, 1, Easing.inQuad + ); + + // 終了アニメーションが完了したら、完了イベントを発行 + this._job.addEventListener(Event.COMPLETE, (): void => + { + callback(); + }); + } + + /** + * @description アニメーション開始 + * Start animation + * + * @method + * @public + */ + start(): void { + this._job.start(); + } +} \ No newline at end of file diff --git a/template/src/ui/component/README.md b/template/src/ui/component/README.md new file mode 100644 index 0000000..c3bfeeb --- /dev/null +++ b/template/src/ui/component/README.md @@ -0,0 +1,137 @@ +# UI Components + +アトミックデザインに基づいたUIコンポーネントを格納するディレクトリです。 + +Directory for UI components based on Atomic Design principles. + +## ディレクトリ構造 / Directory Structure + +``` +component/ +├── atom/ # 最小単位 +│ ├── ButtonAtom.ts +│ └── TextAtom.ts +├── molecule/ # 複合コンポーネント +│ ├── HomeBtnMolecule.ts +│ └── TopBtnMolecule.ts +├── organism/ # 複数Moleculeの組み合わせ(将来の拡張用) +├── page/ # ページコンポーネント +│ ├── home/ +│ │ └── HomePage.ts +│ └── top/ +│ └── TopPage.ts +└── template/ # ページテンプレート(将来の拡張用) +``` + +## アトミックデザイン階層 / Atomic Design Hierarchy + +### Atom (原子) + +最も基本的なUI要素です。これ以上分割できない最小のコンポーネントです。 + +The most basic UI elements. The smallest components that cannot be divided further. + +- `ButtonAtom` - ボタンの基本機能を提供 +- `TextAtom` - テキスト表示の基本機能を提供 + +### Molecule (分子) + +複数のAtomを組み合わせて、より複雑な機能を持つコンポーネントです。 + +Components with more complex functionality, combining multiple Atoms. + +- `HomeBtnMolecule` - Home画面用のボタン(ドラッグ機能付き、`IDraggable`は内部の`HomeContent`経由で提供) +- `TopBtnMolecule` - Top画面用のボタン(アニメーション付き) + +### Organism (有機体) - 将来の拡張用 + +複数のMoleculeを組み合わせた、より大きな機能単位のコンポーネントです。現在は`.gitkeep`のみで、必要に応じて実装します。 + +Larger functional unit components combining multiple Molecules. Currently contains only `.gitkeep`, to be implemented as needed. + +### Template (テンプレート) - 将来の拡張用 + +ページのレイアウト構造を定義するテンプレートです。現在は`.gitkeep`のみで、必要に応じて実装します。 + +Templates that define page layout structures. Currently contains only `.gitkeep`, to be implemented as needed. + +### Page (ページ) + +画面全体を構成するコンポーネントです。ViewからPageを配置し、PageがMoleculeやAtomを組み合わせて画面を構築します。 + +Components that compose entire screens. Views place Pages, and Pages combine Molecules and Atoms to build screens. + +- `HomePage` - Home画面のページコンポーネント +- `TopPage` - Top画面のページコンポーネント + +## 設計原則 / Design Principles + +### 1. 単一責任の原則 / Single Responsibility + +各コンポーネントは1つの責務のみを持ちます。 + +Each component has only one responsibility. + +```typescript +// ✅ 良い例: 表示のみを担当 +export class TextAtom extends TextField { ... } + +// ❌ 悪い例: データ取得とUIを混在 +export class TextAtom extends TextField { + fetchData() { ... } // NG +} +``` + +### 2. インターフェース指向 / Interface-Oriented + +抽象に依存し、具象に依存しません。 + +Depend on abstractions, not concretions. + +```typescript +// ✅ 良い例: IDraggableを実装し、内部のContentに委譲 +export class HomeBtnMolecule extends ButtonAtom implements IDraggable { + private readonly homeContent: HomeContent; + // IDraggableメソッドはMovieClipContent(HomeContent)の親クラスから継承 +} +``` + +### 3. 再利用性 / Reusability + +- **Atom** - 汎用的に設計 +- **Molecule** - 特定の用途に設計 + +## 新しいコンポーネントの追加 / Adding New Components + +### Atomの追加 + +```typescript +// src/ui/component/atom/YourAtom.ts +import { Sprite } from "@next2d/display"; + +export class YourAtom extends Sprite { + constructor() { + super(); + } +} +``` + +### Moleculeの追加 + +```typescript +// src/ui/component/molecule/YourMolecule.ts +import { ButtonAtom } from "../atom/ButtonAtom"; + +export class YourMolecule extends ButtonAtom { + constructor() { + super(); + } +} +``` + +## 関連ドキュメント / Related Documentation + +- [atom/README.md](./atom/README.md) - Atomコンポーネント +- [molecule/README.md](./molecule/README.md) - Moleculeコンポーネント +- [../README.md](../README.md) - UI全体の説明 +- [../../interface/README.md](../../interface/README.md) - インターフェース定義 diff --git a/template/src/ui/component/atom/ButtonAtom.ts b/template/src/ui/component/atom/ButtonAtom.ts new file mode 100644 index 0000000..160e2fa --- /dev/null +++ b/template/src/ui/component/atom/ButtonAtom.ts @@ -0,0 +1,52 @@ +import { Sprite } from "@next2d/display"; + +/** + * @description ボタンアトム + * Button Atom + * + * @class + * @extends {Sprite} + * @public + */ +export class ButtonAtom extends Sprite { + + /** + * @description ボタンアトムを生成する + * Create a button atom + * + * @constructor + * @public + */ + constructor() { + super(); + + // ボタンモードを有効化する + this.buttonMode = true; + } + + /** + * @description ボタンを有効化する + * Enable button + * + * @return {void} + * @method + * @public + */ + enable(): void { + this.mouseEnabled = true; + this.mouseChildren = true; + } + + /** + * @description ボタンを無効化する + * Disable button + * + * @return {void} + * @method + * @public + */ + disable(): void { + this.mouseEnabled = false; + this.mouseChildren = false; + } +} \ No newline at end of file diff --git a/template/src/ui/component/atom/TextAtom.ts b/template/src/ui/component/atom/TextAtom.ts new file mode 100644 index 0000000..ec33c21 --- /dev/null +++ b/template/src/ui/component/atom/TextAtom.ts @@ -0,0 +1,68 @@ +import type { ITextField } from "@/interface/ITextField"; +import type { ITextFieldProps } from "@/interface/ITextFieldProps"; +import type { ITextFormatObject } from "@/interface/ITextFormatObject"; +import { TextField } from "@next2d/text"; + +/** + * @description テキストの基本コンポーネント + * Basic component of text + * + * @class + * @extends {TextField} + * @implements {ITextField} + * @public + */ +export class TextAtom extends TextField implements ITextField { + + /** + * @param {string} [text=""] + * @param {ITextFieldProps | null} [props=null] + * @param {ITextFormatObject | null} [format_object=null] + * @constructor + * @public + */ + constructor( + text: string = "", + props: ITextFieldProps | null = null, + format_object: ITextFormatObject | null = null + ) { + super(); + + if (props) { + const keys = Object.keys(props) as (keyof ITextFieldProps)[]; + for (let idx = 0; idx < keys.length; idx++) { + + const name = keys[idx]; + const value = props[name]; + + if (!(name in this) || value === undefined) { + continue; + } + + (this as unknown as Record)[name] = value; + } + } + + if (format_object) { + const keys = Object.keys(format_object) as (keyof ITextFormatObject)[]; + if (keys.length) { + const textFormat = this.defaultTextFormat; + for (let idx = 0; idx < keys.length; idx++) { + + const name = keys[idx]; + const value = format_object[name]; + + if (!(name in textFormat)) { + continue; + } + + (textFormat as unknown as Record)[name] = value; + } + + this.defaultTextFormat = textFormat; + } + } + + this.text = text; + } +} \ No newline at end of file diff --git a/template/src/ui/component/molecule/HomeBtnMolecule.ts b/template/src/ui/component/molecule/HomeBtnMolecule.ts new file mode 100644 index 0000000..6bf69f1 --- /dev/null +++ b/template/src/ui/component/molecule/HomeBtnMolecule.ts @@ -0,0 +1,32 @@ +import type { IDraggable } from "@/interface/IDraggable"; +import { HomeContent } from "@/ui/content/HomeContent"; +import { ButtonAtom } from "@/ui/component/atom/ButtonAtom"; + +/** + * @description Home画面のボタン分子 + * Home Screen Button Molecule + * + * @class + * @extends {ButtonAtom} + * @implements {IDraggable} + * @public + */ +export class HomeBtnMolecule extends ButtonAtom implements IDraggable { + + private readonly homeContent: HomeContent; + + /** + * @constructor + * @public + */ + constructor () + { + super(); + + this.homeContent = new HomeContent(); + this.homeContent.scaleX = 2; + this.homeContent.scaleY = 2; + + this.addChild(this.homeContent); + } +} \ No newline at end of file diff --git a/template/src/ui/component/molecule/TopBtnMolecule.ts b/template/src/ui/component/molecule/TopBtnMolecule.ts new file mode 100644 index 0000000..5a139be --- /dev/null +++ b/template/src/ui/component/molecule/TopBtnMolecule.ts @@ -0,0 +1,47 @@ +import { TopBtnShowAnimation } from "@/ui/animation/top/TopBtnShowAnimation"; +import { ButtonAtom } from "../atom/ButtonAtom"; +import { TextAtom } from "../atom/TextAtom"; + +/** + * @description Top画面のボタン分子 + * Top Screen Button Molecule + * + * @class + * @extends {ButtonAtom} + * @public + */ +export class TopBtnMolecule extends ButtonAtom { + + /** + * @param {string} text - ボタンに表示するテキスト / Text to display on the button + * @constructor + * @public + */ + constructor (text: string) + { + super(); + + const textField = new TextAtom(text, { + "autoSize": "center" + }); + + textField.x = -textField.width / 2; + textField.y = -textField.height / 2; + + this.addChild(textField); + } + + /** + * @description ボタンのアニメーションを再生 + * Play button entrance animation + * + * @param {() => void} callback + * @return {void} + * @method + * @public + */ + show (callback: () => void): void + { + new TopBtnShowAnimation(this, callback).start(); + } +} \ No newline at end of file diff --git a/template/src/ui/component/organism/.gitkeep b/template/src/ui/component/organism/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/template/src/ui/component/page/home/HomePage.ts b/template/src/ui/component/page/home/HomePage.ts new file mode 100644 index 0000000..ab5e7ca --- /dev/null +++ b/template/src/ui/component/page/home/HomePage.ts @@ -0,0 +1,84 @@ +import type { HomeViewModel } from "@/view/home/HomeViewModel"; +import { Sprite } from "@next2d/display"; +import { HomeBtnMolecule } from "@/ui/component/molecule/HomeBtnMolecule"; +import { config } from "@/config/Config"; +import { PointerEvent, Event } from "@next2d/events"; +import { TextAtom } from "../../atom/TextAtom"; + +/** + * @description ホーム画面のページ + * Home Screen Page + * + * @class + * @extends {Sprite} + * @public + */ +export class HomePage extends Sprite { + + /** + * @description 初期起動関数 + * Initializer function + * + * @param {HomeViewModel} vm + * @return {void} + * @method + * @public + */ + initialize (vm: HomeViewModel): void { + + /** + * ホームコンテンツの座標をセット + * Set the coordinates of the home content + */ + const homeContent = new HomeBtnMolecule(); + homeContent.x = config.stage.width / 2 - 5; + homeContent.y = config.stage.height / 2; + + /** + * ホームコンテンツのイベントをViewModelに送信 + * Send home content events to ViewModel + */ + homeContent.addEventListener(PointerEvent.POINTER_DOWN, (event: PointerEvent) => { + vm.homeContentPointerDownEvent(event); + }); + homeContent.addEventListener(PointerEvent.POINTER_UP, (event: PointerEvent) => { + vm.homeContentPointerUpEvent(event); + }); + + /** + * ホームコンテンツを追加 + * Add home content + */ + this.addChild(homeContent); + + /** + * ホームテキストをViewModelから取得 + * Get home text from ViewModel + */ + const textField = new TextAtom(vm.getHomeText(), { + "autoSize": "center", + "type": "input" + }); + + /** + * ホームテキストの座標をセット + * Set the coordinates of the home text + */ + textField.x = (config.stage.width - textField.width) / 2; + textField.y = homeContent.y + homeContent.height / 2 + textField.height; + + /** + * ホームテキストのイベントをViewModelに送信 + * Send home text events to ViewModel + */ + textField.addEventListener(Event.CHANGE, (event: Event) => { + vm.homeTextChangeEvent(event); + }); + + /** + * ホームテキストを追加 + * Add home text + */ + this.addChild(textField); + } +} \ No newline at end of file diff --git a/template/src/ui/component/page/top/TopPage.ts b/template/src/ui/component/page/top/TopPage.ts new file mode 100644 index 0000000..0c1edc4 --- /dev/null +++ b/template/src/ui/component/page/top/TopPage.ts @@ -0,0 +1,89 @@ +import type { TopViewModel } from "@/view/top/TopViewModel"; +import { config } from "@/config/Config"; +import { TopContent } from "@/ui/content/TopContent"; +import { Sprite } from "@next2d/display"; +import { PointerEvent } from "@next2d/events"; +import { TopBtnMolecule } from "../../molecule/TopBtnMolecule"; + +/** + * @description トップ画面のページ + * Top Screen Page + * + * @class + * @extends {Sprite} + * @public + */ +export class TopPage extends Sprite { + + private _topBtnMolecule!: TopBtnMolecule; + + /** + * @description 初期起動関数 + * Initializer function + * + * @param {TopViewModel} vm + * @return {void} + * @method + * @public + */ + initialize (vm: TopViewModel): void { + + /** + * ロゴアニメーションをAnimation ToolのJSONから生成 + * Logo animation generated from Animation Tool's JSON + */ + const topContent = new TopContent(); + + /** + * ロゴアニメーションを画面中央に配置 + * Place logo animation in the center of the screen + */ + topContent.x = config.stage.width / 2; + topContent.y = config.stage.height / 2; + this.addChild(topContent); + + /** + * Topボタンを生成して、座標をセット + * Create Top button and set coordinates + */ + const topBtnMolecule = new TopBtnMolecule(vm.getTopText()); + topBtnMolecule.alpha = 0; + topBtnMolecule.x = config.stage.width / 2; + topBtnMolecule.y = config.stage.height / 2 + topContent.height / 2 + topBtnMolecule.height; + + /** + * アニメーションが完了するまでボタンを無効化 + * Disable button until animation is complete + */ + topBtnMolecule.disable(); + + /** + * ボタンのクリックイベントをViewModelに送信 + * Send button click event to ViewModel + */ + topBtnMolecule.addEventListener(PointerEvent.POINTER_UP, async (): Promise => { + await vm.onClickStartButton(); + }); + + /** + * Topボタンを画面に追加 + * Add Top button to the screen + */ + this.addChild(topBtnMolecule); + this._topBtnMolecule = topBtnMolecule; + } + + /** + * @description ページ表示時の処理 + * Processing when the page is displayed + * + * @return {Promise} + * @method + * @public + */ + async onEnter (): Promise { + this._topBtnMolecule?.show((): void => { + this._topBtnMolecule.enable(); + }); + } +} \ No newline at end of file diff --git a/template/src/ui/component/template/.gitkeep b/template/src/ui/component/template/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/template/src/ui/content/HomeContent.test.ts b/template/src/ui/content/HomeContent.test.ts new file mode 100644 index 0000000..2cd3f43 --- /dev/null +++ b/template/src/ui/content/HomeContent.test.ts @@ -0,0 +1,82 @@ +import { describe, it, expect, vi } from "vitest"; + +// @next2d/framework モジュールをモック +vi.mock("@next2d/framework", () => ({ + MovieClipContent: vi.fn().mockImplementation(function(this: any) { + this.startDrag = vi.fn(); + this.stopDrag = vi.fn(); + }) +})); + +import { HomeContent } from "./HomeContent"; + +/** + * @description HomeContent のテスト + * Tests for HomeContent + */ +describe("HomeContent", () => { + /** + * @description コンストラクタのテスト + * Test for constructor + */ + describe("Constructor / コンストラクタ", () => { + it("インスタンスが正常に生成されること", () => { + const content = new HomeContent(); + expect(content).toBeInstanceOf(HomeContent); + }); + }); + + /** + * @description namespace プロパティのテスト + * Test for namespace property + */ + describe("namespace Property / namespace プロパティ", () => { + it("namespace が 'HomeContent' を返すこと", () => { + const content = new HomeContent(); + expect(content.namespace).toBe("HomeContent"); + }); + + it("namespace が readonly であること", () => { + const content = new HomeContent(); + // getter のみなので readonly + expect(content.namespace).toBe("HomeContent"); + }); + }); + + /** + * @description IDraggable インターフェースのテスト + * Test for IDraggable interface + */ + describe("IDraggable Interface / IDraggable インターフェース", () => { + it("startDrag メソッドを持つこと", () => { + const content = new HomeContent(); + expect(typeof content.startDrag).toBe("function"); + }); + + it("stopDrag メソッドを持つこと", () => { + const content = new HomeContent(); + expect(typeof content.stopDrag).toBe("function"); + }); + + it("startDrag を呼び出せること", () => { + const content = new HomeContent(); + expect(() => content.startDrag()).not.toThrow(); + }); + + it("stopDrag を呼び出せること", () => { + const content = new HomeContent(); + expect(() => content.stopDrag()).not.toThrow(); + }); + }); + + /** + * @description MovieClipContent 継承のテスト + * Test for MovieClipContent inheritance + */ + describe("MovieClipContent Inheritance / MovieClipContent 継承", () => { + it("MovieClipContent を継承していること", () => { + const content = new HomeContent(); + expect(content).toBeInstanceOf(HomeContent); + }); + }); +}); diff --git a/template/src/model/application/content/HomeContent.ts b/template/src/ui/content/HomeContent.ts similarity index 62% rename from template/src/model/application/content/HomeContent.ts rename to template/src/ui/content/HomeContent.ts index 50ee595..9a0d3a3 100644 --- a/template/src/model/application/content/HomeContent.ts +++ b/template/src/ui/content/HomeContent.ts @@ -1,12 +1,13 @@ +import type { IDraggable } from "@/interface/IDraggable"; import { MovieClipContent } from "@next2d/framework"; /** * @see file/sample.n2d * @class * @extends {MovieClipContent} + * @implements {IDraggable} */ -export class HomeContent extends MovieClipContent -{ +export class HomeContent extends MovieClipContent implements IDraggable { /** * @return {string} * @readonly diff --git a/template/src/ui/content/README.md b/template/src/ui/content/README.md new file mode 100644 index 0000000..1210ab6 --- /dev/null +++ b/template/src/ui/content/README.md @@ -0,0 +1,197 @@ +# Animation Tool Content + +Next2D Animation Toolで作成されたコンテンツを格納するディレクトリです。 + +Directory for storing content created with the Next2D Animation Tool. + +## 概要 / Overview + +Animation Toolで作成したアニメーションをTypeScriptクラスとしてラップし、アプリケーションで使用できるようにします。 + +Wraps animations created with the Animation Tool as TypeScript classes for use in the application. + +## ディレクトリ構造 / Directory Structure + +``` +content/ +├── HomeContent.ts # Home画面用 +└── TopContent.ts # Top画面用 +``` + +## コンテンツの仕組み / How Content Works + +### 1. Animation Toolでn2dファイルを作成 + +Animation Toolでアニメーションを作成し、`.n2d` ファイルとしてエクスポートします。エクスポートしたファイルは `file/` ディレクトリに配置します。 + +Create animations in the Animation Tool and export as `.n2d` files. Place exported files in the `file/` directory. + +### 2. TypeScriptクラスでラップ + +`.n2d` ファイル内のシンボルをTypeScriptクラスとして定義します。 + +Define symbols from `.n2d` files as TypeScript classes. + +### 3. コンポーネントで使用 + +作成したContentクラスをMoleculeなどのコンポーネントで使用します。 + +Use the created Content classes in components such as Molecules. + +## 実装例 / Implementation Example + +### HomeContent.ts + +```typescript +import { MovieClipContent } from "@next2d/framework"; +import type { IDraggable } from "@/interface/IDraggable"; + +/** + * @description Home画面用のアニメーションコンテンツ + * Animation content for Home screen + * + * @class + * @extends {MovieClipContent} + * @implements {IDraggable} + */ +export class HomeContent extends MovieClipContent implements IDraggable +{ + /** + * @description Animation Toolのシンボル名を返す + * Returns the Animation Tool symbol name + * + * @return {string} + * @readonly + */ + get namespace (): string + { + return "HomeContent"; // Animation Toolで設定したシンボル名 + } + + // IDraggableのメソッド(startDrag/stopDrag)は + // MovieClipContentの親クラス(MovieClip)から継承されます + // The IDraggable methods (startDrag/stopDrag) are inherited + // from MovieClipContent's parent class (MovieClip) +} +``` + +### TopContent.ts + +```typescript +import { MovieClipContent } from "@next2d/framework"; + +/** + * @description Top画面用のアニメーションコンテンツ + * Animation content for Top screen + * + * @class + * @extends {MovieClipContent} + */ +export class TopContent extends MovieClipContent +{ + /** + * @description Animation Toolのシンボル名を返す + * Returns the Animation Tool symbol name + * + * @return {string} + * @readonly + */ + get namespace (): string + { + return "TopContent"; + } +} +``` + +## 設計原則 / Design Principles + +### 1. MovieClipContentの継承 / Extend MovieClipContent + +すべてのコンテンツクラスは `MovieClipContent` を継承します。 + +All content classes extend `MovieClipContent`. + +```typescript +import { MovieClipContent } from "@next2d/framework"; + +export class YourContent extends MovieClipContent { + get namespace(): string { + return "YourSymbolName"; + } +} +``` + +### 2. namespaceプロパティ / namespace Property + +`namespace` ゲッターでAnimation Toolで設定したシンボル名を返します。 + +Return the symbol name set in the Animation Tool with the `namespace` getter. + +```typescript +get namespace(): string { + return "HomeContent"; // Animation Toolのシンボル名と一致させる +} +``` + +### 3. インターフェース実装 / Interface Implementation + +必要に応じてインターフェースを実装し、ビジネスロジック層と連携します。 + +Implement interfaces as needed to integrate with the business logic layer. + +```typescript +export class HomeContent extends MovieClipContent implements IDraggable { + // IDraggableメソッドはMovieClipContentの親クラスから継承 +} +``` + +## 新しいContentの追加 / Adding New Content + +### 手順 / Steps + +1. Animation Toolでシンボルを作成 +2. `.n2d` ファイルを `file/` ディレクトリに配置 +3. Contentクラスを作成(`namespace` はシンボル名と一致させる) +4. Moleculeなどのコンポーネントで使用 + +### テンプレート / Template + +```typescript +import { MovieClipContent } from "@next2d/framework"; + +/** + * @description [コンテンツの説明] + * [Content description] + * + * @class + * @extends {MovieClipContent} + */ +export class YourContent extends MovieClipContent +{ + /** + * @description Animation Toolのシンボル名を返す + * Returns the Animation Tool symbol name + * + * @return {string} + * @readonly + */ + get namespace (): string + { + return "YourSymbolName"; // Animation Toolで設定した名前 + } +} +``` + +## ベストプラクティス / Best Practices + +1. **命名規則** - クラス名とシンボル名を一致させる +2. **インターフェース** - 必要な機能はインターフェースで定義 +3. **責務の分離** - アニメーションの制御のみを担当 +4. **ドキュメント** - シンボル名をJSDocに記載 + +## 関連ドキュメント / Related Documentation + +- [../../file/README.md](../../../file/README.md) - n2dファイルの格納場所 +- [../component/README.md](../component/README.md) - UIコンポーネント +- [../README.md](../README.md) - UI全体の説明 +- [Next2D Animation Tool](https://tool.next2d.app/) - Animation Toolの使い方 diff --git a/template/src/ui/content/TopContent.test.ts b/template/src/ui/content/TopContent.test.ts new file mode 100644 index 0000000..296ec7f --- /dev/null +++ b/template/src/ui/content/TopContent.test.ts @@ -0,0 +1,55 @@ +import { describe, it, expect, vi } from "vitest"; + +// @next2d/framework モジュールをモック +vi.mock("@next2d/framework", () => ({ + MovieClipContent: vi.fn().mockImplementation(function(this: any) { + this.height = 100; + }) +})); + +import { TopContent } from "./TopContent"; + +/** + * @description TopContent のテスト + * Tests for TopContent + */ +describe("TopContent", () => { + /** + * @description コンストラクタのテスト + * Test for constructor + */ + describe("Constructor / コンストラクタ", () => { + it("インスタンスが正常に生成されること", () => { + const content = new TopContent(); + expect(content).toBeInstanceOf(TopContent); + }); + }); + + /** + * @description namespace プロパティのテスト + * Test for namespace property + */ + describe("namespace Property / namespace プロパティ", () => { + it("namespace が 'TopContent' を返すこと", () => { + const content = new TopContent(); + expect(content.namespace).toBe("TopContent"); + }); + + it("namespace が readonly であること", () => { + const content = new TopContent(); + // getter のみなので readonly + expect(content.namespace).toBe("TopContent"); + }); + }); + + /** + * @description MovieClipContent 継承のテスト + * Test for MovieClipContent inheritance + */ + describe("MovieClipContent Inheritance / MovieClipContent 継承", () => { + it("MovieClipContent を継承していること", () => { + const content = new TopContent(); + expect(content).toBeInstanceOf(TopContent); + }); + }); +}); diff --git a/template/src/model/application/content/TopContent.ts b/template/src/ui/content/TopContent.ts similarity index 84% rename from template/src/model/application/content/TopContent.ts rename to template/src/ui/content/TopContent.ts index d6564d4..90f05be 100644 --- a/template/src/model/application/content/TopContent.ts +++ b/template/src/ui/content/TopContent.ts @@ -5,8 +5,7 @@ import { MovieClipContent } from "@next2d/framework"; * @class * @extends {MovieClipContent} */ -export class TopContent extends MovieClipContent -{ +export class TopContent extends MovieClipContent { /** * @return {string} * @readonly diff --git a/template/src/view/README.md b/template/src/view/README.md index 1260c46..a75ec83 100644 --- a/template/src/view/README.md +++ b/template/src/view/README.md @@ -1,110 +1,894 @@ # View and ViewModel -1画面にViewとViewModelをワンセット作成するのが基本スタイルです。ディレクトリ構成はキャメルケースの最初のブロックで作成するのを推奨しています。 +1画面にViewとViewModelをワンセット作成するのが基本スタイルです。ディレクトリ構成はキャメルケースの最初のブロックで作成するのを推奨しています。 -The basic style is to create one set of View and ViewModel per screen. It is recommended that the directory structure be organized using the first segment in camelCase. +The basic style is to create one set of View and ViewModel per screen. It is recommended that the directory structure be organized using the first segment in camelCase. + +## アーキテクチャ / Architecture + +このプロジェクトは **MVVM (Model-View-ViewModel)** パターンを採用しています。 + +This project adopts the **MVVM (Model-View-ViewModel)** pattern. + +```mermaid +graph TB + subgraph ViewLayer["🎨 View Layer"] + direction TB + ViewRole["画面の構造と表示を担当
Screen structure and display"] + ViewRule["ビジネスロジックは持たない
No business logic"] + end + + subgraph ViewModelLayer["⚙️ ViewModel Layer"] + direction TB + VMRole1["ViewとModelの橋渡し
Bridge between View and Model"] + VMRole2["UseCaseを保持
Holds UseCases"] + VMRole3["イベントハンドリング
Event handling"] + end + + subgraph InterfaceLayer["📋 Interface Layer"] + direction TB + InterfaceDesc["抽象化レイヤー
Abstraction layer"] + end + + subgraph ModelLayer["💎 Model Layer"] + direction TB + ModelRole1["ビジネスロジック
UseCase"] + ModelRole2["データアクセス
Repository"] + end + + ViewLayer <-->|双方向
Bidirectional| ViewModelLayer + ViewModelLayer -->|Interface経由
Via Interface| InterfaceLayer + InterfaceLayer <--> ModelLayer + + classDef viewStyle fill:#e1f5ff,stroke:#01579b,stroke-width:2px + classDef vmStyle fill:#f3e5f5,stroke:#4a148c,stroke-width:2px + classDef interfaceStyle fill:#fff9c4,stroke:#f57f17,stroke-width:2px + classDef modelStyle fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px + + class ViewLayer,ViewRole,ViewRule viewStyle + class ViewModelLayer,VMRole1,VMRole2,VMRole3 vmStyle + class InterfaceLayer,InterfaceDesc interfaceStyle + class ModelLayer,ModelRole1,ModelRole2 modelStyle +``` + +### MVVMパターンの流れ / MVVM Pattern Flow + +```mermaid +sequenceDiagram + participant User as 👤 User + participant View as View + participant VM as ViewModel + participant UC as UseCase + participant Repo as Repository + + User->>View: 1. ユーザー操作
User action + View->>VM: 2. イベント通知
Event notification + activate VM + VM->>UC: 3. ビジネスロジック実行
Execute business logic + activate UC + UC->>Repo: 4. データ取得
Fetch data + activate Repo + Repo-->>UC: 5. データ返却
Return data + deactivate Repo + UC-->>VM: 6. 処理結果
Result + deactivate UC + VM->>View: 7. 状態更新
Update state + deactivate VM + View->>User: 8. UI更新
Update UI + + Note over View,Repo: Interface経由で疎結合
Loosely coupled via interfaces +``` ## Example of directory structure -```sh -project -└── src - └── view - ├── top - │ ├── TopView.js - │ └── TopViewModel.js - └── home - ├── HomeView.js - └── HomeViewModel.js +``` +src/ +└── view/ + ├── top/ + │ ├── TopView.ts + │ └── TopViewModel.ts + └── home/ + ├── HomeView.ts + └── HomeViewModel.ts ``` ## Generator -複数のViewクラス、及び、ViewModelクラスを生成する際は、以下のコマンドで自動生成する事をお勧めします。このコマンドは `routing.json` のトッププロパティの値を分解し、`view` ディレクトリ直下に対象のディレクトリがなければディレクトリを作成し、ViewとViewModelが存在しない場合のみ新規でクラスを生成します。 +複数のViewクラス、及び、ViewModelクラスを生成する際は、以下のコマンドで自動生成する事をお勧めします。このコマンドは `routing.json` のトッププロパティの値を分解し、`view` ディレクトリ直下に対象のディレクトリがなければディレクトリを作成し、ViewとViewModelが存在しない場合のみ新規でクラスを生成します。 -When generating multiple View and ViewModel classes, it is recommended to use the following command for auto-generation. This command parses the top-level property values in `routing.json`, creates the target directories under the `view` directory if they do not exist, and generates new classes only if the corresponding View and ViewModel classes are missing. +When generating multiple View and ViewModel classes, it is recommended to use the following command for auto-generation. This command parses the top-level property values in `routing.json`, creates the target directories under the `view` directory if they do not exist, and generates new classes only if the corresponding View and ViewModel classes are missing. ```sh npm run generate ``` ## View Class -メインコンテキストにアタッチされるコンテナです。その為、記述は至ってシンプルで、 `routing.json` で設定した値のキャメルケースでファイルを作成し、Viewクラスを継承するのが基本のスタイルです。起動時に `initialize` 関数がコールされます。ですが、特殊な要件がない限り、Viewでロジックを組む事はありません。 - -It is a container attached to the main context. Therefore, its implementation is kept very simple: files are created using the camelCase version of the values specified in `routing.json`, and the basic style is to extend the View class. The `initialize` function is called at startup; however, unless there are special requirements, no logic should be implemented in the View. - + +メインコンテキストにアタッチされるコンテナです。その為、記述は至ってシンプルで、 `routing.json` で設定した値のキャメルケースでファイルを作成し、Viewクラスを継承するのが基本のスタイルです。起動時に `initialize` 関数がコールされます。Viewは表示構造のみを担当し、ビジネスロジックはViewModelに委譲します。 + +It is a container attached to the main context. Therefore, its implementation is kept very simple: files are created using the camelCase version of the values specified in `routing.json`, and the basic style is to extend the View class. The `initialize` function is called at startup. The View handles only the display structure and delegates business logic to the ViewModel. + +### View の責務 / View Responsibilities + +- ✅ **画面の構造定義** - UIコンポーネントの配置と座標設定 +- ✅ **イベントリスナーの登録** - ViewModelのメソッドと接続 +- ✅ **ライフサイクル管理** - `initialize`, `onEnter`, `onExit` +- ❌ **ビジネスロジック** - ViewModelに委譲 +- ❌ **データアクセス** - Repositoryに委譲 +- ❌ **状態管理** - ViewModelに委譲 + +### ライフサイクル / Lifecycle + +Viewには3つの主要なライフサイクルメソッドがあります。各メソッドは特定のタイミングで自動的に呼び出されます。 + +Views have three main lifecycle methods. Each method is automatically called at a specific timing. + +```mermaid +sequenceDiagram + participant Framework as Framework + participant View as View + participant VM as ViewModel + participant UI as UI Components + + Note over Framework,UI: 画面遷移開始 / Screen transition starts + + Framework->>View: new View(vm) + activate View + Framework->>View: initialize() + View->>UI: Create components + View->>UI: Set positions + View->>VM: Register event listeners + Note over View: UIコンポーネントの構築
Build UI components + + Framework->>View: onEnter() + activate View + View->>UI: Start animations + View->>VM: Initialize data + Note over View: 画面表示時の処理
On screen shown + deactivate View + + Note over Framework,UI: ユーザーが画面を操作 / User interacts + + Note over Framework,UI: 別の画面へ遷移 / Navigate to another screen + + Framework->>View: onExit() + activate View + View->>UI: Stop animations + View->>VM: Clean up listeners + Note over View: 画面非表示時の処理
On screen hidden + deactivate View + deactivate View +``` + +#### 1. initialize() - 初期化 + +**呼び出しタイミング / When Called:** +- Viewのインスタンスが生成された直後、画面が表示される前 +- 画面遷移時に1回だけ呼び出される +- `onEnter()` より前に実行される + +After the View instance is created, before the screen is displayed. Called only once during screen transition. Executed before `onEnter()`. + +**主な用途 / Primary Usage:** +- ✅ UIコンポーネントの生成と配置 +- ✅ イベントリスナーの登録 +- ✅ 子要素の追加(`addChild`) +- ✅ 初期レイアウトの設定 + +**コード例 / Code Example:** + +```typescript +async initialize(): Promise { + // 1. コンポーネントの生成 + const homeContent = new HomeBtnMolecule(); + + // 2. 位置の設定 + homeContent.x = 120; + homeContent.y = 120; + + // 3. イベントリスナーの登録 + homeContent.addEventListener( + PointerEvent.POINTER_DOWN, + this.vm.homeContentPointerDownEvent + ); + + // 4. 表示リストに追加 + this.addChild(homeContent); + + // 5. テキストフィールドの作成 + const textField = new TextAtom("Hello, World!"); + textField.y = 50; + this.addChild(textField); +} +``` + +#### 2. onEnter() - 画面表示時 + +**呼び出しタイミング / When Called:** +- `initialize()` の実行完了後 +- 画面が実際に表示される直前 +- 画面遷移のたびに毎回呼び出される + +After `initialize()` completes. Just before the screen is actually displayed. Called every time during screen transition. + +**主な用途 / Primary Usage:** +- ✅ 入場アニメーションの開始 +- ✅ データの取得・更新 +- ✅ タイマーやインターバルの開始 +- ✅ フォーカス設定 +- ✅ 背景音楽の再生開始 + +**コード例 / Code Example:** + +```typescript +async onEnter(): Promise { + // 1. 入場アニメーションの再生 + const topBtn = this.getChildByName("topBtn") as TopBtnMolecule; + topBtn.playEntrance(() => { + console.log("Entrance animation completed"); + }); + + // 2. データの取得(ViewModelに委譲) + await this.vm.fetchInitialData(); + + // 3. タイマーの開始 + this.startAutoSlideTimer(); + + // 4. アクティブ状態の設定 + this.isActive = true; +} +``` + +#### 3. onExit() - 画面非表示時 + +**呼び出しタイミング / When Called:** +- 別の画面に遷移する直前 +- 画面が非表示になる時 +- Viewが破棄される前 + +Just before transitioning to another screen. When the screen is hidden. Before the View is destroyed. + +**主な用途 / Primary Usage:** +- ✅ アニメーションの停止 +- ✅ タイマーやインターバルのクリア +- ✅ イベントリスナーの削除(必要に応じて) +- ✅ リソースの解放 +- ✅ 背景音楽の停止 +- ✅ 一時データのクリア + +**コード例 / Code Example:** + +```typescript +async onExit(): Promise { + // 1. アニメーションの停止 + const animations = this.getAnimations(); + animations.forEach(anim => anim.stop()); + + // 2. タイマーのクリア + if (this.autoSlideTimer) { + clearInterval(this.autoSlideTimer); + this.autoSlideTimer = null; + } + + // 3. 不要なイベントリスナーの削除(必要に応じて) + // ※ Viewが破棄される場合は自動的に削除されるため通常不要 + + // 4. 一時データのクリア + this.tempData = null; + + // 5. 非アクティブ状態に設定 + this.isActive = false; +} +``` + +### ライフサイクルの注意点 / Lifecycle Notes + +#### ✅ すべきこと / Do + +1. **initialize()** - UIの構築のみ、データ取得は避ける +2. **onEnter()** - アニメーション、データ取得、タイマー開始 +3. **onExit()** - リソース解放、タイマー停止 + +#### ❌ すべきでないこと / Don't + +1. **initialize()** - 重い処理、API呼び出し(画面表示が遅くなる) +2. **onEnter()** - UIコンポーネントの生成(`initialize()`で行う) +3. **onExit()** - 新しいリソースの作成 + ### Example of View class source -```javascript +```typescript +import type { HomeViewModel } from "./HomeViewModel"; import { View } from "@next2d/framework"; +import { HomeBtnMolecule } from "@/ui/component/molecule/HomeBtnMolecule"; +import { TextAtom } from "@/ui/component/atom/TextAtom"; +import { PointerEvent, Event } from "@next2d/events"; -export class TopView extends View +/** + * @class + * @extends {View} + */ +export class HomeView extends View { + private autoSlideTimer: number | null = null; + private isActive: boolean = false; + /** + * @param {HomeViewModel} vm * @constructor * @public */ - constructor () - { + constructor ( + private readonly vm: HomeViewModel + ) { super(); } + + /** + * @description 画面の初期化 - UIコンポーネントの構築 + * Initialize - Build UI components + * + * @return {Promise} + * @method + * @override + * @public + */ + async initialize (): Promise + { + // UIコンポーネントの作成と配置 + const homeContent = new HomeBtnMolecule(); + homeContent.x = 120; + homeContent.y = 120; + homeContent.name = "homeContent"; + + // イベントをViewModelに委譲 + homeContent.addEventListener( + PointerEvent.POINTER_DOWN, + this.vm.homeContentPointerDownEvent + ); + + this.addChild(homeContent); + } + + /** + * @description 画面表示時の処理 - アニメーション開始、データ取得 + * On screen shown - Start animations, fetch data + * + * @return {Promise} + * @method + * @override + * @public + */ + async onEnter (): Promise + { + // アニメーション開始 + const homeContent = this.getChildByName("homeContent") as HomeBtnMolecule; + if (homeContent && homeContent.playEntrance) { + homeContent.playEntrance(() => { + console.log("Entrance animation completed"); + }); + } + + // データ取得(ViewModelに委譲) + await this.vm.initialize(); + + // アクティブ状態に設定 + this.isActive = true; + } + + /** + * @description 画面非表示時の処理 - クリーンアップ + * On screen hidden - Clean up resources + * + * @return {Promise} + * @method + * @override + * @public + */ + async onExit (): Promise + { + // タイマーのクリア + if (this.autoSlideTimer) { + clearInterval(this.autoSlideTimer); + this.autoSlideTimer = null; + } + + // 非アクティブ状態に設定 + this.isActive = false; + } } ``` +### View のライフサイクル / View Lifecycle + +1. **コンストラクタ** - ViewModelをインジェクション +2. **initialize()** - UIコンポーネントの作成と配置 +3. **onEnter()** - 画面表示時の処理(アニメーション開始など) +4. **onExit()** - 画面非表示時の処理(クリーンアップなど) + ## ViewModel Class -画面遷移するタイミングで終了処理として `unbind` 関数がコールされ、表示の開始時に `bind` 関数がコールされます。Viewに任意のDisplayObjectをbindするのが、ViewModelの役割です。今回のテンプレートでは、ViewModelは `model/ui/component/template/{{page}}/*.[ts|js]` のアクセスのみ許可するスタイルで作成しています。 -During screen transitions, the `unbind` function is called as part of the cleanup process, and the `bind` function is called when the display starts. The role of the ViewModel is to bind an arbitrary DisplayObject to the View. In this template, the ViewModel is designed to only allow access to files in `model/ui/component/template/{{page}}/*.[ts|js]`. +ViewとModelの橋渡しを行います。UseCaseを保持し、Viewからのイベントを処理してビジネスロジックを実行します。ViewModelは依存性注入パターンを使用し、コンストラクタでUseCaseのインスタンスを生成します。 + +Acts as a bridge between View and Model. Holds UseCases and processes events from View to execute business logic. ViewModel uses the dependency injection pattern, creating UseCase instances in the constructor. + +### ViewModel の責務 / ViewModel Responsibilities + +- ✅ **イベント処理** - Viewからのイベントを受け取る +- ✅ **UseCaseの実行** - ビジネスロジックを呼び出す +- ✅ **依存性の管理** - UseCaseのインスタンスを保持 +- ✅ **状態管理** - 画面固有の状態を管理(必要に応じて) +- ❌ **UI操作** - Viewに委譲 +- ❌ **ビジネスロジック** - UseCaseに委譲 + +### ライフサイクル / Lifecycle + +ViewModelには主要なライフサイクルメソッドがあります。重要なのは、**ViewModelの`initialize()`はViewの`initialize()`より前に呼び出される**という点です。 + +ViewModel has key lifecycle methods. Importantly, **ViewModel's `initialize()` is called before View's `initialize()`**. + +```mermaid +sequenceDiagram + participant Framework as Framework + participant VM as ViewModel + participant View as View + participant UC as UseCase + participant UI as UI Components + + Note over Framework,UI: 画面遷移開始 / Screen transition starts + + Framework->>VM: new ViewModel() + activate VM + VM->>UC: Create UseCases + Note over VM: コンストラクタで
UseCaseを生成 + + Framework->>VM: initialize() + VM->>UC: Fetch initial data + Note over VM: データ取得など
事前準備を実施 + + Framework->>View: new View(vm) + activate View + + Framework->>View: initialize() + View->>UI: Create components + View->>VM: Register event listeners + Note over View: UIコンポーネント
の構築 + + Framework->>View: onEnter() + View->>UI: Start animations + View->>VM: Notify ready + Note over View: 画面表示処理 + + Note over Framework,UI: ユーザーが操作 / User interacts + + Framework->>View: onExit() + View->>UI: Stop animations + View->>VM: Clean up + deactivate View + deactivate VM +``` + +#### 実行順序 / Execution Order + +``` +1. ViewModel のインスタンス生成 + ↓ +2. ViewModel.initialize() ⭐ ViewModelが先 + ↓ +3. View のインスタンス生成(ViewModelを注入) + ↓ +4. View.initialize() + ↓ +5. View.onEnter() + ↓ + (ユーザー操作) + ↓ +6. View.onExit() +``` + +#### ViewModel.initialize() の詳細 + +**呼び出しタイミング / When Called:** +- ViewModelのインスタンス生成直後 +- **Viewの`initialize()`より前** +- 画面遷移時に1回だけ呼び出される + +After ViewModel instance is created. **Before View's `initialize()`**. Called only once during screen transition. + +**主な用途 / Primary Usage:** +- ✅ 初期データの取得 +- ✅ 共通設定の読み込み +- ✅ 状態の初期化 +- ✅ Repositoryからのデータフェッチ +- ❌ UI操作(まだViewが存在しない) + +**コード例 / Code Example:** + +```typescript +async initialize(): Promise { + // 1. 初期データの取得 + try { + const data = await HomeTextRepository.get(); + this.homeText = data.word; + } catch (error) { + console.error('Failed to fetch initial data:', error); + this.homeText = 'Hello, World!'; // フォールバック + } + + // 2. 状態の初期化 + this.isLoading = false; + this.errorMessage = null; + + // 3. 共通設定の読み込み + this.config = await this.loadConfig(); +} +``` + +**重要な注意点 / Important Notes:** + +```typescript +// ✅ 良い例: データ取得とビジネスロジックの準備 +async initialize(): Promise { + // データ取得: Viewが表示される前に完了 + const data = await this.fetchHomeTextUseCase.execute(); + this.homeText = data.word; +} + +// ❌ 悪い例: UI操作(まだViewが存在しない) +async initialize(): Promise { + // NG: この時点ではまだViewのinitialize()が呼ばれていない + const textField = this.view.getTextField(); // エラー! + textField.text = "Hello"; +} +``` + +#### ViewModelのライフサイクルメソッド比較 + +| メソッド | 呼び出しタイミング | 主な用途 | View参照 | +|---------|-----------------|---------|---------| +| `constructor()` | インスタンス生成時 | UseCaseの生成 | ❌ 不可 | +| `initialize()` | Viewより**前** | データ取得、状態初期化 | ❌ 不可 | +| イベントハンドラ | ユーザー操作時 | ビジネスロジック実行 | ✅ 可能 | ### Example of ViewModel class source -```javascript +```typescript +import type { IDraggable } from "@/interface/IDraggable"; +import type { ITextField } from "@/interface/ITextField"; import { ViewModel } from "@next2d/framework"; -import { TopContentTemplate } from "@/model/ui/component/template/top/TopContentTemplate"; -import { TopButtonTemplate } from "@/model/ui/component/template/top/TopButtonTemplate"; +import type { PointerEvent, Event } from "@next2d/events"; +import { StartDragUseCase } from "@/model/application/home/usecase/StartDragUseCase"; +import { StopDragUseCase } from "@/model/application/home/usecase/StopDragUseCase"; +import { CenterTextFieldUseCase } from "@/model/application/home/usecase/CenterTextFieldUseCase"; +import { HomeTextRepository } from "@/model/infrastructure/repository/HomeTextRepository"; /** * @class * @extends {ViewModel} */ -export class TopViewModel extends ViewModel +export class HomeViewModel extends ViewModel { + // 依存性注入: UseCaseのインスタンスを保持 + private readonly startDragUseCase: StartDragUseCase; + private readonly stopDragUseCase: StopDragUseCase; + private readonly centerTextFieldUseCase: CenterTextFieldUseCase; + + // 画面の状態管理 + private homeText: string = ""; + private isLoading: boolean = true; + + /** + * @description ViewModelの初期化とUseCaseの注入 + * Initialize ViewModel and inject UseCases + * + * @constructor + * @public + */ + constructor () + { + super(); + + // UseCaseのインスタンスを生成 + this.startDragUseCase = new StartDragUseCase(); + this.stopDragUseCase = new StopDragUseCase(); + this.centerTextFieldUseCase = new CenterTextFieldUseCase(); + } + /** - * @param {View} view + * @description ViewModelの初期化 - データ取得と状態準備 + * Initialize ViewModel - Fetch data and prepare state + * ⭐ Viewのinitialize()より前に呼ばれる + * * @return {Promise} * @method + * @override + * @public + */ + async initialize (): Promise + { + // 初期データの取得(Viewが表示される前に完了) + try { + const data = await HomeTextRepository.get(); + this.homeText = data.word; + this.isLoading = false; + } catch (error) { + console.error('Failed to fetch home text:', error); + this.homeText = 'Hello, World!'; + this.isLoading = false; + } + } + + /** + * @description 取得したテキストを返す + * Return fetched text + * + * @return {string} + * @method + * @public + */ + getHomeText (): string + { + return this.homeText; + } + + /** + * @description ドラッグ開始イベントのハンドラ + * Handler for drag start event + * + * @param {PointerEvent} event + * @return {void} + * @method * @public */ - async bind (view) + homeContentPointerDownEvent (event: PointerEvent): void { - /** - * ロゴアニメーションをAnimation ToolのJSONから生成 - * Logo animation generated from Animation Tool's JSON - */ - const topContent = new TopContentTemplate().factory(); - view.addChild(topContent); + // インターフェースを通じてターゲットを取得 + const target = event.currentTarget as unknown as IDraggable; + + // UseCaseを実行 + this.startDragUseCase.execute(target); + } - /** - * ボタンエリアを生成 - * Generate button area - */ - const button = new TopButtonTemplate().factory(topContent); - view.addChild(button); + /** + * @description ドラッグ停止イベントのハンドラ + * Handler for drag stop event + * + * @param {PointerEvent} event + * @return {void} + * @method + * @public + */ + homeContentPointerUpEvent (event: PointerEvent): void + { + const target = event.currentTarget as unknown as IDraggable; + this.stopDragUseCase.execute(target); } /** - * @param {View} view - * @return {Promise} + * @description テキスト変更イベントのハンドラ + * Handler for text change event + * + * @param {Event} event + * @return {void} * @method * @public */ - unbind (view) + homeTextChangeEvent (event: Event): void { - /** - * unbind関数を利用しなければ削除 - * Delete if unbind function is not used - */ - return super.unbind(view); + const textField = event.currentTarget as unknown as ITextField; + this.centerTextFieldUseCase.execute(textField); + } +} +``` + +### ViewModelとViewの連携 / ViewModel and View Coordination + +ViewModelの`initialize()`で取得したデータをViewで使用する例: + +Example of using data fetched in ViewModel's `initialize()` from View: + +```typescript +// HomeViewModel.ts +export class HomeViewModel extends ViewModel { + private homeText: string = ""; + + async initialize(): Promise { + // ViewModelのinitializeで事前にデータ取得 + const data = await HomeTextRepository.get(); + this.homeText = data.word; + } + + getHomeText(): string { + return this.homeText; + } +} + +// HomeView.ts +export class HomeView extends View { + constructor(private readonly vm: HomeViewModel) { + super(); + } + + async initialize(): Promise { + // この時点でvm.initialize()は既に完了している + const text = this.vm.getHomeText(); + + // 取得済みのデータを使ってUIを構築 + const textField = new TextAtom(text); + this.addChild(textField); + } +} +``` + +## 設計原則 / Design Principles + +### 1. 関心の分離 / Separation of Concerns + +```typescript +// ✅ 良い例: Viewは表示のみ、ViewModelはロジック +class HomeView extends View { + async initialize() { + // UI構築のみ + const btn = new HomeBtnMolecule(); + btn.addEventListener(PointerEvent.POINTER_DOWN, this.vm.onClick); + } +} + +class HomeViewModel extends ViewModel { + onClick(event: PointerEvent) { + // ビジネスロジック実行 + this.someUseCase.execute(); + } +} + +// ❌ 悪い例: Viewにビジネスロジック +class HomeView extends View { + async initialize() { + const btn = new HomeBtnMolecule(); + btn.addEventListener(PointerEvent.POINTER_DOWN, () => { + // NG: Viewでビジネスロジック実行 + const data = await Repository.get(); + this.processData(data); + }); + } +} +``` + +### 2. 依存性の逆転 / Dependency Inversion + +ViewModelはインターフェースに依存し、具象クラスに依存しません。 + +ViewModel depends on interfaces, not concrete classes. + +```typescript +// ✅ 良い例: インターフェースに依存 +homeContentPointerDownEvent(event: PointerEvent): void { + const target = event.currentTarget as unknown as IDraggable; + this.startDragUseCase.execute(target); +} + +// ❌ 悪い例: 具象クラスに依存 +homeContentPointerDownEvent(event: PointerEvent): void { + const target = event.currentTarget as HomeBtnMolecule; // NG + target.startDrag(); +} +``` + +### 3. テスタビリティ / Testability + +UseCaseをモックに差し替えることで、ViewModelを独立してテスト可能です。 + +ViewModel can be tested independently by replacing UseCases with mocks. + +```typescript +describe('HomeViewModel', () => { + test('should call UseCase when event is triggered', () => { + // モックUseCaseを作成 + const mockUseCase = { + execute: jest.fn() + }; + + // ViewModelにモックを注入 + const vm = new HomeViewModel(); + vm['startDragUseCase'] = mockUseCase; + + // イベント発火 + const mockEvent = { currentTarget: mockDraggable }; + vm.homeContentPointerDownEvent(mockEvent); + + // UseCaseが呼ばれたか検証 + expect(mockUseCase.execute).toHaveBeenCalled(); + }); +}); +``` + +## ベストプラクティス / Best Practices + +### 1. ViewとViewModelは1対1 + +1つのViewに対して1つのViewModelを作成します。 + +Create one ViewModel for each View. + +### 2. Viewはステートレス + +Viewは状態を持たず、ViewModelから渡されたデータを表示するだけです。 + +View is stateless and only displays data passed from ViewModel. + +### 3. イベントは必ずViewModelに委譲 + +View内でイベント処理を完結させず、必ずViewModelに委譲します。 + +Never handle events entirely within View; always delegate to ViewModel. + +### 4. 型アサーションは慎重に + +`as unknown as` を使う場合は、インターフェースに変換する目的のみで使用します。 + +When using `as unknown as`, only use it to convert to interfaces. + +## 新しいView/ViewModelの作成 / Creating New View/ViewModel + +### 手順 / Steps + +1. **routing.jsonに追加** - 新しいルートを定義 +2. **自動生成** - `npm run generate` を実行 +3. **ViewModelにUseCaseを追加** - コンストラクタで依存性注入 +4. **Viewに表示ロジック追加** - UIコンポーネントの配置 +5. **イベント連携** - ViewからViewModelのメソッドを呼び出し + +### テンプレート / Template + +```typescript +// YourView.ts +import type { YourViewModel } from "./YourViewModel"; +import { View } from "@next2d/framework"; + +export class YourView extends View { + constructor(private readonly vm: YourViewModel) { + super(); + } + + async initialize(): Promise { + // UIコンポーネントの作成と配置 + } + + async onEnter(): Promise { + // 画面表示時の処理 + } + + async onExit(): Promise { + // 画面非表示時の処理 + } +} + +// YourViewModel.ts +import { ViewModel } from "@next2d/framework"; +import { YourUseCase } from "@/model/application/your/usecase/YourUseCase"; + +export class YourViewModel extends ViewModel { + private readonly yourUseCase: YourUseCase; + + constructor() { + super(); + this.yourUseCase = new YourUseCase(); + } + + async initialize(): Promise { + return void 0; + } + + yourEventHandler(event: Event): void { + this.yourUseCase.execute(); } } ``` + +## 関連ドキュメント / Related Documentation + +- [ARCHITECTURE.md](../../ARCHITECTURE.md) - アーキテクチャ全体の説明 +- [model/README.md](../model/README.md) - Model層の説明 +- [interface/README.md](../interface/README.md) - インターフェース定義 +- [ui/README.md](../ui/README.md) - UIコンポーネント +- [config/README.md](../config/README.md) - ルーティング設定 diff --git a/template/src/view/home/HomeView.ts b/template/src/view/home/HomeView.ts index 3e21750..eb9057b 100644 --- a/template/src/view/home/HomeView.ts +++ b/template/src/view/home/HomeView.ts @@ -1,17 +1,62 @@ +import type { HomeViewModel } from "./HomeViewModel"; import { View } from "@next2d/framework"; +import { HomePage } from "@/ui/component/page/home/HomePage"; /** * @class * @extends {View} */ -export class HomeView extends View -{ +export class HomeView extends View { + + /** + * @private + * @readonly + */ + private readonly _homePage: HomePage; + /** + * @param {HomeViewModel} vm * @constructor * @public */ - constructor () + constructor (vm: HomeViewModel) + { + super(vm); + + this._homePage = new HomePage(); + this.addChild(this._homePage); + } + + /** + * @return {Promise} + * @method + * @override + * @public + */ + async initialize (): Promise + { + this._homePage.initialize(this.vm); + } + + /** + * @return {Promise} + * @method + * @override + * @public + */ + async onEnter (): Promise + { + return void 0; + } + + /** + * @return {Promise} + * @method + * @override + * @public + */ + async onExit (): Promise { - super(); + return void 0; } } \ No newline at end of file diff --git a/template/src/view/home/HomeViewModel.ts b/template/src/view/home/HomeViewModel.ts index 76f10c7..f5da420 100644 --- a/template/src/view/home/HomeViewModel.ts +++ b/template/src/view/home/HomeViewModel.ts @@ -1,43 +1,104 @@ -import { View, ViewModel } from "@next2d/framework"; -import { execute as homeButtonTemplate } from "@/model/ui/component/template/home/HomeButtonTemplate"; -import { execute as homeTextTemplate } from "@/model/ui/component/template/home/HomeTextTemplate"; +import type { IDraggable } from "@/interface/IDraggable"; +import type { ITextField } from "@/interface/ITextField"; +import { ViewModel, app } from "@next2d/framework"; +import type { PointerEvent, Event } from "@next2d/events"; +import { StartDragUseCase } from "@/model/application/home/usecase/StartDragUseCase"; +import { StopDragUseCase } from "@/model/application/home/usecase/StopDragUseCase"; +import { CenterTextFieldUseCase } from "@/model/application/home/usecase/CenterTextFieldUseCase"; +import { config } from "@/config/Config"; /** * @class * @extends {ViewModel} */ -export class HomeViewModel extends ViewModel -{ +export class HomeViewModel extends ViewModel { + + private readonly startDragUseCase: StartDragUseCase; + private readonly stopDragUseCase: StopDragUseCase; + private readonly centerTextFieldUseCase: CenterTextFieldUseCase; + private homeText: string = ""; + /** - * @param {View} view - * @return {Promise} - * @method + * @constructor * @public */ - async unbind (view: View): Promise + constructor () { - return super.unbind(view); + super(); + this.startDragUseCase = new StartDragUseCase(); + this.stopDragUseCase = new StopDragUseCase(); + this.centerTextFieldUseCase = new CenterTextFieldUseCase(); } /** - * @param {View} view * @return {Promise} * @method + * @override + * @public + */ + async initialize (): Promise + { + const response = app.getResponse(); + this.homeText = response.has("HomeText") + ? (response.get("HomeText") as { word: string }).word + : ""; + } + + /** + * @description ホームテキストを取得 + * Get home text + * + * @return {string} + * @method + * @public + */ + getHomeText (): string + { + return this.homeText; + } + + /** + * @description ホームコンテンツのポインターダウン時の処理 + * Handle when home content is pointer down + * + * @param {PointerEvent} event + * @return {void} + * @method + * @public + */ + homeContentPointerDownEvent (event: PointerEvent): void + { + const target = event.currentTarget as unknown as IDraggable; + this.startDragUseCase.execute(target); + } + + /** + * @description ホームコンテンツのポインターアップ時の処理 + * Handle when home content is pointer up + * + * @param {PointerEvent} event + * @return {void} + * @method + * @public + */ + homeContentPointerUpEvent (event: PointerEvent): void + { + const target = event.currentTarget as unknown as IDraggable; + this.stopDragUseCase.execute(target); + } + + /** + * @description ホームテキストの変更時の処理 + * Handle when home text is changed + * + * @param {Event} event + * @return {void} + * @method * @public */ - async bind (view: View): Promise + homeTextChangeEvent (event: Event): void { - /** - * アニメーションをAnimation ToolのJSONから生成 - * Generate animation from Animation Tool's JSON - */ - const homeContent = homeButtonTemplate(); - view.addChild(homeContent); - - /** - * Hello, Worldのテキストを生成 - * Generate Hello, World text - */ - view.addChild(homeTextTemplate(homeContent)); + const textField = event.currentTarget as unknown as ITextField; + this.centerTextFieldUseCase.execute(textField, config.stage.width); } } \ No newline at end of file diff --git a/template/src/view/top/TopView.ts b/template/src/view/top/TopView.ts index 5f8c028..8cc2457 100644 --- a/template/src/view/top/TopView.ts +++ b/template/src/view/top/TopView.ts @@ -1,17 +1,62 @@ +import type { TopViewModel } from "./TopViewModel"; import { View } from "@next2d/framework"; +import { TopPage } from "@/ui/component/page/top/TopPage"; /** * @class * @extends {View} */ -export class TopView extends View -{ +export class TopView extends View { + + /** + * @private + * @readonly + */ + private readonly _topPage: TopPage; + /** + * @param {TopViewModel} vm * @constructor * @public */ - constructor () + constructor (vm: TopViewModel) + { + super(vm); + + this._topPage = new TopPage(); + this.addChild(this._topPage); + } + + /** + * @return {Promise} + * @method + * @override + * @public + */ + async initialize (): Promise + { + this._topPage.initialize(this.vm); + } + + /** + * @return {Promise} + * @method + * @override + * @public + */ + async onEnter (): Promise + { + await this._topPage.onEnter(); + } + + /** + * @return {Promise} + * @method + * @override + * @public + */ + async onExit (): Promise { - super(); + return void 0; } } \ No newline at end of file diff --git a/template/src/view/top/TopViewModel.ts b/template/src/view/top/TopViewModel.ts index d78ea54..716e4f3 100644 --- a/template/src/view/top/TopViewModel.ts +++ b/template/src/view/top/TopViewModel.ts @@ -1,43 +1,62 @@ -import { View, ViewModel } from "@next2d/framework"; -import { execute as topContentTemplate } from "@/model/ui/component/template/top/TopContentTemplate"; -import { execute as topButtonTemplate } from "@/model/ui/component/template/top/TopButtonTemplate"; +import { ViewModel, app } from "@next2d/framework"; +import { NavigateToViewUseCase } from "@/model/application/top/usecase/NavigateToViewUseCase"; /** * @class * @extends {ViewModel} */ -export class TopViewModel extends ViewModel -{ +export class TopViewModel extends ViewModel { + + private readonly navigateToViewUseCase: NavigateToViewUseCase; + private topText: string = ""; + /** - * @param {View} view - * @return {Promise} - * @method + * @constructor * @public */ - async unbind (view: View): Promise + constructor () { - return super.unbind(view); + super(); + this.navigateToViewUseCase = new NavigateToViewUseCase(); } /** - * @param {View} view * @return {Promise} * @method + * @override * @public */ - async bind (view: View): Promise + async initialize (): Promise { - /** - * ロゴアニメーションをAnimation ToolのJSONから生成 - * Logo animation generated from Animation Tool JSON - */ - const topContent = topContentTemplate(); - view.addChild(topContent); + const response = app.getResponse(); + this.topText = response.has("TopText") + ? (response.get("TopText") as { word: string }).word + : ""; + } - /** - * ボタンエリアを生成 - * Generate button area - */ - view.addChild(topButtonTemplate(topContent)); + /** + * @description Topテキストを取得 + * Get top text + * + * @return {string} + * @method + * @public + */ + getTopText (): string + { + return this.topText; + } + + /** + * @description スタートボタンがクリックされたときの処理 + * Handle when the start button is clicked + * + * @return {Promise} + * @method + * @public + */ + async onClickStartButton (): Promise + { + await this.navigateToViewUseCase.execute("home"); } } \ No newline at end of file diff --git a/template/tsconfig.json b/template/tsconfig.json index 2fd20d5..747296e 100644 --- a/template/tsconfig.json +++ b/template/tsconfig.json @@ -3,37 +3,36 @@ "target": "ES2020", "useDefineForClassFields": true, "module": "ESNext", - "lib": ["ES2020", "DOM", "DOM.Iterable"], + "lib": [ + "ES2020", + "DOM", + "DOM.Iterable" + ], "skipLibCheck": true, - "ignoreDeprecations": "6.0", - /* Bundler mode */ "moduleResolution": "Bundler", "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, - /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, - "types": [ "vitest/globals" ], - - "baseUrl": ".", "paths": { - "@/*": ["src/*"] - }, + "@/*": ["./src/*"] + } }, "include": [ "src/**/*.ts", "@types/**/*.ts" ], "exclude": [ - "node_modules/**" + "node_modules/**", + "src/**/*.test.ts" ] } \ No newline at end of file diff --git a/template/vite.config.ts b/template/vite.config.ts index 0e5fd7c..3e41b2b 100644 --- a/template/vite.config.ts +++ b/template/vite.config.ts @@ -37,14 +37,13 @@ export default defineConfig({ }, "resolve": { "alias": { - "@": path.resolve(process.cwd(), "./src") + "@": path.resolve(__dirname, "src") } }, "test": { "globals": true, "environment": "jsdom", "setupFiles": [ - "test.setup.ts", "@vitest/web-worker", "vitest-webgl-canvas-mock" ],