From 288f01deb2bc9cbf343d6abb5772f244deeb5fe6 Mon Sep 17 00:00:00 2001 From: ienaga Date: Wed, 3 Dec 2025 08:26:49 +0900 Subject: [PATCH 01/35] =?UTF-8?q?#56=20framework=20v4.0.0=E3=81=AB?= =?UTF-8?q?=E5=90=88=E3=82=8F=E3=81=9B=E3=81=A6=E3=82=B5=E3=83=B3=E3=83=97?= =?UTF-8?q?=E3=83=AB=E3=81=AE=E3=83=87=E3=82=A3=E3=83=AC=E3=82=AF=E3=83=88?= =?UTF-8?q?=E3=83=AA=E6=A7=8B=E6=88=90=E3=82=92=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/lint.yml | 19 +++- .github/workflows/publish.yml | 6 +- package.json | 2 +- template/README.md | 16 ++-- template/package.json | 54 ++++++----- template/src/interface/ITextFieldProps.ts | 11 +++ template/src/interface/ITextFormatAlign.ts | 1 + template/src/interface/ITextFormatObject.ts | 15 +++ .../event/home/HomeButtonPointerDownEvent.ts | 16 ---- .../event/home/HomeButtonPointerUpEvent.ts | 16 ---- .../event/top/TopButtonPointerUpEvent.ts | 14 --- .../ui/component/atom/ButtonComponent.ts | 18 ---- .../model/ui/component/atom/TextComponent.ts | 61 ------------ .../template/home/HomeButtonTemplate.ts | 30 ------ .../template/home/HomeTextTemplate.ts | 36 ------- .../template/top/TopButtonTemplate.ts | 40 -------- .../template/top/TopContentTemplate.ts | 24 ----- .../animation/top/TopBtnEntranceAnimation.ts | 56 +++++++++++ template/src/ui/component/atom/ButtonAtom.ts | 25 +++++ template/src/ui/component/atom/TextAtom.ts | 65 +++++++++++++ .../ui/component/molecule/TopBtnMolecule.ts | 49 ++++++++++ .../application => ui}/content/HomeContent.ts | 0 .../application => ui}/content/TopContent.ts | 0 template/src/view/home/HomeView.ts | 40 +++++++- template/src/view/home/HomeViewModel.ts | 32 +------ template/src/view/top/TopView.ts | 94 ++++++++++++++++++- template/src/view/top/TopViewModel.ts | 31 ++---- template/tsconfig.json | 16 ++-- template/vite.config.ts | 2 +- 29 files changed, 428 insertions(+), 361 deletions(-) create mode 100644 template/src/interface/ITextFieldProps.ts create mode 100644 template/src/interface/ITextFormatAlign.ts create mode 100644 template/src/interface/ITextFormatObject.ts delete mode 100644 template/src/model/domain/event/home/HomeButtonPointerDownEvent.ts delete mode 100644 template/src/model/domain/event/home/HomeButtonPointerUpEvent.ts delete mode 100644 template/src/model/domain/event/top/TopButtonPointerUpEvent.ts delete mode 100644 template/src/model/ui/component/atom/ButtonComponent.ts delete mode 100644 template/src/model/ui/component/atom/TextComponent.ts delete mode 100644 template/src/model/ui/component/template/home/HomeButtonTemplate.ts delete mode 100644 template/src/model/ui/component/template/home/HomeTextTemplate.ts delete mode 100644 template/src/model/ui/component/template/top/TopButtonTemplate.ts delete mode 100644 template/src/model/ui/component/template/top/TopContentTemplate.ts create mode 100644 template/src/ui/animation/top/TopBtnEntranceAnimation.ts create mode 100644 template/src/ui/component/atom/ButtonAtom.ts create mode 100644 template/src/ui/component/atom/TextAtom.ts create mode 100644 template/src/ui/component/molecule/TopBtnMolecule.ts rename template/src/{model/application => ui}/content/HomeContent.ts (100%) rename template/src/{model/application => ui}/content/TopContent.ts (100%) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 1261df1..d70a3b5 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -17,8 +17,12 @@ jobs: 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: 24 + registry-url: "https://registry.npmjs.org" + - run: npm install -g npm@latest - run: | npm install npx eslint ./src/**/*.ts @@ -30,9 +34,14 @@ jobs: 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: 24 + registry-url: "https://registry.npmjs.org" + - run: npm install -g npm@latest - run: | npm install npx eslint ./src/**/*.ts - working-directory: ./template \ No newline at end of file + working-directory: ./template + \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 4a2fb1d..e2e4ac1 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -12,10 +12,10 @@ jobs: 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: 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/README.md b/template/README.md index ed097ca..2d85a0f 100644 --- a/template/README.md +++ b/template/README.md @@ -35,14 +35,12 @@ Launches the test runner. ## Build -### `npm run build:web` -### `npm run build:steam:windows` -### `npm run build:steam:macos` -### `npm run build:steam:linux` -### `npm run build -- --platform *** --env***` +### `npm run build:web -- --env prd` +### `npm run build:steam:windows -- --env prd` +### `npm run build:steam:macos -- --env prd` +### `npm run build:steam:linux -- --env prd` +### `npm run build:ios -- --env prd` +### `npm run build:android -- --env prd` Multi-platform builder, writes to various platforms including macOS, Windows, iOS, Android, and Web (HTML). -Builds apps for the environment specified by env=***. - -### Flowchart -![Flowchart](https://raw.githubusercontent.com/Next2D/framework/main/Framework_Flowchart.svg) \ No newline at end of file +Builds apps for the environment specified by env=***. \ No newline at end of file diff --git a/template/package.json b/template/package.json index 01c67d3..273429e 100644 --- a/template/package.json +++ b/template/package.json @@ -4,40 +4,44 @@ "description": "Next2D Framework TypeScript template.", "type": "module", "scripts": { - "start": "vite --host", - "preview:ios": "npx @next2d/builder --platform ios --preview", - "preview:android": "npx @next2d/builder --platform android --preview", - "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", - "build": "npx @next2d/builder", - "test": "npx vitest", - "generate": "npx @next2d/view-generator" + "start": "vite --host", + "preview:ios": "npx @next2d/builder --platform ios --preview", + "preview:android": "npx @next2d/builder --platform android --preview", + "preview:macos": "npx @next2d/builder --platform macos --preview", + "preview:windows": "npx @next2d/builder --platform windows --preview", + "preview:linux": "npx @next2d/builder --platform linux --preview", + "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", + "@eslint/eslintrc": "^3.3.3", + "@eslint/js": "^9.39.1", + "@next2d/vite-plugin-next2d-auto-loader": "^3.1.10", + "@types/node": "^24.10.1", + "@typescript-eslint/eslint-plugin": "^8.48.0", + "@typescript-eslint/parser": "^8.48.0", + "@vitest/web-worker": "^4.0.14", + "eslint": "^9.39.1", "eslint-plugin-unused-imports": "^4.3.0", - "globals": "^16.4.0", - "jsdom": "^27.1.0", + "globals": "^16.5.0", + "jsdom": "^27.2.0", "typescript": "^5.9.3", - "vite": "^7.1.12", + "vite": "^7.2.6", "vite-tsconfig-paths": "^5.1.4", - "vitest": "^4.0.6", + "vitest": "^4.0.14", "vitest-webgl-canvas-mock": "^1.1.0" }, "peerDependencies": { diff --git a/template/src/interface/ITextFieldProps.ts b/template/src/interface/ITextFieldProps.ts new file mode 100644 index 0000000..401bfa7 --- /dev/null +++ b/template/src/interface/ITextFieldProps.ts @@ -0,0 +1,11 @@ +export interface ITextFieldProps { + selectable: boolean | null; + mouseEnabled: boolean | null; + wordWrap: boolean | null; + multiline: boolean | null; + maxChars: number | null; + background: boolean | null; + backgroundColor: number | null; + border: boolean | null; + borderColor: number | null; +} \ 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..df67bab --- /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/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/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/animation/top/TopBtnEntranceAnimation.ts b/template/src/ui/animation/top/TopBtnEntranceAnimation.ts new file mode 100644 index 0000000..003739f --- /dev/null +++ b/template/src/ui/animation/top/TopBtnEntranceAnimation.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 TopBtnEntranceAnimation +{ + private readonly _entranceJob: Job; + + /** + * @param {TopBtnMolecule} sprite + * @param {() => void} callback + * @constructor + * @public + */ + constructor( + sprite: TopBtnMolecule, + callback: () => void + ) { + + // アニメーションの初期値に設定 + sprite.alpha = 0; + + this._entranceJob = Tween.add(sprite, + { + "alpha": 0 + }, + { + "alpha": 1 + }, 0.5, 1, Easing.inQuad + ); + + // 終了アニメーションが完了したら、完了イベントを発行 + this._entranceJob.addEventListener(Event.COMPLETE, (): void => + { + callback(); + }); + } + + /** + * @description アニメーション開始 + * Start animation + * + * @method + * @public + */ + start(): void { + this._entranceJob.start(); + } +} \ No newline at end of file diff --git a/template/src/ui/component/atom/ButtonAtom.ts b/template/src/ui/component/atom/ButtonAtom.ts new file mode 100644 index 0000000..90c73dd --- /dev/null +++ b/template/src/ui/component/atom/ButtonAtom.ts @@ -0,0 +1,25 @@ +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; + } +} \ 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..6c19506 --- /dev/null +++ b/template/src/ui/component/atom/TextAtom.ts @@ -0,0 +1,65 @@ +import type { ITextFormatObject } from "@/interface/ITextFormatObject"; +import { TextField } from "@next2d/text"; + +/** + * @description テキストの基本コンポーネント + * Basic component of text + * + * @class + * @extends {TextField} + * @public + */ +export class TextAtom extends TextField { + + /** + * @param {string} [text=""] + * @param {ITextFieldProps | null} [props=null] + * @param {ITextFormatObject | null} [format_object=null] + * @constructor + * @public + */ + constructor( + text: string = "", + props: any | null = null, + format_object: ITextFormatObject | null = null + ) { + super(); + + if (props) { + const keys: string[] = Object.keys(props); + for (let idx = 0; idx < keys.length; idx++) { + + const name = keys[idx]; + + if (!(name in this)) { + continue; + } + + // @ts-ignore + this[name] = props[name]; + } + } + + if (format_object) { + const keys: string[] = Object.keys(format_object); + if (keys.length) { + const textFormat = this.defaultTextFormat; + for (let idx = 0; idx < keys.length; idx++) { + + const name = keys[idx]; + + if (!(name in textFormat)) { + continue; + } + + // @ts-ignore + textFormat[name] = format_object[name]; + } + + this.defaultTextFormat = textFormat; + } + } + + this.text = text; + } +} \ 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..6904967 --- /dev/null +++ b/template/src/ui/component/molecule/TopBtnMolecule.ts @@ -0,0 +1,49 @@ +import { TopBtnEntranceAnimation } from "@/ui/animation/top/TopBtnEntranceAnimation"; +import { ButtonAtom } from "../atom/ButtonAtom"; +import { TextAtom } from "../atom/TextAtom"; +import { app } from "@next2d/framework"; + +/** + * @description Top画面のボタン分子 + * Top Screen Button Molecule + * + * @class + * @extends {ButtonAtom} + * @public + */ +export class TopBtnMolecule extends ButtonAtom +{ + /** + * @constructor + * @public + */ + constructor () + { + super(); + + const response = app.getResponse(); + + const text = response.has("TopText") ? response.get("TopText").word : ""; + 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 + * + * @return {void} + * @method + * @public + */ + playEntrance (callback: () => void): void + { + new TopBtnEntranceAnimation(this, callback).start(); + } +} \ No newline at end of file diff --git a/template/src/model/application/content/HomeContent.ts b/template/src/ui/content/HomeContent.ts similarity index 100% rename from template/src/model/application/content/HomeContent.ts rename to template/src/ui/content/HomeContent.ts diff --git a/template/src/model/application/content/TopContent.ts b/template/src/ui/content/TopContent.ts similarity index 100% rename from template/src/model/application/content/TopContent.ts rename to template/src/ui/content/TopContent.ts diff --git a/template/src/view/home/HomeView.ts b/template/src/view/home/HomeView.ts index 3e21750..f897e31 100644 --- a/template/src/view/home/HomeView.ts +++ b/template/src/view/home/HomeView.ts @@ -1,3 +1,4 @@ +import type { HomeViewModel } from "./HomeViewModel"; import { View } from "@next2d/framework"; /** @@ -7,11 +8,46 @@ import { View } from "@next2d/framework"; export class HomeView extends View { /** + * @param {HomeViewModel} vm * @constructor * @public */ - constructor () - { + constructor ( + private readonly vm: HomeViewModel + ) { super(); } + + /** + * @return {Promise} + * @method + * @override + * @public + */ + async initialize (): Promise + { + return void 0; + } + + /** + * @return {Promise} + * @method + * @override + * @public + */ + async onEnter (): Promise + { + return void 0; + } + + /** + * @return {Promise} + * @method + * @override + * @public + */ + async onExit (): Promise + { + 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..180d7bf 100644 --- a/template/src/view/home/HomeViewModel.ts +++ b/template/src/view/home/HomeViewModel.ts @@ -1,6 +1,4 @@ -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 { ViewModel } from "@next2d/framework"; /** * @class @@ -9,35 +7,13 @@ import { execute as homeTextTemplate } from "@/model/ui/component/template/home/ export class HomeViewModel extends ViewModel { /** - * @param {View} view - * @return {Promise} - * @method - * @public - */ - async unbind (view: View): Promise - { - return super.unbind(view); - } - - /** - * @param {View} view * @return {Promise} * @method + * @override * @public */ - async bind (view: View): Promise + async initialize (): Promise { - /** - * アニメーションを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)); + return void 0; } } \ No newline at end of file diff --git a/template/src/view/top/TopView.ts b/template/src/view/top/TopView.ts index 5f8c028..acb3311 100644 --- a/template/src/view/top/TopView.ts +++ b/template/src/view/top/TopView.ts @@ -1,4 +1,9 @@ +import type { TopViewModel } from "./TopViewModel"; import { View } from "@next2d/framework"; +import { TopBtnMolecule } from "@/ui/component/molecule/TopBtnMolecule"; +import { config } from "@/config/Config"; +import { PointerEvent } from "@next2d/events"; +import { TopContent } from "@/ui/content/TopContent"; /** * @class @@ -7,11 +12,96 @@ import { View } from "@next2d/framework"; export class TopView extends View { /** + * @param {TopViewModel} vm * @constructor * @public */ - constructor () - { + constructor ( + private readonly vm: TopViewModel + ) { super(); } + + /** + * @return {Promise} + * @method + * @override + * @public + */ + async initialize (): Promise + { + /** + * ロゴアニメーションを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; + + this.addChild(topContent); + + /** + * Topボタンを生成して、座標をセット + * Create Top button and set coordinates + */ + const topBtn = new TopBtnMolecule(); + topBtn.name = "topBtn"; + topBtn.x = config.stage.width / 2; + topBtn.y = config.stage.height / 2 + topContent.height / 2 + topBtn.height; + + /** + * アニメーションが完了するまでボタンを無効化 + * Disable button until animation is complete + */ + topBtn.mouseChildren = false; + topBtn.mouseEnabled = false; + + /** + * ボタンのクリックイベントをViewModelに送信 + * Send button click event to ViewModel + */ + topBtn.addEventListener(PointerEvent.POINTER_UP, async (): Promise => + { + await this.vm.onClickStartButton(); + }); + + /** + * Topボタンを画面に追加 + * Add Top button to the screen + */ + this.addChild(topBtn); + } + + /** + * @return {Promise} + * @method + * @override + * @public + */ + async onEnter (): Promise + { + const topBtn = this.getChildByName("topBtn"); + if (!topBtn) { + return; + } + topBtn.playEntrance(() => + { + // アニメーション完了後にボタンを有効化 + // Enable button after animation is complete + topBtn.mouseChildren = true; + topBtn.mouseEnabled = true; + }); + } + + /** + * @return {Promise} + * @method + * @override + * @public + */ + async onExit (): Promise + { + 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..7da2cd1 100644 --- a/template/src/view/top/TopViewModel.ts +++ b/template/src/view/top/TopViewModel.ts @@ -1,6 +1,4 @@ -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"; /** * @class @@ -9,35 +7,26 @@ import { execute as topButtonTemplate } from "@/model/ui/component/template/top/ export class TopViewModel extends ViewModel { /** - * @param {View} view - * @return {Promise} + * @return {Promise} * @method + * @override * @public */ - async unbind (view: View): Promise + async initialize (): Promise { - return super.unbind(view); + return void 0; } /** - * @param {View} view + * @description スタートボタンがクリックされたときの処理 + * Handle when the start button is clicked + * * @return {Promise} * @method * @public */ - async bind (view: View): Promise + async onClickStartButton (): Promise { - /** - * ロゴアニメーションをAnimation ToolのJSONから生成 - * Logo animation generated from Animation Tool JSON - */ - const topContent = topContentTemplate(); - view.addChild(topContent); - - /** - * ボタンエリアを生成 - * Generate button area - */ - view.addChild(topButtonTemplate(topContent)); + await app.gotoView("home"); } } \ No newline at end of file diff --git a/template/tsconfig.json b/template/tsconfig.json index 2fd20d5..fc69ee5 100644 --- a/template/tsconfig.json +++ b/template/tsconfig.json @@ -3,31 +3,29 @@ "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", diff --git a/template/vite.config.ts b/template/vite.config.ts index 0e5fd7c..25a1156 100644 --- a/template/vite.config.ts +++ b/template/vite.config.ts @@ -37,7 +37,7 @@ export default defineConfig({ }, "resolve": { "alias": { - "@": path.resolve(process.cwd(), "./src") + "@": path.resolve(__dirname, "src") } }, "test": { From 934086075dd9e5af95cf55f431efd988805a9b5c Mon Sep 17 00:00:00 2001 From: ienaga Date: Wed, 3 Dec 2025 08:50:25 +0900 Subject: [PATCH 02/35] =?UTF-8?q?#56=20framework=20v4.0.0=E3=81=AB?= =?UTF-8?q?=E5=90=88=E3=82=8F=E3=81=9B=E3=81=A6=E3=82=B5=E3=83=B3=E3=83=97?= =?UTF-8?q?=E3=83=AB=E3=81=AE=E3=83=87=E3=82=A3=E3=83=AC=E3=82=AF=E3=83=88?= =?UTF-8?q?=E3=83=AA=E6=A7=8B=E6=88=90=E3=82=92=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/HomeContentPointerDownService.ts | 17 ++++++ .../service/HomeContentPointerUpService.ts | 17 ++++++ .../home/service/HomeTextChangeService.ts | 18 ++++++ .../ui/component/molecule/HomeBtnMolecule.ts | 28 +++++++++ template/src/view/home/HomeView.ts | 60 ++++++++++++++++++- template/src/view/home/HomeViewModel.ts | 46 ++++++++++++++ 6 files changed, 184 insertions(+), 2 deletions(-) create mode 100644 template/src/model/application/home/service/HomeContentPointerDownService.ts create mode 100644 template/src/model/application/home/service/HomeContentPointerUpService.ts create mode 100644 template/src/model/application/home/service/HomeTextChangeService.ts create mode 100644 template/src/ui/component/molecule/HomeBtnMolecule.ts diff --git a/template/src/model/application/home/service/HomeContentPointerDownService.ts b/template/src/model/application/home/service/HomeContentPointerDownService.ts new file mode 100644 index 0000000..81a2c31 --- /dev/null +++ b/template/src/model/application/home/service/HomeContentPointerDownService.ts @@ -0,0 +1,17 @@ +import type { HomeContent } from "@/ui/content/HomeContent"; +import { PointerEvent } from "@next2d/events"; + +/** + * @description Home画面のコンテンツのPointerDownイベントを処理 + * Handle PointerDown events for Home screen content + * + * @param {PointerEvent} event + * @return {void} + * @method + * @public + */ +export const execute = (event: PointerEvent): void => +{ + const homeContent = event.currentTarget as HomeContent; + homeContent.startDrag(); +}; \ No newline at end of file diff --git a/template/src/model/application/home/service/HomeContentPointerUpService.ts b/template/src/model/application/home/service/HomeContentPointerUpService.ts new file mode 100644 index 0000000..11abd77 --- /dev/null +++ b/template/src/model/application/home/service/HomeContentPointerUpService.ts @@ -0,0 +1,17 @@ +import type { HomeContent } from "@/ui/content/HomeContent"; +import { PointerEvent } from "@next2d/events"; + +/** + * @description Home画面のコンテンツのPointerUpイベントを処理 + * Handle PointerUp events for Home screen content + * + * @param {PointerEvent} event + * @return {void} + * @method + * @public + */ +export const execute = (event: PointerEvent): void => +{ + const homeContent = event.currentTarget as HomeContent; + homeContent.stopDrag(); +}; \ No newline at end of file diff --git a/template/src/model/application/home/service/HomeTextChangeService.ts b/template/src/model/application/home/service/HomeTextChangeService.ts new file mode 100644 index 0000000..03c6ca1 --- /dev/null +++ b/template/src/model/application/home/service/HomeTextChangeService.ts @@ -0,0 +1,18 @@ +import type { TextAtom } from "@/ui/component/atom/TextAtom"; +import type { Event } from "@next2d/events"; +import { config } from "@/config/Config"; + +/** + * @description Home画面のTextFieldのChangeイベントを処理 + * Handle Change events for TextField on Home screen + * + * @param {Event} event + * @return {void} + * @method + * @public + */ +export const execute = (event: Event): void => +{ + const textAtom = event.currentTarget as TextAtom; + textAtom.x = (config.stage.width - textAtom.width) / 2; +}; \ 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..0e879de --- /dev/null +++ b/template/src/ui/component/molecule/HomeBtnMolecule.ts @@ -0,0 +1,28 @@ +import { HomeContent } from "@/ui/content/HomeContent"; +import { ButtonAtom } from "../atom/ButtonAtom"; + +/** + * @description Home画面のボタン分子 + * Home Screen Button Molecule + * + * @class + * @extends {ButtonAtom} + * @public + */ +export class HomeBtnMolecule extends ButtonAtom +{ + /** + * @constructor + * @public + */ + constructor () + { + super(); + + const homeContent = new HomeContent(); + homeContent.scaleX = 2; + homeContent.scaleY = 2; + + this.addChild(homeContent); + } +} \ No newline at end of file diff --git a/template/src/view/home/HomeView.ts b/template/src/view/home/HomeView.ts index f897e31..b7c5f76 100644 --- a/template/src/view/home/HomeView.ts +++ b/template/src/view/home/HomeView.ts @@ -1,5 +1,9 @@ import type { HomeViewModel } from "./HomeViewModel"; -import { View } from "@next2d/framework"; +import { View, app } from "@next2d/framework"; +import { config } from "@/config/Config"; +import { HomeBtnMolecule } from "@/ui/component/molecule/HomeBtnMolecule"; +import { TextAtom } from "@/ui/component/atom/TextAtom"; +import { PointerEvent, Event } from "@next2d/events"; /** * @class @@ -26,7 +30,59 @@ export class HomeView extends View */ async initialize (): Promise { - return void 0; + /** + * ホームコンテンツの座標をセット + * 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, + this.vm.homeContentPointerDownEvent + ); + homeContent.addEventListener(PointerEvent.POINTER_UP, + this.vm.homeContentPointerUpEvent + ); + + /** + * ホームコンテンツを追加 + * Add home content + */ + this.addChild(homeContent); + + // Hello, World. + const response = app.getResponse(); + const text = response.has("HomeText") ? response.get("HomeText").word : ""; + const textField = new TextAtom(text, { + "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, + this.vm.homeTextChangeEvent + ); + + /** + * ホームテキストを追加 + * Add home text + */ + this.addChild(textField); } /** diff --git a/template/src/view/home/HomeViewModel.ts b/template/src/view/home/HomeViewModel.ts index 180d7bf..4d7df15 100644 --- a/template/src/view/home/HomeViewModel.ts +++ b/template/src/view/home/HomeViewModel.ts @@ -1,4 +1,8 @@ import { ViewModel } from "@next2d/framework"; +import type { PointerEvent, Event } from "@next2d/events"; +import { execute as homeContentPointerDownService } from "@/model/application/home/service/HomeContentPointerDownService"; +import { execute as homeContentPointerUpService } from "@/model/application/home/service/HomeContentPointerUpService"; +import { execute as homeTextChangeService } from "@/model/application/home/service/HomeTextChangeService"; /** * @class @@ -16,4 +20,46 @@ export class HomeViewModel extends ViewModel { return void 0; } + + /** + * @description ホームコンテンツのポインターダウン時の処理 + * Handle when home content is pointer down + * + * @param {PointerEvent} event + * @return {void} + * @method + * @public + */ + homeContentPointerDownEvent (event: PointerEvent): void + { + homeContentPointerDownService(event); + } + + /** + * @description ホームコンテンツのポインターアップ時の処理 + * Handle when home content is pointer up + * + * @param {PointerEvent} event + * @return {void} + * @method + * @public + */ + homeContentPointerUpEvent (event: PointerEvent): void + { + homeContentPointerUpService(event); + } + + /** + * @description ホームテキストの変更時の処理 + * Handle when home text is changed + * + * @param {Event} event + * @return {void} + * @method + * @public + */ + homeTextChangeEvent (event: Event): void + { + homeTextChangeService(event); + } } \ No newline at end of file From 282c096897a5973c8320a65a425b1b440f10eba4 Mon Sep 17 00:00:00 2001 From: ienaga Date: Wed, 3 Dec 2025 13:44:38 +0900 Subject: [PATCH 03/35] =?UTF-8?q?#56=20=E6=A7=8B=E6=88=90=E5=A4=89?= =?UTF-8?q?=E6=9B=B4=E3=81=AB=E5=90=88=E3=82=8F=E3=81=9B=E3=81=9FREADME?= =?UTF-8?q?=E3=82=92=E4=BD=9C=E6=88=90(WIP)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/ARCHITECTURE.md | 229 +++++++ template/src/interface/IDraggable.ts | 25 + template/src/interface/IHomeTextResponse.ts | 15 + template/src/interface/ITextField.ts | 23 + template/src/interface/README.md | 270 +++++++++ template/src/model/README.md | 32 +- template/src/model/application/README.md | 407 +++++++++++++ .../service/HomeContentPointerDownService.ts | 17 - .../service/HomeContentPointerUpService.ts | 17 - .../home/service/HomeTextChangeService.ts | 18 - .../home/usecase/CenterTextFieldUseCase.ts | 25 + .../home/usecase/StartDragUseCase.ts | 24 + .../home/usecase/StopDragUseCase.ts | 24 + .../top/usecase/NavigateToViewUseCase.ts | 24 + template/src/model/domain/README.md | 559 ++++++++++++++++++ .../service/BackgroundDrawService.ts | 2 +- template/src/model/infrastructure/README.md | 438 ++++++++++++++ .../repository/HomeTextRepository.ts | 23 +- template/src/ui/README.md | 324 ++++++++++ template/src/ui/component/atom/TextAtom.ts | 4 +- .../ui/component/molecule/HomeBtnMolecule.ts | 40 +- template/src/ui/content/HomeContent.ts | 4 +- template/src/view/README.md | 428 ++++++++++++-- template/src/view/home/HomeViewModel.ts | 33 +- template/src/view/top/TopViewModel.ts | 17 +- 25 files changed, 2891 insertions(+), 131 deletions(-) create mode 100644 template/ARCHITECTURE.md create mode 100644 template/src/interface/IDraggable.ts create mode 100644 template/src/interface/IHomeTextResponse.ts create mode 100644 template/src/interface/ITextField.ts create mode 100644 template/src/interface/README.md create mode 100644 template/src/model/application/README.md delete mode 100644 template/src/model/application/home/service/HomeContentPointerDownService.ts delete mode 100644 template/src/model/application/home/service/HomeContentPointerUpService.ts delete mode 100644 template/src/model/application/home/service/HomeTextChangeService.ts create mode 100644 template/src/model/application/home/usecase/CenterTextFieldUseCase.ts create mode 100644 template/src/model/application/home/usecase/StartDragUseCase.ts create mode 100644 template/src/model/application/home/usecase/StopDragUseCase.ts create mode 100644 template/src/model/application/top/usecase/NavigateToViewUseCase.ts create mode 100644 template/src/model/domain/README.md create mode 100644 template/src/model/infrastructure/README.md create mode 100644 template/src/ui/README.md diff --git a/template/ARCHITECTURE.md b/template/ARCHITECTURE.md new file mode 100644 index 0000000..70e846c --- /dev/null +++ b/template/ARCHITECTURE.md @@ -0,0 +1,229 @@ +# Clean Architecture & MVVM Implementation + +このプロジェクトは、クリーンアーキテクチャとMVVMパターンを組み合わせて実装されています。 + +This project implements a combination of Clean Architecture and MVVM pattern. + +## アーキテクチャの概要 / Architecture Overview + +``` +┌─────────────────────────────────────────────────────────┐ +│ View Layer │ +│ (view/*, ui/*) │ +│ - View: 画面の構造を定義 │ +│ - ViewModel: Viewとビジネスロジックの橋渡し │ +│ - UI Components: 再利用可能なUIパーツ │ +└─────────────────────────────────────────────────────────┘ + ↓ ↑ + (Interface 経由) + ↓ ↑ +┌─────────────────────────────────────────────────────────┐ +│ Application Layer │ +│ (model/application/*/usecase/*) │ +│ - UseCase: ビジネスロジックの実装 │ +│ - アプリケーション固有の処理を担当 │ +└─────────────────────────────────────────────────────────┘ + ↓ ↑ +┌─────────────────────────────────────────────────────────┐ +│ Domain Layer │ +│ (model/domain/*) │ +│ - コアビジネスロジック │ +│ - フレームワークや外部ライブラリに依存しない │ +└─────────────────────────────────────────────────────────┘ + ↓ ↑ +┌─────────────────────────────────────────────────────────┐ +│ Infrastructure Layer │ +│ (model/infrastructure/repository/*) │ +│ - 外部API、データベースへのアクセス │ +│ - Repository: データソースの抽象化 │ +└─────────────────────────────────────────────────────────┘ +``` + +## 依存関係の方向 / Dependency Direction + +クリーンアーキテクチャの原則に従い、依存関係は常に内側(Domain層)に向かいます。 + +Following Clean Architecture principles, dependencies always point inward (toward the Domain layer). + +``` +View ──→ Interface ←── Application ──→ Domain ←── Infrastructure +``` + +- **View層**: インターフェースを通じてApplication層を使用 +- **Application層**: インターフェースを通じてDomain層とInfrastructure層を使用 +- **Domain層**: 何にも依存しない(純粋なビジネスロジック) +- **Infrastructure層**: Domain層のインターフェースを実装 + +## ディレクトリ構造 / Directory Structure + +``` +src/ +├── interface/ # インターフェース定義 +│ ├── IDraggable.ts # ドラッグ可能なオブジェクト +│ ├── ITextField.ts # テキストフィールド +│ └── IHomeTextResponse.ts # API レスポンス型 +│ +├── 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 + +### 例: ドラッグ操作の場合 + +1. **ユーザーアクション**: ユーザーがコンテンツをポインターダウン +2. **View**: イベントをキャッチし、ViewModelのメソッドを呼び出し +3. **ViewModel**: UseCaseを実行 +4. **UseCase**: インターフェースを通じてビジネスロジックを実行 +5. **UI Component**: 実際のドラッグ処理を実行 + +```typescript +// 1. View: イベントハンドリング +homeContent.addEventListener(PointerEvent.POINTER_DOWN, + this.vm.homeContentPointerDownEvent +); + +// 2. ViewModel: UseCaseの実行 +homeContentPointerDownEvent(event: PointerEvent): void { + const target = event.currentTarget 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/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/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/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/README.md b/template/src/interface/README.md new file mode 100644 index 0000000..2417a5e --- /dev/null +++ b/template/src/interface/README.md @@ -0,0 +1,270 @@ +# 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` 配列の型定義 + +## ベストプラクティス / 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..ece6654 100644 --- a/template/src/model/README.md +++ b/template/src/model/README.md @@ -36,13 +36,13 @@ We will describe the role of each directory. ### Application ```sh -application -└── content + application + └── content ``` -`content` ディレクトリにはAnimation Toolで作成された `DisplayObject` を動的に生成するためのクラスが格納されてます。動的に生成された `DisplayObject` は起動時に `initialize` 関数が実行されます。 `service`、 `usecase` ディレクトリを作成して、`domain` へのアクセスを行う責務を担う事も良いかもしれません。 +`content` ディレクトリにはAnimation Toolで作成された `DisplayObject` を動的に生成するためのクラスが格納されてます。`usecase` ディレクトリを作成して、ビジネスロジックを実装し、`domain` へのアクセスを行う責務を担います。 -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`. +The `content` directory contains classes for dynamically generating `DisplayObject`s created by the Animation Tool. The `usecase` directory is created to implement business logic and handle the responsibility of accessing the `domain`. #### Example of cooperation with Animation Tool @@ -83,11 +83,9 @@ domain └── home ``` -アプリケーションの固有ロジックを格納するディレクトリで、プロジェクトの核心になる層です。このテンプレートでは `callback` で、背景に全画面のグラデーション描画を行なっています。 `event` ディレクトリは各ページのイベント処理関数が管理されています。 `event` ディレクトリのクラスがユーザーからの `InputUseCase` の責務を担っています。 +アプリケーションの固有ロジックを格納するディレクトリで、プロジェクトの核心になる層です。このテンプレートでは `callback` で、背景に全画面のグラデーション描画を行なっています。ドメインロジックは外部の詳細実装に依存せず、インターフェースを通じて抽象化された依存関係を持つべきです。 -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. +This directory stores the application-specific logic and is the core layer of the project. The `callback` generates the background for all screens. Domain logic should not depend on external implementation details and should have dependencies abstracted through interfaces. ### Infrastructure @@ -96,21 +94,25 @@ infrastructure └── repository ``` -外部へのアクセスロジックを格納するディレクトリです。データベースからの情報であれば `entity` ディレクトリを作成して可変可能オブジェクトとして運用し、可変想定がないオブジェクトなどはデータ転送オブジェクト(DTO)として `dto` ディレクトリにそれぞれ責務を分散させるのが良いかもしれません。 +外部へのアクセスロジックを格納するディレクトリです。データベースからの情報であれば `entity` ディレクトリを作成して可変可能オブジェクトとして運用し、可変想定がないオブジェクトなどはデータ転送オブジェクト(DTO)として `dto` ディレクトリにそれぞれ責務を分散させるのが良いかもしれません。Repositoryは具体的な実装であり、ドメイン層からはインターフェースを通じてアクセスされるべきです。 -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). +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). Repositories are concrete implementations and should be accessed from the domain layer through interfaces. ### UI(User Interface) +このテンプレートでは、UIコンポーネントは `src/ui` ディレクトリに配置されており、`model/ui` ではありません。 + +In this template, UI components are located in the `src/ui` directory, not in `model/ui`. + +## UI Components + ```sh ui └── component ├── atom - └── template - ├── top - └── home + └── molecule ``` -アトミックデザインを意識したディレクトリ構成になってます。`atom` ディレクトリに最小の表示要素を作成して、`template` ディレクトリで各atomの要素を呼び出しページのレイアウトを作成してます。今回は `template` 内のクラスが `UseCase` の責務も担っています。`application` ディレクトリへのアクセスは `template` 内のクラスに制限する想定です。 +アトミックデザインを意識したディレクトリ構成になってます。`atom` ディレクトリに最小の表示要素を作成して、`molecule` ディレクトリで各atomの要素を組み合わせたコンポーネントを作成します。UIコンポーネントはインターフェースを実装することで、ビジネスロジック層から抽象化された形で扱われます。 -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 +The directory structure is designed with atomic design in mind. Minimal display elements are created in the `atom` directory, and in the `molecule` directory, components are created by combining atom elements. UI components implement interfaces to be handled in an abstracted manner from the business logic layer. \ 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..b6f4804 --- /dev/null +++ b/template/src/model/application/README.md @@ -0,0 +1,407 @@ +# 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 { app } from "@next2d/framework"; + +/** + * @description 画面遷移のユースケース + * Use case for navigating to a view + * + * @class + */ +export class NavigateToViewUseCase +{ + /** + * @description 指定された画面に遷移する + * Navigate to the specified view + * + * @param {string} viewName + * @return {Promise} + * @method + * @public + */ + async execute (viewName: string): Promise + { + // ビジネスルール: 遷移前の検証など + // 例: 未保存データのチェック、権限確認など + + await app.gotoView(viewName); + } +} +``` + +### Example: CenterTextFieldUseCase + +```typescript +import type { ITextField } from "@/interface/ITextField"; +import { config } from "@/config/Config"; + +/** + * @description テキストフィールド中央揃えのユースケース + * Use case for centering text field + * + * @class + */ +export class CenterTextFieldUseCase +{ + /** + * @description テキストフィールドを画面中央に配置する + * Center the text field on the screen + * + * @param {ITextField} textField + * @return {void} + * @method + * @public + */ + execute (textField: ITextField): void + { + // ビジネスロジック: 中央配置の計算 + textField.x = (config.stage.width - textField.width) / 2; + } +} +``` + +## 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/service/HomeContentPointerDownService.ts b/template/src/model/application/home/service/HomeContentPointerDownService.ts deleted file mode 100644 index 81a2c31..0000000 --- a/template/src/model/application/home/service/HomeContentPointerDownService.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { HomeContent } from "@/ui/content/HomeContent"; -import { PointerEvent } from "@next2d/events"; - -/** - * @description Home画面のコンテンツのPointerDownイベントを処理 - * Handle PointerDown events for Home screen content - * - * @param {PointerEvent} event - * @return {void} - * @method - * @public - */ -export const execute = (event: PointerEvent): void => -{ - const homeContent = event.currentTarget as HomeContent; - homeContent.startDrag(); -}; \ No newline at end of file diff --git a/template/src/model/application/home/service/HomeContentPointerUpService.ts b/template/src/model/application/home/service/HomeContentPointerUpService.ts deleted file mode 100644 index 11abd77..0000000 --- a/template/src/model/application/home/service/HomeContentPointerUpService.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { HomeContent } from "@/ui/content/HomeContent"; -import { PointerEvent } from "@next2d/events"; - -/** - * @description Home画面のコンテンツのPointerUpイベントを処理 - * Handle PointerUp events for Home screen content - * - * @param {PointerEvent} event - * @return {void} - * @method - * @public - */ -export const execute = (event: PointerEvent): void => -{ - const homeContent = event.currentTarget as HomeContent; - homeContent.stopDrag(); -}; \ No newline at end of file diff --git a/template/src/model/application/home/service/HomeTextChangeService.ts b/template/src/model/application/home/service/HomeTextChangeService.ts deleted file mode 100644 index 03c6ca1..0000000 --- a/template/src/model/application/home/service/HomeTextChangeService.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { TextAtom } from "@/ui/component/atom/TextAtom"; -import type { Event } from "@next2d/events"; -import { config } from "@/config/Config"; - -/** - * @description Home画面のTextFieldのChangeイベントを処理 - * Handle Change events for TextField on Home screen - * - * @param {Event} event - * @return {void} - * @method - * @public - */ -export const execute = (event: Event): void => -{ - const textAtom = event.currentTarget as TextAtom; - textAtom.x = (config.stage.width - textAtom.width) / 2; -}; \ No newline at end of file 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..9693625 --- /dev/null +++ b/template/src/model/application/home/usecase/CenterTextFieldUseCase.ts @@ -0,0 +1,25 @@ +import type { ITextField } from "@/interface/ITextField"; +import { config } from "@/config/Config"; + +/** + * @description テキストフィールド中央揃えのユースケース + * Use case for centering text field + * + * @class + */ +export class CenterTextFieldUseCase +{ + /** + * @description テキストフィールドを画面中央に配置する + * Center the text field on the screen + * + * @param {ITextField} textField + * @return {void} + * @method + * @public + */ + execute (textField: ITextField): void + { + textField.x = (config.stage.width - textField.width) / 2; + } +} 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..300addb --- /dev/null +++ b/template/src/model/application/home/usecase/StartDragUseCase.ts @@ -0,0 +1,24 @@ +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.ts b/template/src/model/application/home/usecase/StopDragUseCase.ts new file mode 100644 index 0000000..b5fcb32 --- /dev/null +++ b/template/src/model/application/home/usecase/StopDragUseCase.ts @@ -0,0 +1,24 @@ +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.ts b/template/src/model/application/top/usecase/NavigateToViewUseCase.ts new file mode 100644 index 0000000..6ed0f92 --- /dev/null +++ b/template/src/model/application/top/usecase/NavigateToViewUseCase.ts @@ -0,0 +1,24 @@ +import { app } from "@next2d/framework"; + +/** + * @description 画面遷移のユースケース + * Use case for navigating to a view + * + * @class + */ +export class NavigateToViewUseCase +{ + /** + * @description 指定された画面に遷移する + * Navigate to the specified view + * + * @param {string} viewName + * @return {Promise} + * @method + * @public + */ + async execute (viewName: string): 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..a81abbd --- /dev/null +++ b/template/src/model/domain/README.md @@ -0,0 +1,559 @@ +# 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: + +- ✅ **純粋なビジネスロジック** - フレームワークに依存しない +- ✅ **再利用可能なロジック** - アプリケーション全体で利用される +- ✅ **ドメイン知識の表現** - ビジネスルールを明確に表現 +- ✅ **安定性** - 外部の変更に影響されにくい +- ❌ **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固有の機能を使う場合は明確に +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/service/BackgroundDrawService.ts b/template/src/model/domain/callback/Background/service/BackgroundDrawService.ts index 2656c00..aadb151 100644 --- a/template/src/model/domain/callback/Background/service/BackgroundDrawService.ts +++ b/template/src/model/domain/callback/Background/service/BackgroundDrawService.ts @@ -17,7 +17,7 @@ export const execute = (background: Background): void => 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/infrastructure/README.md b/template/src/model/infrastructure/README.md new file mode 100644 index 0000000..9cd1ffa --- /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/ # データベースエンティティ +│ └── 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.ts b/template/src/model/infrastructure/repository/HomeTextRepository.ts index 5f75b26..fbcd05c 100644 --- a/template/src/model/infrastructure/repository/HomeTextRepository.ts +++ b/template/src/model/infrastructure/repository/HomeTextRepository.ts @@ -1,3 +1,4 @@ +import type { IHomeTextResponse } from "@/interface/IHomeTextResponse"; import { config } from "@/config/Config"; /** @@ -6,12 +7,26 @@ import { config } from "@/config/Config"; 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/ui/README.md b/template/src/ui/README.md new file mode 100644 index 0000000..932f8fe --- /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/ # Atomを組み合わせたコンポーネント +└── 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); + } + + startDrag(): void { ... } + stopDrag(): void { ... } +} +``` + +**特徴 / Features:** +- `ButtonAtom` を継承 +- `IDraggable` インターフェースを実装 +- ドラッグ&ドロップ機能を提供 + +#### component/molecule/TopBtnMolecule.ts +Top画面のボタンコンポーネントです。 + +Button component for the Top screen. + +```typescript +export class TopBtnMolecule extends ButtonAtom { + constructor() { + super(); + // レスポンスデータからテキストを取得 + const text = app.getResponse().get("TopText").word; + const textField = new TextAtom(text, { autoSize: "center" }); + this.addChild(textField); + } + + playEntrance(callback: () => void): void { + // アニメーション再生 + } +} +``` + +**特徴 / Features:** +- テキストとアニメーションを含む +- 入場アニメーション機能 + +### 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 +// ✅ 良い例: インターフェースを実装 +export class HomeBtnMolecule extends ButtonAtom implements IDraggable { + startDrag(): void { ... } + stopDrag(): void { ... } +} +``` + +### 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/component/atom/TextAtom.ts b/template/src/ui/component/atom/TextAtom.ts index 6c19506..5cba877 100644 --- a/template/src/ui/component/atom/TextAtom.ts +++ b/template/src/ui/component/atom/TextAtom.ts @@ -1,3 +1,4 @@ +import type { ITextField } from "@/interface/ITextField"; import type { ITextFormatObject } from "@/interface/ITextFormatObject"; import { TextField } from "@next2d/text"; @@ -7,9 +8,10 @@ import { TextField } from "@next2d/text"; * * @class * @extends {TextField} + * @implements {ITextField} * @public */ -export class TextAtom extends TextField { +export class TextAtom extends TextField implements ITextField { /** * @param {string} [text=""] diff --git a/template/src/ui/component/molecule/HomeBtnMolecule.ts b/template/src/ui/component/molecule/HomeBtnMolecule.ts index 0e879de..9b7757e 100644 --- a/template/src/ui/component/molecule/HomeBtnMolecule.ts +++ b/template/src/ui/component/molecule/HomeBtnMolecule.ts @@ -1,3 +1,4 @@ +import type { IDraggable } from "@/interface/IDraggable"; import { HomeContent } from "@/ui/content/HomeContent"; import { ButtonAtom } from "../atom/ButtonAtom"; @@ -7,10 +8,13 @@ import { ButtonAtom } from "../atom/ButtonAtom"; * * @class * @extends {ButtonAtom} + * @implements {IDraggable} * @public */ -export class HomeBtnMolecule extends ButtonAtom +export class HomeBtnMolecule extends ButtonAtom implements IDraggable { + private readonly homeContent: HomeContent; + /** * @constructor * @public @@ -19,10 +23,36 @@ export class HomeBtnMolecule extends ButtonAtom { super(); - const homeContent = new HomeContent(); - homeContent.scaleX = 2; - homeContent.scaleY = 2; + this.homeContent = new HomeContent(); + this.homeContent.scaleX = 2; + this.homeContent.scaleY = 2; + + this.addChild(this.homeContent); + } - this.addChild(homeContent); + /** + * @description ドラッグを開始する + * Start dragging + * + * @return {void} + * @method + * @public + */ + startDrag (): void + { + this.homeContent.startDrag(); + } + + /** + * @description ドラッグを停止する + * Stop dragging + * + * @return {void} + * @method + * @public + */ + stopDrag (): void + { + this.homeContent.stopDrag(); } } \ No newline at end of file diff --git a/template/src/ui/content/HomeContent.ts b/template/src/ui/content/HomeContent.ts index 50ee595..0069be3 100644 --- a/template/src/ui/content/HomeContent.ts +++ b/template/src/ui/content/HomeContent.ts @@ -1,11 +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} diff --git a/template/src/view/README.md b/template/src/view/README.md index 1260c46..b200eba 100644 --- a/template/src/view/README.md +++ b/template/src/view/README.md @@ -1,8 +1,37 @@ # 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. + +``` +┌─────────────────────────────────────────┐ +│ View Layer │ +│ - 画面の構造と表示を担当 │ +│ - ビジネスロジックは持たない │ +└─────────────────────────────────────────┘ + ↓ ↑ +┌─────────────────────────────────────────┐ +│ ViewModel Layer │ +│ - ViewとModelの橋渡し │ +│ - UseCaseを保持 │ +│ - イベントハンドリング │ +└─────────────────────────────────────────┘ + ↓ ↑ + (Interface 経由) + ↓ ↑ +┌─────────────────────────────────────────┐ +│ Model Layer │ +│ - ビジネスロジック (UseCase) │ +│ - データアクセス (Repository) │ +└─────────────────────────────────────────┘ +``` ## Example of directory structure @@ -11,100 +40,411 @@ project └── src └── view ├── top - │ ├── TopView.js - │ └── TopViewModel.js + │ ├── TopView.ts + │ └── TopViewModel.ts └── home - ├── HomeView.js - └── HomeViewModel.js + ├── 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に委譲 + ### 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 { /** + * @param {HomeViewModel} vm * @constructor * @public */ - constructor () - { + constructor ( + private readonly vm: HomeViewModel + ) { super(); } + + /** + * @description 画面の初期化 + * Initialize the screen + * + * @return {Promise} + * @method + * @override + * @public + */ + async initialize (): Promise + { + // UIコンポーネントの作成と配置 + const homeContent = new HomeBtnMolecule(); + homeContent.x = 120; + homeContent.y = 120; + + // イベントをViewModelに委譲 + homeContent.addEventListener( + PointerEvent.POINTER_DOWN, + this.vm.homeContentPointerDownEvent + ); + + this.addChild(homeContent); + } + + /** + * @description 画面表示時の処理 + * Called when screen is shown + * + * @return {Promise} + * @method + * @override + * @public + */ + async onEnter (): Promise + { + // アニメーション開始などの処理 + } + + /** + * @description 画面非表示時の処理 + * Called when screen is hidden + * + * @return {Promise} + * @method + * @override + * @public + */ + async onExit (): Promise + { + // クリーンアップ処理 + } } ``` +### 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に委譲 ### 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"; /** * @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; + + /** + * @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 * @return {Promise} * @method + * @override * @public */ - async bind (view) + async initialize (): Promise { - /** - * ロゴアニメーションをAnimation ToolのJSONから生成 - * Logo animation generated from Animation Tool's JSON - */ - const topContent = new TopContentTemplate().factory(); - view.addChild(topContent); + // 初期化処理(必要に応じて) + return void 0; + } - /** - * ボタンエリアを生成 - * Generate button area - */ - const button = new TopButtonTemplate().factory(topContent); - view.addChild(button); + /** + * @description ドラッグ開始イベントのハンドラ + * Handler for drag start event + * + * @param {PointerEvent} event + * @return {void} + * @method + * @public + */ + homeContentPointerDownEvent (event: PointerEvent): void + { + // インターフェースを通じてターゲットを取得 + const target = event.currentTarget as unknown as IDraggable; + + // UseCaseを実行 + this.startDragUseCase.execute(target); } /** - * @param {View} view - * @return {Promise} + * @description ドラッグ停止イベントのハンドラ + * Handler for drag stop event + * + * @param {PointerEvent} event + * @return {void} * @method * @public */ - unbind (view) + homeContentPointerUpEvent (event: PointerEvent): void { - /** - * unbind関数を利用しなければ削除 - * Delete if unbind function is not used - */ - return super.unbind(view); + const target = event.currentTarget as unknown as IDraggable; + this.stopDragUseCase.execute(target); + } + + /** + * @description テキスト変更イベントのハンドラ + * Handler for text change event + * + * @param {Event} event + * @return {void} + * @method + * @public + */ + homeTextChangeEvent (event: Event): void + { + const textField = event.currentTarget as unknown as ITextField; + this.centerTextFieldUseCase.execute(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/HomeViewModel.ts b/template/src/view/home/HomeViewModel.ts index 4d7df15..14c9016 100644 --- a/template/src/view/home/HomeViewModel.ts +++ b/template/src/view/home/HomeViewModel.ts @@ -1,8 +1,10 @@ +import type { IDraggable } from "@/interface/IDraggable"; +import type { ITextField } from "@/interface/ITextField"; import { ViewModel } from "@next2d/framework"; import type { PointerEvent, Event } from "@next2d/events"; -import { execute as homeContentPointerDownService } from "@/model/application/home/service/HomeContentPointerDownService"; -import { execute as homeContentPointerUpService } from "@/model/application/home/service/HomeContentPointerUpService"; -import { execute as homeTextChangeService } from "@/model/application/home/service/HomeTextChangeService"; +import { StartDragUseCase } from "@/model/application/home/usecase/StartDragUseCase"; +import { StopDragUseCase } from "@/model/application/home/usecase/StopDragUseCase"; +import { CenterTextFieldUseCase } from "@/model/application/home/usecase/CenterTextFieldUseCase"; /** * @class @@ -10,6 +12,22 @@ import { execute as homeTextChangeService } from "@/model/application/home/servi */ export class HomeViewModel extends ViewModel { + private readonly startDragUseCase: StartDragUseCase; + private readonly stopDragUseCase: StopDragUseCase; + private readonly centerTextFieldUseCase: CenterTextFieldUseCase; + + /** + * @constructor + * @public + */ + constructor () + { + super(); + this.startDragUseCase = new StartDragUseCase(); + this.stopDragUseCase = new StopDragUseCase(); + this.centerTextFieldUseCase = new CenterTextFieldUseCase(); + } + /** * @return {Promise} * @method @@ -32,7 +50,8 @@ export class HomeViewModel extends ViewModel */ homeContentPointerDownEvent (event: PointerEvent): void { - homeContentPointerDownService(event); + const target = event.currentTarget as unknown as IDraggable; + this.startDragUseCase.execute(target); } /** @@ -46,7 +65,8 @@ export class HomeViewModel extends ViewModel */ homeContentPointerUpEvent (event: PointerEvent): void { - homeContentPointerUpService(event); + const target = event.currentTarget as unknown as IDraggable; + this.stopDragUseCase.execute(target); } /** @@ -60,6 +80,7 @@ export class HomeViewModel extends ViewModel */ homeTextChangeEvent (event: Event): void { - homeTextChangeService(event); + const textField = event.currentTarget as unknown as ITextField; + this.centerTextFieldUseCase.execute(textField); } } \ No newline at end of file diff --git a/template/src/view/top/TopViewModel.ts b/template/src/view/top/TopViewModel.ts index 7da2cd1..efb8db6 100644 --- a/template/src/view/top/TopViewModel.ts +++ b/template/src/view/top/TopViewModel.ts @@ -1,4 +1,5 @@ -import { ViewModel, app } from "@next2d/framework"; +import { ViewModel } from "@next2d/framework"; +import { NavigateToViewUseCase } from "@/model/application/top/usecase/NavigateToViewUseCase"; /** * @class @@ -6,6 +7,18 @@ import { ViewModel, app } from "@next2d/framework"; */ export class TopViewModel extends ViewModel { + private readonly navigateToViewUseCase: NavigateToViewUseCase; + + /** + * @constructor + * @public + */ + constructor () + { + super(); + this.navigateToViewUseCase = new NavigateToViewUseCase(); + } + /** * @return {Promise} * @method @@ -27,6 +40,6 @@ export class TopViewModel extends ViewModel */ async onClickStartButton (): Promise { - await app.gotoView("home"); + await this.navigateToViewUseCase.execute("home"); } } \ No newline at end of file From efb07a924eabe394b204eb2163c10212feafdfd1 Mon Sep 17 00:00:00 2001 From: ienaga Date: Wed, 3 Dec 2025 13:51:14 +0900 Subject: [PATCH 04/35] =?UTF-8?q?#56=20md=E3=83=95=E3=82=A1=E3=82=A4?= =?UTF-8?q?=E3=83=AB=E3=82=92=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/ARCHITECTURE.md | 340 ++++++++++++++++++++++++++++++++------- 1 file changed, 281 insertions(+), 59 deletions(-) diff --git a/template/ARCHITECTURE.md b/template/ARCHITECTURE.md index 70e846c..b553955 100644 --- a/template/ARCHITECTURE.md +++ b/template/ARCHITECTURE.md @@ -6,47 +6,102 @@ This project implements a combination of Clean Architecture and MVVM pattern. ## アーキテクチャの概要 / Architecture Overview +```mermaid +graph TB + subgraph ViewLayer["🎨 View Layer
(view/*, ui/*)"] + View["View
画面の構造を定義"] + ViewModel["ViewModel
Viewとビジネスロジックの橋渡し"] + UI["UI Components
再利用可能なUIパーツ"] + end + + subgraph InterfaceLayer["📋 Interface Layer
(interface/*)"] + IDraggable["IDraggable"] + ITextField["ITextField"] + IResponse["IHomeTextResponse"] + end + + subgraph ApplicationLayer["⚙️ Application Layer
(model/application/*/usecase/*)"] + UseCase["UseCase
ビジネスロジックの実装"] + AppLogic["アプリケーション固有の処理"] + end + + subgraph DomainLayer["💎 Domain Layer
(model/domain/*)"] + DomainLogic["コアビジネスロジック"] + DomainService["ドメインサービス"] + end + + subgraph InfraLayer["🔧 Infrastructure Layer
(model/infrastructure/repository/*)"] + Repository["Repository
データソースの抽象化"] + ExternalAPI["外部API・DBアクセス"] + end + + %% 依存関係 + View -.->|uses| ViewModel + ViewModel -.->|depends on| InterfaceLayer + ViewModel -.->|calls| UseCase + UseCase -.->|implements| InterfaceLayer + UseCase -.->|uses| DomainLogic + UseCase -.->|calls| Repository + Repository -.->|accesses| ExternalAPI + DomainService -.->|uses| DomainLogic + UI -.->|implements| InterfaceLayer + + %% スタイル + 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 View,ViewModel,UI viewStyle + class IDraggable,ITextField,IResponse interfaceStyle + class UseCase,AppLogic appStyle + class DomainLogic,DomainService domainStyle + class Repository,ExternalAPI infraStyle ``` -┌─────────────────────────────────────────────────────────┐ -│ View Layer │ -│ (view/*, ui/*) │ -│ - View: 画面の構造を定義 │ -│ - ViewModel: Viewとビジネスロジックの橋渡し │ -│ - UI Components: 再利用可能なUIパーツ │ -└─────────────────────────────────────────────────────────┘ - ↓ ↑ - (Interface 経由) - ↓ ↑ -┌─────────────────────────────────────────────────────────┐ -│ Application Layer │ -│ (model/application/*/usecase/*) │ -│ - UseCase: ビジネスロジックの実装 │ -│ - アプリケーション固有の処理を担当 │ -└─────────────────────────────────────────────────────────┘ - ↓ ↑ -┌─────────────────────────────────────────────────────────┐ -│ Domain Layer │ -│ (model/domain/*) │ -│ - コアビジネスロジック │ -│ - フレームワークや外部ライブラリに依存しない │ -└─────────────────────────────────────────────────────────┘ - ↓ ↑ -┌─────────────────────────────────────────────────────────┐ -│ Infrastructure Layer │ -│ (model/infrastructure/repository/*) │ -│ - 外部API、データベースへのアクセス │ -│ - Repository: データソースの抽象化 │ -└─────────────────────────────────────────────────────────┘ + +### レイヤー間の依存関係 / Layer Dependencies + +```mermaid +flowchart LR + 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 +### 依存関係の方向 / Dependency Direction -クリーンアーキテクチャの原則に従い、依存関係は常に内側(Domain層)に向かいます。 +クリーンアーキテクチャの原則に従い、依存関係は常に内側(Domain層)に向かい、外側の層は内側の層を知りません。 -Following Clean Architecture principles, dependencies always point inward (toward the Domain layer). +Following Clean Architecture principles, dependencies always point inward (toward the Domain layer), and outer layers don't know about inner layers. -``` -View ──→ Interface ←── Application ──→ Domain ←── Infrastructure +```mermaid +graph LR + View["View
Layer"] -->|depends on| Interface["Interface
Layer"] + App["Application
Layer"] -->|depends on| Interface + App -->|depends on| Domain["Domain
Layer"] + Infra["Infrastructure
Layer"] -->|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層を使用 @@ -56,25 +111,105 @@ View ──→ Interface ←── Application ──→ Domain ←── Infras ## ディレクトリ構造 / Directory Structure +```mermaid +graph TB + subgraph src["📁 src/"] + subgraph interface["📋 interface/"] + IDrag["IDraggable.ts"] + IText["ITextField.ts"] + IRes["IHomeTextResponse.ts"] + end + + subgraph view["🎨 view/"] + subgraph home1["home/"] + HView["HomeView.ts"] + HVM["HomeViewModel.ts"] + end + subgraph top1["top/"] + TView["TopView.ts"] + TVM["TopViewModel.ts"] + end + end + + subgraph model["⚙️ model/"] + subgraph application["application/"] + subgraph homeApp["home/usecase/"] + StartUC["StartDragUseCase.ts"] + StopUC["StopDragUseCase.ts"] + CenterUC["CenterTextFieldUseCase.ts"] + end + subgraph topApp["top/usecase/"] + NavUC["NavigateToViewUseCase.ts"] + end + end + + subgraph domain["💎 domain/"] + subgraph callback["callback/"] + BG["Background.ts"] + end + end + + subgraph infrastructure["🔧 infrastructure/"] + subgraph repository["repository/"] + HomeRepo["HomeTextRepository.ts"] + end + end + end + + subgraph ui["🎨 ui/"] + subgraph component["component/"] + subgraph atom["atom/"] + BtnAtom["ButtonAtom.ts"] + TxtAtom["TextAtom.ts"] + end + subgraph molecule["molecule/"] + HomeMol["HomeBtnMolecule.ts"] + TopMol["TopBtnMolecule.ts"] + end + end + subgraph content["content/"] + HomeContent["HomeContent.ts"] + TopContent["TopContent.ts"] + end + end + end + + classDef interfaceClass fill:#fff9c4,stroke:#f57f17,stroke-width:2px + classDef viewClass fill:#e1f5ff,stroke:#01579b,stroke-width:2px + classDef appClass fill:#f3e5f5,stroke:#4a148c,stroke-width:2px + classDef domainClass fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px + classDef infraClass fill:#fce4ec,stroke:#880e4f,stroke-width:2px + classDef uiClass fill:#e1f5ff,stroke:#01579b,stroke-width:2px + + class interface,IDrag,IText,IRes interfaceClass + class view,home1,top1,HView,HVM,TView,TVM viewClass + class application,homeApp,topApp,StartUC,StopUC,CenterUC,NavUC appClass + class domain,callback,BG domainClass + class infrastructure,repository,HomeRepo infraClass + class ui,component,atom,molecule,content,BtnAtom,TxtAtom,HomeMol,TopMol,HomeContent,TopContent uiClass +``` + +### ファイル・ディレクトリ一覧 / File & Directory List + ``` src/ -├── interface/ # インターフェース定義 -│ ├── IDraggable.ts # ドラッグ可能なオブジェクト -│ ├── ITextField.ts # テキストフィールド -│ └── IHomeTextResponse.ts # API レスポンス型 +├── 📋 interface/ # インターフェース定義 +│ ├── IDraggable.ts # ドラッグ可能なオブジェクト +│ ├── ITextField.ts # テキストフィールド +│ └── IHomeTextResponse.ts # API レスポンス型 │ -├── view/ # View & ViewModel +├── 🎨 view/ # View & ViewModel │ ├── home/ -│ │ ├── HomeView.ts # 画面の構造定義 -│ │ └── HomeViewModel.ts # ビジネスロジックとの橋渡し +│ │ ├── HomeView.ts # 画面の構造定義 +│ │ └── HomeViewModel.ts # ビジネスロジックとの橋渡し │ └── top/ │ ├── TopView.ts │ └── TopViewModel.ts │ -├── model/ -│ ├── application/ # アプリケーション層 +├── ⚙️ model/ +│ ├── application/ # アプリケーション層 │ │ ├── home/ -│ │ │ └── usecase/ # ビジネスロジック実装 +│ │ │ └── usecase/ # ビジネスロジック実装 │ │ │ ├── StartDragUseCase.ts │ │ │ ├── StopDragUseCase.ts │ │ │ └── CenterTextFieldUseCase.ts @@ -82,23 +217,23 @@ src/ │ │ └── usecase/ │ │ └── NavigateToViewUseCase.ts │ │ -│ ├── domain/ # ドメイン層 -│ │ └── callback/ # コアビジネスロジック +│ ├── 💎 domain/ # ドメイン層 +│ │ └── callback/ # コアビジネスロジック │ │ └── Background.ts │ │ -│ └── infrastructure/ # インフラ層 +│ └── 🔧 infrastructure/ # インフラ層 │ └── repository/ │ └── HomeTextRepository.ts # データアクセス │ -└── ui/ # UIコンポーネント +└── 🎨 ui/ # UIコンポーネント ├── component/ - │ ├── atom/ # 最小単位のコンポーネント + │ ├── atom/ # 最小単位のコンポーネント │ │ ├── ButtonAtom.ts │ │ └── TextAtom.ts - │ └── molecule/ # Atomを組み合わせたコンポーネント + │ └── molecule/ # Atomを組み合わせたコンポーネント │ ├── HomeBtnMolecule.ts │ └── TopBtnMolecule.ts - └── content/ # Animation Tool生成コンテンツ + └── content/ # Animation Tool生成コンテンツ ├── HomeContent.ts └── TopContent.ts ``` @@ -161,13 +296,99 @@ export class HomeTextRepository { ## データフロー / Data Flow -### 例: ドラッグ操作の場合 +### 例: ドラッグ操作の場合 / Example: Drag Operation + +```mermaid +sequenceDiagram + actor User as 👤 User + participant View as View
(HomeView) + participant VM as ViewModel
(HomeViewModel) + participant UC as UseCase
(StartDragUseCase) + participant UI as UI Component
(HomeBtnMolecule) + participant Content as Content
(HomeContent) + + User->>View: 1. ポインターダウン
(Pointer Down) + View->>VM: 2. homeContentPointerDownEvent(event) + activate VM + VM->>VM: 3. event.currentTarget as IDraggable + VM->>UC: 4. execute(target) + activate UC + UC->>UI: 5. target.startDrag() + activate UI + UI->>Content: 6. homeContent.startDrag() + activate Content + Content-->>Content: 7. ドラッグ処理実行
(Execute drag) + Content-->>UI: 8. 完了 + deactivate Content + UI-->>UC: 9. 完了 + deactivate UI + UC-->>VM: 10. 完了 + deactivate UC + VM-->>View: 11. 完了 + deactivate VM + View-->>User: 12. ドラッグ開始
(Drag started) + + Note over View,Content: すべてインターフェース経由で通信
All communication via interfaces +``` + +### データ取得フロー / Data Fetch Flow + +```mermaid +sequenceDiagram + participant View as View
(HomeView) + participant VM as ViewModel
(HomeViewModel) + participant UC as UseCase
(FetchHomeTextUseCase) + participant Repo as Repository
(HomeTextRepository) + participant API as External API
(api/home.json) + + 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. データをViewに設定
(Set data to View) + 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] + B --> C[ViewModel
onClickStartButton] + C --> D[UseCase
NavigateToViewUseCase] + D --> E{ビジネスルール
チェック} + E -->|OK| F[app.gotoView
'home'] + E -->|NG| G[エラー処理] + F --> H[Framework
Routing処理] + H --> I[新しいView
ロード] + I --> J[ViewModel
initialize] + J --> K[View
initialize] + K --> L[View
onEnter] + 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 +``` -1. **ユーザーアクション**: ユーザーがコンテンツをポインターダウン -2. **View**: イベントをキャッチし、ViewModelのメソッドを呼び出し -3. **ViewModel**: UseCaseを実行 -4. **UseCase**: インターフェースを通じてビジネスロジックを実行 -5. **UI Component**: 実際のドラッグ処理を実行 +### コード例 / Code Example ```typescript // 1. View: イベントハンドリング @@ -177,12 +398,13 @@ homeContent.addEventListener(PointerEvent.POINTER_DOWN, // 2. ViewModel: UseCaseの実行 homeContentPointerDownEvent(event: PointerEvent): void { - const target = event.currentTarget as IDraggable; + const target = event.currentTarget as unknown as IDraggable; this.startDragUseCase.execute(target); } // 3. UseCase: ビジネスロジック execute(target: IDraggable): void { + // ビジネスルール: ドラッグ可能かチェック target.startDrag(); } From c55f367c3f4b87a666ef336c03c59a7fa457eea7 Mon Sep 17 00:00:00 2001 From: ienaga Date: Wed, 3 Dec 2025 14:05:05 +0900 Subject: [PATCH 05/35] =?UTF-8?q?#56=20markdown=E3=82=92=E6=9B=B4=E6=96=B0?= =?UTF-8?q?(WIP)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/ARCHITECTURE.md | 274 +++++++++----------- template/src/model/README.md | 472 +++++++++++++++++++++++++++++------ 2 files changed, 507 insertions(+), 239 deletions(-) diff --git a/template/ARCHITECTURE.md b/template/ARCHITECTURE.md index b553955..2fe10a1 100644 --- a/template/ARCHITECTURE.md +++ b/template/ARCHITECTURE.md @@ -8,67 +8,77 @@ This project implements a combination of Clean Architecture and MVVM pattern. ```mermaid graph TB - subgraph ViewLayer["🎨 View Layer
(view/*, ui/*)"] - View["View
画面の構造を定義"] - ViewModel["ViewModel
Viewとビジネスロジックの橋渡し"] - UI["UI Components
再利用可能なUIパーツ"] + 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
(interface/*)"] + subgraph InterfaceLayer["📋 Interface Layer"] + direction TB + InterfacePath["interface/*"] IDraggable["IDraggable"] ITextField["ITextField"] IResponse["IHomeTextResponse"] end - subgraph ApplicationLayer["⚙️ Application Layer
(model/application/*/usecase/*)"] - UseCase["UseCase
ビジネスロジックの実装"] + subgraph ApplicationLayer["⚙️ Application Layer"] + direction TB + AppPath["model/application/*/usecase/*"] + UseCase["UseCase"] + UseCaseDesc["ビジネスロジックの実装"] AppLogic["アプリケーション固有の処理"] end - subgraph DomainLayer["💎 Domain Layer
(model/domain/*)"] + subgraph DomainLayer["💎 Domain Layer"] + direction TB + DomainPath["model/domain/*"] DomainLogic["コアビジネスロジック"] DomainService["ドメインサービス"] end - subgraph InfraLayer["🔧 Infrastructure Layer
(model/infrastructure/repository/*)"] - Repository["Repository
データソースの抽象化"] + subgraph InfraLayer["🔧 Infrastructure Layer"] + direction TB + InfraPath["model/infrastructure/repository/*"] + Repository["Repository"] + RepoDesc["データソースの抽象化"] ExternalAPI["外部API・DBアクセス"] end - %% 依存関係 - View -.->|uses| ViewModel - ViewModel -.->|depends on| InterfaceLayer - ViewModel -.->|calls| UseCase - UseCase -.->|implements| InterfaceLayer - UseCase -.->|uses| DomainLogic - UseCase -.->|calls| Repository - Repository -.->|accesses| ExternalAPI - DomainService -.->|uses| DomainLogic - UI -.->|implements| InterfaceLayer - - %% スタイル + 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 View,ViewModel,UI viewStyle - class IDraggable,ITextField,IResponse interfaceStyle - class UseCase,AppLogic appStyle - class DomainLogic,DomainService domainStyle - class Repository,ExternalAPI infraStyle + 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 LR - View["View Layer
視覚表現"] - Interface["Interface Layer
抽象化"] - App["Application Layer
ユースケース"] - Domain["Domain Layer
ビジネスルール"] - Infra["Infrastructure Layer
外部接続"] +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 @@ -91,11 +101,17 @@ flowchart LR Following Clean Architecture principles, dependencies always point inward (toward the Domain layer), and outer layers don't know about inner layers. ```mermaid -graph LR - View["View
Layer"] -->|depends on| Interface["Interface
Layer"] - App["Application
Layer"] -->|depends on| Interface - App -->|depends on| Domain["Domain
Layer"] - Infra["Infrastructure
Layer"] -->|implements| Interface +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 @@ -109,96 +125,16 @@ graph LR - **Domain層**: 何にも依存しない(純粋なビジネスロジック) - **Infrastructure層**: Domain層のインターフェースを実装 -## ディレクトリ構造 / Directory Structure - -```mermaid -graph TB - subgraph src["📁 src/"] - subgraph interface["📋 interface/"] - IDrag["IDraggable.ts"] - IText["ITextField.ts"] - IRes["IHomeTextResponse.ts"] - end - - subgraph view["🎨 view/"] - subgraph home1["home/"] - HView["HomeView.ts"] - HVM["HomeViewModel.ts"] - end - subgraph top1["top/"] - TView["TopView.ts"] - TVM["TopViewModel.ts"] - end - end - - subgraph model["⚙️ model/"] - subgraph application["application/"] - subgraph homeApp["home/usecase/"] - StartUC["StartDragUseCase.ts"] - StopUC["StopDragUseCase.ts"] - CenterUC["CenterTextFieldUseCase.ts"] - end - subgraph topApp["top/usecase/"] - NavUC["NavigateToViewUseCase.ts"] - end - end - - subgraph domain["💎 domain/"] - subgraph callback["callback/"] - BG["Background.ts"] - end - end - - subgraph infrastructure["🔧 infrastructure/"] - subgraph repository["repository/"] - HomeRepo["HomeTextRepository.ts"] - end - end - end - - subgraph ui["🎨 ui/"] - subgraph component["component/"] - subgraph atom["atom/"] - BtnAtom["ButtonAtom.ts"] - TxtAtom["TextAtom.ts"] - end - subgraph molecule["molecule/"] - HomeMol["HomeBtnMolecule.ts"] - TopMol["TopBtnMolecule.ts"] - end - end - subgraph content["content/"] - HomeContent["HomeContent.ts"] - TopContent["TopContent.ts"] - end - end - end - - classDef interfaceClass fill:#fff9c4,stroke:#f57f17,stroke-width:2px - classDef viewClass fill:#e1f5ff,stroke:#01579b,stroke-width:2px - classDef appClass fill:#f3e5f5,stroke:#4a148c,stroke-width:2px - classDef domainClass fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px - classDef infraClass fill:#fce4ec,stroke:#880e4f,stroke-width:2px - classDef uiClass fill:#e1f5ff,stroke:#01579b,stroke-width:2px - - class interface,IDrag,IText,IRes interfaceClass - class view,home1,top1,HView,HVM,TView,TVM viewClass - class application,homeApp,topApp,StartUC,StopUC,CenterUC,NavUC appClass - class domain,callback,BG domainClass - class infrastructure,repository,HomeRepo infraClass - class ui,component,atom,molecule,content,BtnAtom,TxtAtom,HomeMol,TopMol,HomeContent,TopContent uiClass -``` - ### ファイル・ディレクトリ一覧 / File & Directory List ``` src/ -├── 📋 interface/ # インターフェース定義 +├── 📋 interface/ # インターフェース定義 │ ├── IDraggable.ts # ドラッグ可能なオブジェクト │ ├── ITextField.ts # テキストフィールド │ └── IHomeTextResponse.ts # API レスポンス型 │ -├── 🎨 view/ # View & ViewModel +├── 🎨 view/ # View & ViewModel │ ├── home/ │ │ ├── HomeView.ts # 画面の構造定義 │ │ └── HomeViewModel.ts # ビジネスロジックとの橋渡し @@ -301,45 +237,45 @@ export class HomeTextRepository { ```mermaid sequenceDiagram actor User as 👤 User - participant View as View
(HomeView) - participant VM as ViewModel
(HomeViewModel) - participant UC as UseCase
(StartDragUseCase) - participant UI as UI Component
(HomeBtnMolecule) - participant Content as Content
(HomeContent) - - User->>View: 1. ポインターダウン
(Pointer Down) - View->>VM: 2. homeContentPointerDownEvent(event) + 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. event.currentTarget as IDraggable - VM->>UC: 4. execute(target) + VM->>VM: 3. cast to IDraggable + VM->>UC: 4. execute() activate UC - UC->>UI: 5. target.startDrag() + UC->>UI: 5. startDrag() activate UI - UI->>Content: 6. homeContent.startDrag() + UI->>Content: 6. content.startDrag() activate Content - Content-->>Content: 7. ドラッグ処理実行
(Execute drag) - Content-->>UI: 8. 完了 + Content-->>Content: 7. Execute drag + Content-->>UI: 8. Complete deactivate Content - UI-->>UC: 9. 完了 + UI-->>UC: 9. Complete deactivate UI - UC-->>VM: 10. 完了 + UC-->>VM: 10. Complete deactivate UC - VM-->>View: 11. 完了 + VM-->>View: 11. Complete deactivate VM - View-->>User: 12. ドラッグ開始
(Drag started) + View-->>User: 12. Drag started - Note over View,Content: すべてインターフェース経由で通信
All communication via interfaces + Note over View,Content: Interface-based communication ``` ### データ取得フロー / Data Fetch Flow ```mermaid sequenceDiagram - participant View as View
(HomeView) - participant VM as ViewModel
(HomeViewModel) - participant UC as UseCase
(FetchHomeTextUseCase) - participant Repo as Repository
(HomeTextRepository) - participant API as External API
(api/home.json) + 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 @@ -351,35 +287,49 @@ sequenceDiagram activate API API-->>Repo: 5. JSON Response deactivate API - Repo->>Repo: 6. エラーチェック
(Error check) + Repo->>Repo: 6. Error check Repo-->>UC: 7. IHomeTextResponse deactivate Repo - UC->>UC: 8. ビジネスロジック
(Business logic) - UC-->>VM: 9. データ返却
(Return data) + UC->>UC: 8. Business logic + UC-->>VM: 9. Return data deactivate UC - VM->>View: 10. データをViewに設定
(Set data to View) + VM->>View: 10. Set data deactivate VM - View->>View: 11. 画面更新
(Update UI) + View->>View: 11. Update UI - Note over Repo,API: エラーハンドリングと
型安全性を保証
(Error handling &
type safety) + Note over Repo,API: Error handling & type safety ``` ### 画面遷移フロー / View Navigation Flow ```mermaid flowchart TD - A[👤 User clicks button] --> B[View
Button Event] - B --> C[ViewModel
onClickStartButton] - C --> D[UseCase
NavigateToViewUseCase] - D --> E{ビジネスルール
チェック} - E -->|OK| F[app.gotoView
'home'] - E -->|NG| G[エラー処理] - F --> H[Framework
Routing処理] - H --> I[新しいView
ロード] - I --> J[ViewModel
initialize] - J --> K[View
initialize] - K --> L[View
onEnter] - L --> M[🎨 画面表示] + 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 diff --git a/template/src/model/README.md b/template/src/model/README.md index ece6654..c46a29d 100644 --- a/template/src/model/README.md +++ b/template/src/model/README.md @@ -1,118 +1,436 @@ # 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/ +│ ├── Background.ts +│ └── service/ +│ ├── BackgroundDrawService.ts +│ └── BackgroundChangeScaleService.ts +│ +└── infrastructure/ # インフラストラクチャ層 + └── repository/ + └── HomeTextRepository.ts +``` + +## 🎨 アーキテクチャ概要 / 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 +``` + +## ⚙️ Application Layer + +### 役割 / Role + +- ユーザーのアクションに対応するビジネスロジックを実装 +- 各ユーザーアクションごとにUseCaseクラスを作成 +- インターフェースを通じてDomainとInfrastructureにアクセス + +Implements business logic corresponding to user actions. Creates a UseCase class for each user action. Accesses Domain and Infrastructure through interfaces. + +### ディレクトリ構造 / Directory Structure ```sh - application - └── content +application/ +├── home/ # Home画面のビジネスロジック +│ └── usecase/ +│ ├── StartDragUseCase.ts # ドラッグ開始 +│ ├── StopDragUseCase.ts # ドラッグ停止 +│ └── CenterTextFieldUseCase.ts # テキスト中央揃え +└── top/ # Top画面のビジネスロジック + └── usecase/ + └── NavigateToViewUseCase.ts # 画面遷移 ``` -`content` ディレクトリにはAnimation Toolで作成された `DisplayObject` を動的に生成するためのクラスが格納されてます。`usecase` ディレクトリを作成して、ビジネスロジックを実装し、`domain` へのアクセスを行う責務を担います。 +### 実装例 / Implementation Example -The `content` directory contains classes for dynamically generating `DisplayObject`s created by the Animation Tool. The `usecase` directory is created to implement business logic and handle the responsibility of accessing the `domain`. +#### StartDragUseCase.ts -#### Example of cooperation with Animation Tool +```typescript +import type { IDraggable } from "@/interface/IDraggable"; -`namespace` にAnimation Toolのシンボルに設定した名前を追記する事で動的生成が可能になります。 -Dynamic generation is enabled by appending the name set for the Animation Tool symbol in the `namespace` field. +export class StartDragUseCase { + /** + * @description ドラッグ可能なオブジェクトのドラッグを開始 + * Start dragging a draggable object + */ + execute(target: IDraggable): void { + target.startDrag(); + } +} +``` -```javascript -import { MovieClipContent } from "@next2d/framework"; +#### NavigateToViewUseCase.ts -/** - * @see file/sample.n2d - * @class - * @extends {MovieClipContent} - */ -export class TopContent extends MovieClipContent -{ +```typescript +import { app } from "@next2d/framework"; + +export class NavigateToViewUseCase { /** - * @return {string} - * @readonly - * @public + * @description 指定された画面に遷移 + * Navigate to the specified view */ - get namespace () - { - // Animation Toolのsymbolで設定した名前を追記 - // Append the name assigned to the Animation Tool symbol. - return "TopContent"; + async execute(viewName: string): Promise { + await app.gotoView(viewName); } } ``` -### Domain +### 特徴 / 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 ```sh -domain -├── callback -└── event - ├── top - └── home +domain/ +└── callback/ + └── Background/ + ├── Background.ts # グラデーション背景クラス + └── service/ + ├── BackgroundDrawService.ts # 描画サービス + └── BackgroundChangeScaleService.ts # スケール変更サービス ``` -アプリケーションの固有ロジックを格納するディレクトリで、プロジェクトの核心になる層です。このテンプレートでは `callback` で、背景に全画面のグラデーション描画を行なっています。ドメインロジックは外部の詳細実装に依存せず、インターフェースを通じて抽象化された依存関係を持つべきです。 +### 実装例 / 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); + } +} +``` + +#### BackgroundDrawService.ts + +```typescript +import type { Background } from "../Background"; +import { config } from "@/config/Config"; +import { Matrix } from "@next2d/geom"; + +/** + * @description 背景のグラデーション描画を実行 + * Execute background gradient drawing + */ +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 -This directory stores the application-specific logic and is the core layer of the project. The `callback` generates the background for all screens. Domain logic should not depend on external implementation details and should have dependencies abstracted through interfaces. +### 役割 / Role -### Infrastructure +- 外部システムとの連携(API、データベース等) +- データアクセスの実装 +- エラーハンドリングと型安全性の保証 + +Integrates with external systems (APIs, databases, etc.). Implements data access. Ensures error handling and type safety. + +### ディレクトリ構造 / Directory Structure ```sh -infrastructure -└── repository +infrastructure/ +└── repository/ + └── HomeTextRepository.ts # Home画面テキストデータアクセス +``` + +### 実装例 / Implementation Example + +#### HomeTextRepository.ts + +```typescript +import type { IHomeTextResponse } from "@/interface/IHomeTextResponse"; +import { config } from "@/config/Config"; + +export class HomeTextRepository { + /** + * @description Home画面のテキストデータを取得 + * Get text data for Home screen + */ + 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; + } + } +} +``` + +### 特徴 / Features + +- ✅ **型安全性** - `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 ``` -外部へのアクセスロジックを格納するディレクトリです。データベースからの情報であれば `entity` ディレクトリを作成して可変可能オブジェクトとして運用し、可変想定がないオブジェクトなどはデータ転送オブジェクト(DTO)として `dto` ディレクトリにそれぞれ責務を分散させるのが良いかもしれません。Repositoryは具体的な実装であり、ドメイン層からはインターフェースを通じてアクセスされるべきです。 +## 📋 設計原則 / Design Principles -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). Repositories are concrete implementations and should be accessed from the domain layer through interfaces. +### 1. 依存関係の方向 / Dependency Direction -### UI(User Interface) +``` +View Layer → Application Layer → Domain Layer + ↓ + Infrastructure Layer +``` -このテンプレートでは、UIコンポーネントは `src/ui` ディレクトリに配置されており、`model/ui` ではありません。 +- **Application層** は **Domain層** と **Infrastructure層** に依存 +- **Domain層** は何にも依存しない(最も安定) +- **Infrastructure層** は **Interface層** を実装 -In this template, UI components are located in the `src/ui` directory, not in `model/ui`. +### 2. インターフェース駆動 / Interface-Driven -## UI Components +すべての層間通信はインターフェースを経由: + +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 -ui -└── component - ├── atom - └── molecule +# 1. ディレクトリ作成 +model/application/{screen-name}/usecase/ + +# 2. UseCaseファイル作成 +model/application/{screen-name}/usecase/YourUseCase.ts + +# 3. インターフェース定義(必要に応じて) +interface/IYourInterface.ts ``` -アトミックデザインを意識したディレクトリ構成になってます。`atom` ディレクトリに最小の表示要素を作成して、`molecule` ディレクトリで各atomの要素を組み合わせたコンポーネントを作成します。UIコンポーネントはインターフェースを実装することで、ビジネスロジック層から抽象化された形で扱われます。 +### 2. Domain Logicの追加 + +```sh +# 1. ディレクトリ作成 +model/domain/{feature-name}/ + +# 2. ドメインロジック作成 +model/domain/{feature-name}/YourDomainLogic.ts +model/domain/{feature-name}/service/YourService.ts +``` + +### 3. Repositoryの追加 + +```sh +# 1. インターフェース定義 +interface/IYourResponse.ts + +# 2. Repository作成 +model/infrastructure/repository/YourRepository.ts +``` + +## ✅ ベストプラクティス / 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 `molecule` directory, components are created by combining atom elements. UI components implement interfaces to be handled in an abstracted manner from the business logic layer. \ 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 From e2b9ecc10869d05060aa8fa25c39827845e76428 Mon Sep 17 00:00:00 2001 From: ienaga Date: Wed, 3 Dec 2025 14:07:08 +0900 Subject: [PATCH 06/35] =?UTF-8?q?#56=20markdown=E3=82=92=E6=9B=B4=E6=96=B0?= =?UTF-8?q?(WIP)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/src/view/README.md | 86 ++++++++++++++++++++++++++++--------- 1 file changed, 66 insertions(+), 20 deletions(-) diff --git a/template/src/view/README.md b/template/src/view/README.md index b200eba..3210b99 100644 --- a/template/src/view/README.md +++ b/template/src/view/README.md @@ -10,27 +10,73 @@ The basic style is to create one set of View and ViewModel per screen. It is rec 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 ``` -┌─────────────────────────────────────────┐ -│ View Layer │ -│ - 画面の構造と表示を担当 │ -│ - ビジネスロジックは持たない │ -└─────────────────────────────────────────┘ - ↓ ↑ -┌─────────────────────────────────────────┐ -│ ViewModel Layer │ -│ - ViewとModelの橋渡し │ -│ - UseCaseを保持 │ -│ - イベントハンドリング │ -└─────────────────────────────────────────┘ - ↓ ↑ - (Interface 経由) - ↓ ↑ -┌─────────────────────────────────────────┐ -│ Model Layer │ -│ - ビジネスロジック (UseCase) │ -│ - データアクセス (Repository) │ -└─────────────────────────────────────────┘ + +### 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 From fdca50f8e1cf635b3186a4ab8dbefaf17e105765 Mon Sep 17 00:00:00 2001 From: ienaga Date: Wed, 3 Dec 2025 14:11:20 +0900 Subject: [PATCH 07/35] =?UTF-8?q?#56=20markdown=E3=82=92=E6=9B=B4=E6=96=B0?= =?UTF-8?q?(WIP)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/src/view/README.md | 217 ++++++++++++++++++++++++++++++++++-- 1 file changed, 209 insertions(+), 8 deletions(-) diff --git a/template/src/view/README.md b/template/src/view/README.md index 3210b99..5c4f9fa 100644 --- a/template/src/view/README.md +++ b/template/src/view/README.md @@ -118,6 +118,184 @@ It is a container attached to the main context. Therefore, its implementation is - ❌ **データアクセス** - 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 ```typescript @@ -133,6 +311,9 @@ import { PointerEvent, Event } from "@next2d/events"; */ export class HomeView extends View { + private autoSlideTimer: number | null = null; + private isActive: boolean = false; + /** * @param {HomeViewModel} vm * @constructor @@ -145,8 +326,8 @@ export class HomeView extends View } /** - * @description 画面の初期化 - * Initialize the screen + * @description 画面の初期化 - UIコンポーネントの構築 + * Initialize - Build UI components * * @return {Promise} * @method @@ -159,6 +340,7 @@ export class HomeView extends View const homeContent = new HomeBtnMolecule(); homeContent.x = 120; homeContent.y = 120; + homeContent.name = "homeContent"; // イベントをViewModelに委譲 homeContent.addEventListener( @@ -170,8 +352,8 @@ export class HomeView extends View } /** - * @description 画面表示時の処理 - * Called when screen is shown + * @description 画面表示時の処理 - アニメーション開始、データ取得 + * On screen shown - Start animations, fetch data * * @return {Promise} * @method @@ -180,12 +362,24 @@ export class HomeView extends View */ 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 画面非表示時の処理 - * Called when screen is hidden + * @description 画面非表示時の処理 - クリーンアップ + * On screen hidden - Clean up resources * * @return {Promise} * @method @@ -194,7 +388,14 @@ export class HomeView extends View */ async onExit (): Promise { - // クリーンアップ処理 + // タイマーのクリア + if (this.autoSlideTimer) { + clearInterval(this.autoSlideTimer); + this.autoSlideTimer = null; + } + + // 非アクティブ状態に設定 + this.isActive = false; } } ``` From 35b87ae5fc6651e2d2b1c20bb1e15b7f0a6e3e50 Mon Sep 17 00:00:00 2001 From: ienaga Date: Wed, 3 Dec 2025 14:28:24 +0900 Subject: [PATCH 08/35] =?UTF-8?q?#56=20markdown=E3=82=92=E6=9B=B4=E6=96=B0?= =?UTF-8?q?(WIP)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/src/view/README.md | 202 ++++++++++++++++++++++++++++- template/src/view/home/HomeView.ts | 4 +- 2 files changed, 202 insertions(+), 4 deletions(-) diff --git a/template/src/view/README.md b/template/src/view/README.md index 5c4f9fa..ed0fb4b 100644 --- a/template/src/view/README.md +++ b/template/src/view/README.md @@ -422,6 +422,135 @@ Acts as a bridge between View and Model. Holds UseCases and processes events fro - ❌ **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 ```typescript @@ -432,6 +561,7 @@ 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 @@ -443,6 +573,10 @@ export class HomeViewModel extends ViewModel private readonly startDragUseCase: StartDragUseCase; private readonly stopDragUseCase: StopDragUseCase; private readonly centerTextFieldUseCase: CenterTextFieldUseCase; + + // 画面の状態管理 + private homeText: string = ""; + private isLoading: boolean = true; /** * @description ViewModelの初期化とUseCaseの注入 @@ -462,6 +596,10 @@ export class HomeViewModel extends ViewModel } /** + * @description ViewModelの初期化 - データ取得と状態準備 + * Initialize ViewModel - Fetch data and prepare state + * ⭐ Viewのinitialize()より前に呼ばれる + * * @return {Promise} * @method * @override @@ -469,8 +607,29 @@ export class HomeViewModel extends ViewModel */ async initialize (): Promise { - // 初期化処理(必要に応じて) - return void 0; + // 初期データの取得(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; } /** @@ -523,6 +682,45 @@ export class HomeViewModel extends ViewModel } ``` +### 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 diff --git a/template/src/view/home/HomeView.ts b/template/src/view/home/HomeView.ts index b7c5f76..3b4fdc7 100644 --- a/template/src/view/home/HomeView.ts +++ b/template/src/view/home/HomeView.ts @@ -43,10 +43,10 @@ export class HomeView extends View * Send home content events to ViewModel */ homeContent.addEventListener(PointerEvent.POINTER_DOWN, - this.vm.homeContentPointerDownEvent + this.vm.homeContentPointerDownEvent.bind(this.vm) ); homeContent.addEventListener(PointerEvent.POINTER_UP, - this.vm.homeContentPointerUpEvent + this.vm.homeContentPointerUpEvent.bind(this.vm) ); /** From 9aa367afc64900c139e126a5da1646fd332c6c6a Mon Sep 17 00:00:00 2001 From: ienaga Date: Wed, 3 Dec 2025 21:09:04 +0900 Subject: [PATCH 09/35] =?UTF-8?q?#56=20=E3=83=AA=E3=83=95=E3=82=A1?= =?UTF-8?q?=E3=82=AF=E3=82=BF=E3=83=AA=E3=83=B3=E3=82=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/README.md | 8 +++++++ template/src/interface/ITextFieldAutoSize.ts | 1 + template/src/interface/ITextFieldProps.ts | 23 ++++++++++++-------- template/src/interface/ITextFieldType.ts | 1 + template/src/model/domain/README.md | 7 +++++- template/src/ui/component/atom/TextAtom.ts | 3 ++- template/src/view/home/HomeView.ts | 12 +++++----- template/src/view/home/HomeViewModel.ts | 19 ++++++++++++++-- 8 files changed, 56 insertions(+), 18 deletions(-) create mode 100644 template/src/interface/ITextFieldAutoSize.ts create mode 100644 template/src/interface/ITextFieldType.ts diff --git a/template/README.md b/template/README.md index 2d85a0f..0e8e491 100644 --- a/template/README.md +++ b/template/README.md @@ -2,6 +2,14 @@ This project was bootstrapped with [Create Next2D App](https://github.com/Next2D/create-next2d-app). +## Architecture + +このプロジェクトは **MVVM + Clean Architecture + Atomic Design** を採用しています。 +アーキテクチャの詳細は [ARCHITECTURE.md](./ARCHITECTURE.md) を参照してください。 + +This project adopts **MVVM + Clean Architecture + Atomic Design**. +See [ARCHITECTURE.md](./ARCHITECTURE.md) for architecture details. + ## Available Scripts In the project directory, you can run: 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 index 401bfa7..63ed748 100644 --- a/template/src/interface/ITextFieldProps.ts +++ b/template/src/interface/ITextFieldProps.ts @@ -1,11 +1,16 @@ +import type { ITextFieldAutoSize } from "./ITextFieldAutoSize"; +import type { ITextFieldType } from "./ITextFieldType"; + export interface ITextFieldProps { - selectable: boolean | null; - mouseEnabled: boolean | null; - wordWrap: boolean | null; - multiline: boolean | null; - maxChars: number | null; - background: boolean | null; - backgroundColor: number | null; - border: boolean | null; - borderColor: number | null; + 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/model/domain/README.md b/template/src/model/domain/README.md index a81abbd..46d2f9a 100644 --- a/template/src/model/domain/README.md +++ b/template/src/model/domain/README.md @@ -10,7 +10,7 @@ Domain層は、アプリケーションの核心となるビジネスルール The Domain layer holds the core business rules of the application. This layer has the following characteristics: -- ✅ **純粋なビジネスロジック** - フレームワークに依存しない +- ✅ **純粋なビジネスロジック** - フレームワークに依存しない(※注: 一部Next2D固有機能を使用) - ✅ **再利用可能なロジック** - アプリケーション全体で利用される - ✅ **ドメイン知識の表現** - ビジネスルールを明確に表現 - ✅ **安定性** - 外部の変更に影響されにくい @@ -215,6 +215,11 @@ export class ValidationService { } // ⚠️ 注意: Next2D固有の機能を使う場合は明確に +// このプロジェクトではNext2Dの描画機能(Shape, stage等)を +// Domain層で使用することを許容しています。 +// これはNext2Dフレームワーク固有の設計判断であり、 +// 純粋なクリーンアーキテクチャからは逸脱していますが、 +// 描画ロジックの再利用性を優先した設計です。 export class Background { // Next2Dのshapeを使用(このプロジェクトでは許容) public readonly shape: Shape; diff --git a/template/src/ui/component/atom/TextAtom.ts b/template/src/ui/component/atom/TextAtom.ts index 5cba877..737efb6 100644 --- a/template/src/ui/component/atom/TextAtom.ts +++ b/template/src/ui/component/atom/TextAtom.ts @@ -1,4 +1,5 @@ import type { ITextField } from "@/interface/ITextField"; +import type { ITextFieldProps } from "@/interface/ITextFieldProps"; import type { ITextFormatObject } from "@/interface/ITextFormatObject"; import { TextField } from "@next2d/text"; @@ -22,7 +23,7 @@ export class TextAtom extends TextField implements ITextField { */ constructor( text: string = "", - props: any | null = null, + props: ITextFieldProps | null = null, format_object: ITextFormatObject | null = null ) { super(); diff --git a/template/src/view/home/HomeView.ts b/template/src/view/home/HomeView.ts index 3b4fdc7..e141de7 100644 --- a/template/src/view/home/HomeView.ts +++ b/template/src/view/home/HomeView.ts @@ -1,5 +1,5 @@ import type { HomeViewModel } from "./HomeViewModel"; -import { View, app } from "@next2d/framework"; +import { View } from "@next2d/framework"; import { config } from "@/config/Config"; import { HomeBtnMolecule } from "@/ui/component/molecule/HomeBtnMolecule"; import { TextAtom } from "@/ui/component/atom/TextAtom"; @@ -55,9 +55,11 @@ export class HomeView extends View */ this.addChild(homeContent); - // Hello, World. - const response = app.getResponse(); - const text = response.has("HomeText") ? response.get("HomeText").word : ""; + /** + * ホームテキストをViewModelから取得 + * Get home text from ViewModel + */ + const text = this.vm.getHomeText(); const textField = new TextAtom(text, { "autoSize": "center", "type": "input" @@ -75,7 +77,7 @@ export class HomeView extends View * Send home text events to ViewModel */ textField.addEventListener(Event.CHANGE, - this.vm.homeTextChangeEvent + this.vm.homeTextChangeEvent.bind(this.vm) ); /** diff --git a/template/src/view/home/HomeViewModel.ts b/template/src/view/home/HomeViewModel.ts index 14c9016..b2e6be7 100644 --- a/template/src/view/home/HomeViewModel.ts +++ b/template/src/view/home/HomeViewModel.ts @@ -1,6 +1,6 @@ import type { IDraggable } from "@/interface/IDraggable"; import type { ITextField } from "@/interface/ITextField"; -import { ViewModel } from "@next2d/framework"; +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"; @@ -15,6 +15,7 @@ export class HomeViewModel extends ViewModel private readonly startDragUseCase: StartDragUseCase; private readonly stopDragUseCase: StopDragUseCase; private readonly centerTextFieldUseCase: CenterTextFieldUseCase; + private homeText: string = ""; /** * @constructor @@ -36,7 +37,21 @@ export class HomeViewModel extends ViewModel */ async initialize (): Promise { - return void 0; + const response = app.getResponse(); + this.homeText = response.has("HomeText") ? response.get("HomeText").word : ""; + } + + /** + * @description ホームテキストを取得 + * Get home text + * + * @return {string} + * @method + * @public + */ + getHomeText (): string + { + return this.homeText; } /** From b93c31e4b8ba28b682b3a4e6573e2e0015d11e95 Mon Sep 17 00:00:00 2001 From: ienaga Date: Wed, 3 Dec 2025 21:16:16 +0900 Subject: [PATCH 10/35] =?UTF-8?q?#56=20=E3=83=AA=E3=83=95=E3=82=A1?= =?UTF-8?q?=E3=82=AF=E3=82=BF=E3=83=AA=E3=83=B3=E3=82=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/src/interface/IViewName.ts | 7 +++++++ .../home/usecase/CenterTextFieldUseCase.ts | 6 +++--- .../top/usecase/NavigateToViewUseCase.ts | 5 +++-- .../ui/component/molecule/TopBtnMolecule.ts | 7 ++----- template/src/view/home/HomeViewModel.ts | 3 ++- template/src/view/top/TopView.ts | 2 +- template/src/view/top/TopViewModel.ts | 19 +++++++++++++++++-- 7 files changed, 35 insertions(+), 14 deletions(-) create mode 100644 template/src/interface/IViewName.ts 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/model/application/home/usecase/CenterTextFieldUseCase.ts b/template/src/model/application/home/usecase/CenterTextFieldUseCase.ts index 9693625..88f1710 100644 --- a/template/src/model/application/home/usecase/CenterTextFieldUseCase.ts +++ b/template/src/model/application/home/usecase/CenterTextFieldUseCase.ts @@ -1,5 +1,4 @@ import type { ITextField } from "@/interface/ITextField"; -import { config } from "@/config/Config"; /** * @description テキストフィールド中央揃えのユースケース @@ -14,12 +13,13 @@ export class CenterTextFieldUseCase * Center the text field on the screen * * @param {ITextField} textField + * @param {number} stageWidth - ステージの幅 / Stage width * @return {void} * @method * @public */ - execute (textField: ITextField): void + execute (textField: ITextField, stageWidth: number): void { - textField.x = (config.stage.width - textField.width) / 2; + textField.x = (stageWidth - textField.width) / 2; } } diff --git a/template/src/model/application/top/usecase/NavigateToViewUseCase.ts b/template/src/model/application/top/usecase/NavigateToViewUseCase.ts index 6ed0f92..0b03567 100644 --- a/template/src/model/application/top/usecase/NavigateToViewUseCase.ts +++ b/template/src/model/application/top/usecase/NavigateToViewUseCase.ts @@ -1,3 +1,4 @@ +import type { ViewName } from "@/interface/IViewName"; import { app } from "@next2d/framework"; /** @@ -12,12 +13,12 @@ export class NavigateToViewUseCase * @description 指定された画面に遷移する * Navigate to the specified view * - * @param {string} viewName + * @param {ViewName} viewName * @return {Promise} * @method * @public */ - async execute (viewName: string): Promise + async execute (viewName: ViewName): Promise { await app.gotoView(viewName); } diff --git a/template/src/ui/component/molecule/TopBtnMolecule.ts b/template/src/ui/component/molecule/TopBtnMolecule.ts index 6904967..5d79bfd 100644 --- a/template/src/ui/component/molecule/TopBtnMolecule.ts +++ b/template/src/ui/component/molecule/TopBtnMolecule.ts @@ -1,7 +1,6 @@ import { TopBtnEntranceAnimation } from "@/ui/animation/top/TopBtnEntranceAnimation"; import { ButtonAtom } from "../atom/ButtonAtom"; import { TextAtom } from "../atom/TextAtom"; -import { app } from "@next2d/framework"; /** * @description Top画面のボタン分子 @@ -14,16 +13,14 @@ import { app } from "@next2d/framework"; export class TopBtnMolecule extends ButtonAtom { /** + * @param {string} text - ボタンに表示するテキスト / Text to display on the button * @constructor * @public */ - constructor () + constructor (text: string) { super(); - const response = app.getResponse(); - - const text = response.has("TopText") ? response.get("TopText").word : ""; const textField = new TextAtom(text, { "autoSize": "center" }); diff --git a/template/src/view/home/HomeViewModel.ts b/template/src/view/home/HomeViewModel.ts index b2e6be7..3dab1b1 100644 --- a/template/src/view/home/HomeViewModel.ts +++ b/template/src/view/home/HomeViewModel.ts @@ -5,6 +5,7 @@ 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 @@ -96,6 +97,6 @@ export class HomeViewModel extends ViewModel homeTextChangeEvent (event: Event): void { const textField = event.currentTarget as unknown as ITextField; - this.centerTextFieldUseCase.execute(textField); + 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 acb3311..6c51ef8 100644 --- a/template/src/view/top/TopView.ts +++ b/template/src/view/top/TopView.ts @@ -45,7 +45,7 @@ export class TopView extends View * Topボタンを生成して、座標をセット * Create Top button and set coordinates */ - const topBtn = new TopBtnMolecule(); + const topBtn = new TopBtnMolecule(this.vm.getTopText()); topBtn.name = "topBtn"; topBtn.x = config.stage.width / 2; topBtn.y = config.stage.height / 2 + topContent.height / 2 + topBtn.height; diff --git a/template/src/view/top/TopViewModel.ts b/template/src/view/top/TopViewModel.ts index efb8db6..9a7a89e 100644 --- a/template/src/view/top/TopViewModel.ts +++ b/template/src/view/top/TopViewModel.ts @@ -1,4 +1,4 @@ -import { ViewModel } from "@next2d/framework"; +import { ViewModel, app } from "@next2d/framework"; import { NavigateToViewUseCase } from "@/model/application/top/usecase/NavigateToViewUseCase"; /** @@ -8,6 +8,7 @@ import { NavigateToViewUseCase } from "@/model/application/top/usecase/NavigateT export class TopViewModel extends ViewModel { private readonly navigateToViewUseCase: NavigateToViewUseCase; + private topText: string = ""; /** * @constructor @@ -27,7 +28,21 @@ export class TopViewModel extends ViewModel */ async initialize (): Promise { - return void 0; + const response = app.getResponse(); + this.topText = response.has("TopText") ? response.get("TopText").word : ""; + } + + /** + * @description Topテキストを取得 + * Get top text + * + * @return {string} + * @method + * @public + */ + getTopText (): string + { + return this.topText; } /** From 6987c18a581c4ab3cfd8d12ddc264416380165e7 Mon Sep 17 00:00:00 2001 From: ienaga Date: Wed, 3 Dec 2025 21:25:01 +0900 Subject: [PATCH 11/35] =?UTF-8?q?#56=20AI=E5=AE=9F=E8=A3=85=E3=81=AB?= =?UTF-8?q?=E5=BF=85=E8=A6=81=E3=81=AAmarkdown=E3=83=95=E3=82=A1=E3=82=A4?= =?UTF-8?q?=E3=83=AB=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/ARCHITECTURE.md | 3 +- template/README.md | 236 ++++++++++++++++++--- template/src/interface/IGotoView.ts | 12 ++ template/src/interface/README.md | 15 ++ template/src/model/application/README.md | 19 +- template/src/ui/README.md | 7 +- template/src/ui/component/atom/TextAtom.ts | 14 +- 7 files changed, 258 insertions(+), 48 deletions(-) diff --git a/template/ARCHITECTURE.md b/template/ARCHITECTURE.md index 2fe10a1..1fd5b06 100644 --- a/template/ARCHITECTURE.md +++ b/template/ARCHITECTURE.md @@ -132,7 +132,8 @@ src/ ├── 📋 interface/ # インターフェース定義 │ ├── IDraggable.ts # ドラッグ可能なオブジェクト │ ├── ITextField.ts # テキストフィールド -│ └── IHomeTextResponse.ts # API レスポンス型 +│ ├── IHomeTextResponse.ts # API レスポンス型 +│ └── IViewName.ts # 画面名の型定義 │ ├── 🎨 view/ # View & ViewModel │ ├── home/ diff --git a/template/README.md b/template/README.md index 0e8e491..fc9e88f 100644 --- a/template/README.md +++ b/template/README.md @@ -1,54 +1,228 @@ -# 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). -## Architecture +--- + +## 目次 / 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) +- [ライセンス / License](#ライセンス--license) + +--- + +## 必要な環境 / Requirements + +| ツール / Tool | バージョン / Version | +|--------------|---------------------| +| Node.js | 18.x 以上 / 18.x or higher | +| npm | 9.x 以上 / 9.x or higher | + +### オプション / Optional + +iOS/Androidビルドを行う場合は、以下も必要です。 + +For iOS/Android builds, the following are also required: + +- **iOS**: Xcode 14 以上、macOS +- **Android**: Android Studio、JDK 17 以上 + +--- + +## セットアップ / Setup + +### 1. リポジトリのクローン / Clone the repository + +```bash +git clone +cd +``` -このプロジェクトは **MVVM + Clean Architecture + Atomic Design** を採用しています。 -アーキテクチャの詳細は [ARCHITECTURE.md](./ARCHITECTURE.md) を参照してください。 +### 2. 依存パッケージのインストール / Install dependencies -This project adopts **MVVM + Clean Architecture + Atomic Design**. -See [ARCHITECTURE.md](./ARCHITECTURE.md) for architecture details. +```bash +npm install +``` -## Available Scripts +### 3. 開発サーバーの起動 / Start the development server -In the project directory, you can run: +```bash +npm start +``` + +ブラウザで [http://localhost:5173](http://localhost:5173) を開いてください。 + +Open [http://localhost:5173](http://localhost:5173) in your browser. + +--- + +## アーキテクチャ / Architecture + +このプロジェクトは **MVVM + Clean Architecture + Atomic Design** を採用しています。 + +This project adopts **MVVM + Clean Architecture + Atomic Design**. + +``` +┌─────────────────────────────────────────────────────────┐ +│ 🎨 View Layer (view/, ui/) │ +│ - View: 画面の構造定義 / Screen structure │ +│ - ViewModel: ビジネスロジックとの橋渡し / Bridge │ +│ - UI Components: 再利用可能なUIパーツ / Reusable UI │ +├─────────────────────────────────────────────────────────┤ +│ 📋 Interface Layer (interface/) │ +│ - 型定義とインターフェース / Type definitions │ +├─────────────────────────────────────────────────────────┤ +│ ⚙️ Application Layer (model/application/) │ +│ - UseCase: ビジネスロジック実装 / Business logic │ +├─────────────────────────────────────────────────────────┤ +│ 💎 Domain Layer (model/domain/) │ +│ - コアビジネスルール / Core business rules │ +├─────────────────────────────────────────────────────────┤ +│ 🔧 Infrastructure Layer (model/infrastructure/) │ +│ - Repository: データアクセス / Data access │ +└─────────────────────────────────────────────────────────┘ +``` + +詳細は [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 クラスを自動生成します。 +新しい画面を追加する際に便利です。 -## Unit Test +Generates the necessary View and ViewModel classes from the `routing.json` file. +Useful when adding new screens. + +--- + +## プラットフォームエミュレーター / 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/ +│ ├── component/ +│ │ ├── atom/ # 最小コンポーネント / Smallest components +│ │ └── molecule/# 複合コンポーネント / Composite components +│ ├── content/ # Animation Tool コンテンツ / Animation Tool content +│ └── animation/ # アニメーション定義 / Animation definitions +└── view/ # View & ViewModel +``` + +各ディレクトリの詳細は、ディレクトリ内の `README.md` を参照してください。 + +See the `README.md` in each directory for details. + +--- + +## ライセンス / License + +MIT License -## Build +--- -### `npm run build:web -- --env prd` -### `npm run build:steam:windows -- --env prd` -### `npm run build:steam:macos -- --env prd` -### `npm run build:steam:linux -- --env prd` -### `npm run build:ios -- --env prd` -### `npm run build:android -- --env prd` +## 関連リンク / Related Links -Multi-platform builder, writes to various platforms including macOS, Windows, iOS, Android, and Web (HTML). -Builds apps for the environment specified by env=***. \ 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/src/interface/IGotoView.ts b/template/src/interface/IGotoView.ts index d94f966..9ba75c5 100644 --- a/template/src/interface/IGotoView.ts +++ b/template/src/interface/IGotoView.ts @@ -1,3 +1,15 @@ +/** + * @description 画面遷移オプションのインターフェース + * Interface for view navigation options + * + * @interface + */ export interface IGotoView { + /** + * @description 画面遷移後に実行するコールバック関数名 + * Callback function name(s) to execute after view transition + * + * @type {string | string[]} + */ callback: string | string[]; } \ No newline at end of file diff --git a/template/src/interface/README.md b/template/src/interface/README.md index 2417a5e..814e6e4 100644 --- a/template/src/interface/README.md +++ b/template/src/interface/README.md @@ -144,6 +144,21 @@ 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 diff --git a/template/src/model/application/README.md b/template/src/model/application/README.md index b6f4804..6185855 100644 --- a/template/src/model/application/README.md +++ b/template/src/model/application/README.md @@ -83,6 +83,7 @@ export class StartDragUseCase ### Example: NavigateToViewUseCase ```typescript +import type { ViewName } from "@/interface/IViewName"; import { app } from "@next2d/framework"; /** @@ -97,12 +98,12 @@ export class NavigateToViewUseCase * @description 指定された画面に遷移する * Navigate to the specified view * - * @param {string} viewName + * @param {ViewName} viewName * @return {Promise} * @method * @public */ - async execute (viewName: string): Promise + async execute (viewName: ViewName): Promise { // ビジネスルール: 遷移前の検証など // 例: 未保存データのチェック、権限確認など @@ -112,11 +113,14 @@ export class NavigateToViewUseCase } ``` +**ポイント / Key Points:** +- `ViewName` 型を使用することで、存在しない画面名を指定するとコンパイルエラーになる +- 型安全な画面遷移を実現 + ### Example: CenterTextFieldUseCase ```typescript import type { ITextField } from "@/interface/ITextField"; -import { config } from "@/config/Config"; /** * @description テキストフィールド中央揃えのユースケース @@ -131,18 +135,23 @@ export class CenterTextFieldUseCase * Center the text field on the screen * * @param {ITextField} textField + * @param {number} stageWidth - ステージの幅 / Stage width * @return {void} * @method * @public */ - execute (textField: ITextField): void + execute (textField: ITextField, stageWidth: number): void { // ビジネスロジック: 中央配置の計算 - textField.x = (config.stage.width - textField.width) / 2; + textField.x = (stageWidth - textField.width) / 2; } } ``` +**ポイント / Key Points:** +- `config` に直接依存せず、`stageWidth` を引数で受け取る +- テスタビリティが向上(任意の幅でテスト可能) + ## UseCaseの設計原則 / UseCase Design Principles ### 1. インターフェースに依存 / Depend on Interfaces diff --git a/template/src/ui/README.md b/template/src/ui/README.md index 932f8fe..8a92594 100644 --- a/template/src/ui/README.md +++ b/template/src/ui/README.md @@ -101,10 +101,9 @@ Button component for the Top screen. ```typescript export class TopBtnMolecule extends ButtonAtom { - constructor() { + constructor(text: string) { super(); - // レスポンスデータからテキストを取得 - const text = app.getResponse().get("TopText").word; + // ViewModelから渡されたテキストを表示 const textField = new TextAtom(text, { autoSize: "center" }); this.addChild(textField); } @@ -116,7 +115,7 @@ export class TopBtnMolecule extends ButtonAtom { ``` **特徴 / Features:** -- テキストとアニメーションを含む +- テキストはViewModelから引数で受け取る(データ取得はViewModelの責務) - 入場アニメーション機能 ### 3. Content - Animation Tool生成コンテンツ diff --git a/template/src/ui/component/atom/TextAtom.ts b/template/src/ui/component/atom/TextAtom.ts index 737efb6..ec33c21 100644 --- a/template/src/ui/component/atom/TextAtom.ts +++ b/template/src/ui/component/atom/TextAtom.ts @@ -29,34 +29,34 @@ export class TextAtom extends TextField implements ITextField { super(); if (props) { - const keys: string[] = Object.keys(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)) { + if (!(name in this) || value === undefined) { continue; } - // @ts-ignore - this[name] = props[name]; + (this as unknown as Record)[name] = value; } } if (format_object) { - const keys: string[] = Object.keys(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; } - // @ts-ignore - textFormat[name] = format_object[name]; + (textFormat as unknown as Record)[name] = value; } this.defaultTextFormat = textFormat; From ccc1a8d8295ac005af857a96df4295e0b4144def Mon Sep 17 00:00:00 2001 From: ienaga Date: Wed, 3 Dec 2025 21:36:53 +0900 Subject: [PATCH 12/35] =?UTF-8?q?#56=20AI=E5=AE=9F=E8=A3=85=E3=81=AB?= =?UTF-8?q?=E5=BF=85=E8=A6=81=E3=81=AAmarkdown=E3=83=95=E3=82=A1=E3=82=A4?= =?UTF-8?q?=E3=83=AB=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/@types/README.md | 50 ++++ template/README.md | 6 +- template/electron/README.md | 79 +++++++ template/mock/api/README.md | 119 ++++++++++ template/mock/content/README.md | 64 +++++ template/mock/img/README.md | 78 +++++++ template/src/ui/animation/README.md | 188 +++++++++++++++ template/src/ui/component/README.md | 109 +++++++++ template/src/ui/component/atom/README.md | 179 ++++++++++++++ template/src/ui/component/molecule/README.md | 231 +++++++++++++++++++ template/src/ui/content/README.md | 211 +++++++++++++++++ 11 files changed, 1311 insertions(+), 3 deletions(-) create mode 100644 template/@types/README.md create mode 100644 template/electron/README.md create mode 100644 template/mock/api/README.md create mode 100644 template/mock/content/README.md create mode 100644 template/mock/img/README.md create mode 100644 template/src/ui/animation/README.md create mode 100644 template/src/ui/component/README.md create mode 100644 template/src/ui/component/atom/README.md create mode 100644 template/src/ui/component/molecule/README.md create mode 100644 template/src/ui/content/README.md 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/README.md b/template/README.md index fc9e88f..a80274e 100644 --- a/template/README.md +++ b/template/README.md @@ -25,8 +25,8 @@ This project was bootstrapped with [Create Next2D App](https://github.com/Next2D | ツール / Tool | バージョン / Version | |--------------|---------------------| -| Node.js | 18.x 以上 / 18.x or higher | -| npm | 9.x 以上 / 9.x or higher | +| Node.js | 22.x 以上 / 22.x or higher | +| npm | 10.x 以上 / 10.x or higher | ### オプション / Optional @@ -35,7 +35,7 @@ iOS/Androidビルドを行う場合は、以下も必要です。 For iOS/Android builds, the following are also required: - **iOS**: Xcode 14 以上、macOS -- **Android**: Android Studio、JDK 17 以上 +- **Android**: Android Studio、JDK 21 以上 --- diff --git a/template/electron/README.md b/template/electron/README.md new file mode 100644 index 0000000..cd2ea0f --- /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/ # アプリケーションアイコン / Application icons +├── index.js # Electronメインプロセス / Electron main process +└── package.json # Electron依存関係 / Electron dependencies +``` + +## ファイル説明 / 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..cf2ef00 --- /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, Next2D!" +} +``` + +**アクセスURL:** `http://localhost:5173/api/home.json` + +### top.json + +Top画面用のモックAPIレスポンスです。 + +Mock API response for the Top screen. + +```json +{ + "title": "Welcome" +} +``` + +**アクセス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..14dff2d --- /dev/null +++ b/template/mock/content/README.md @@ -0,0 +1,64 @@ +# Mock Content + +ローカル開発用のモックコンテンツデータを格納するディレクトリです。 + +Directory for storing mock content data for local development. + +## 概要 / Overview + +Animation Toolからエクスポートされたコンテンツのモックデータを配置します。`http://localhost:5173/content/` でアクセス可能です。 + +Place mock data for content exported from the Animation Tool. Accessible at `http://localhost:5173/content/`. + +## ファイル一覧 / File List + +### sample.json + +サンプルのコンテンツデータです。 + +Sample content data. + +**アクセスURL:** `http://localhost:5173/content/sample.json` + +## 使用方法 / Usage + +### 1. コンテンツデータの配置 + +Animation Toolからエクスポートしたデータ、または開発用のダミーデータを配置します。 + +Place data exported from the Animation Tool or dummy data for development. + +### 2. アクセス + +開発サーバー経由でコンテンツにアクセスします。 + +Access content through the development server. + +```typescript +const response = await fetch("http://localhost:5173/content/sample.json"); +const data = await response.json(); +``` + +## モックコンテンツの追加 / Adding Mock Content + +### 手順 / Steps + +1. コンテンツデータ(JSON形式)を作成 +2. このディレクトリに配置 +3. 開発サーバーからアクセスをテスト + +## 注意事項 / Notes + +- モックコンテンツは開発環境でのみ使用してください +- 本番環境では実際のコンテンツサーバーを設定してください +- `routing.json` のパス設定と重複しないように注意してください + +- Use mock content only in development environments +- Set actual content server in production environments +- Be careful not to conflict with path settings in `routing.json` + +## 関連ドキュメント / Related Documentation + +- [../README.md](../README.md) - Mockディレクトリの説明 +- [../../file/README.md](../../file/README.md) - n2dファイルの格納場所 +- [../../src/ui/content/README.md](../../src/ui/content/README.md) - Animation Toolコンテンツ 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/src/ui/animation/README.md b/template/src/ui/animation/README.md new file mode 100644 index 0000000..b71ba01 --- /dev/null +++ b/template/src/ui/animation/README.md @@ -0,0 +1,188 @@ +# Animation Definitions + +コンポーネントのアニメーションロジックを格納するディレクトリです。 + +Directory for storing animation logic for components. + +## 概要 / Overview + +アニメーション定義をコンポーネントから分離することで、コードの再利用性と保守性を向上させます。 + +Separating animation definitions from components improves code reusability and maintainability. + +## ディレクトリ構造 / Directory Structure + +``` +animation/ +└── top/ + └── TopBtnEntranceAnimation.ts +``` + +画面ごとにサブディレクトリを作成し、その中にアニメーション定義ファイルを配置します。 + +Create subdirectories for each screen and place animation definition files within them. + +## アニメーションの種類 / Animation Types + +### 入場アニメーション / Entrance Animation + +画面表示時のアニメーションです。 + +Animation when the screen is displayed. + +### 退場アニメーション / Exit Animation + +画面遷移時のアニメーションです。 + +Animation during screen transitions. + +### インタラクションアニメーション / Interaction Animation + +ユーザー操作に対するアニメーションです。 + +Animation in response to user actions. + +## 実装例 / Implementation Example + +### TopBtnEntranceAnimation.ts + +```typescript +import type { TopBtnMolecule } from "@/ui/component/molecule/TopBtnMolecule"; +import { Tween } from "@next2d/framework"; + +/** + * @description Topボタンの入場アニメーション + * Entrance animation for Top button + * + * @param {TopBtnMolecule} target + * @param {() => void} callback + * @return {void} + */ +export const playEntrance = ( + target: TopBtnMolecule, + callback: () => void +): void => { + // 初期状態 + target.alpha = 0; + target.scaleX = 0.5; + target.scaleY = 0.5; + + // アニメーション + Tween.to(target, { + alpha: 1, + scaleX: 1, + scaleY: 1, + duration: 0.5, + ease: "easeOutBack", + onComplete: callback + }); +}; +``` + +## 設計原則 / Design Principles + +### 1. コンポーネントとの分離 / Separation from Components + +アニメーションロジックをコンポーネントから分離します。 + +Separate animation logic from components. + +```typescript +// ✅ 良い例: アニメーションを別ファイルに分離 +// animation/top/TopBtnEntranceAnimation.ts +export const playEntrance = (target, callback) => { ... }; + +// component/molecule/TopBtnMolecule.ts +import { playEntrance } from "@/ui/animation/top/TopBtnEntranceAnimation"; + +export class TopBtnMolecule extends ButtonAtom { + playEntrance(callback: () => void): void { + playEntrance(this, callback); + } +} +``` + +### 2. 再利用性 / Reusability + +同じアニメーションを複数のコンポーネントで使用できるようにします。 + +Make the same animation usable across multiple components. + +```typescript +// ✅ 良い例: 汎用的なフェードインアニメーション +export const fadeIn = (target: DisplayObject, duration: number = 0.3): void => { + target.alpha = 0; + Tween.to(target, { alpha: 1, duration }); +}; +``` + +### 3. コールバック対応 / Callback Support + +アニメーション完了時のコールバックをサポートします。 + +Support callbacks for when animation completes. + +```typescript +export const playEntrance = ( + target: DisplayObject, + callback?: () => void +): void => { + Tween.to(target, { + // ... + onComplete: callback + }); +}; +``` + +## 新しいアニメーションの追加 / Adding New Animations + +### 手順 / Steps + +1. 対象画面のディレクトリを確認(なければ作成) +2. アニメーション関数を作成 +3. コンポーネントから呼び出し +4. JSDocコメントを追加 + +### テンプレート / Template + +```typescript +import type { DisplayObject } from "@next2d/display"; +import { Tween } from "@next2d/framework"; + +/** + * @description [アニメーションの説明] + * [Animation description] + * + * @param {DisplayObject} target - アニメーション対象 + * @param {() => void} callback - 完了時コールバック + * @return {void} + */ +export const yourAnimation = ( + target: DisplayObject, + callback?: () => void +): void => { + // 初期状態設定 + target.alpha = 0; + + // アニメーション実行 + Tween.to(target, { + alpha: 1, + duration: 0.5, + ease: "easeOutQuad", + onComplete: callback + }); +}; +``` + +## ベストプラクティス / Best Practices + +1. **分離** - アニメーションロジックをコンポーネントから分離 +2. **命名** - `{Component}{Action}Animation.ts` の形式で命名 +3. **コールバック** - 完了時の処理をサポート +4. **再利用** - 汎用的なアニメーションは共通化 + +## 関連ドキュメント / 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/component/README.md b/template/src/ui/component/README.md new file mode 100644 index 0000000..0dc1ff6 --- /dev/null +++ b/template/src/ui/component/README.md @@ -0,0 +1,109 @@ +# UI Components + +アトミックデザインに基づいたUIコンポーネントを格納するディレクトリです。 + +Directory for UI components based on Atomic Design principles. + +## ディレクトリ構造 / Directory Structure + +``` +component/ +├── atom/ # 最小単位のコンポーネント / Smallest components +│ ├── ButtonAtom.ts +│ └── TextAtom.ts +└── molecule/ # Atomを組み合わせたコンポーネント / Composite components + ├── HomeBtnMolecule.ts + └── TopBtnMolecule.ts +``` + +## アトミックデザイン階層 / 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画面用のボタン(ドラッグ機能付き) +- `TopBtnMolecule` - 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 +// ✅ 良い例 +export class HomeBtnMolecule implements IDraggable { + startDrag(): void { ... } + stopDrag(): void { ... } +} +``` + +### 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/README.md b/template/src/ui/component/atom/README.md new file mode 100644 index 0000000..c66fc1b --- /dev/null +++ b/template/src/ui/component/atom/README.md @@ -0,0 +1,179 @@ +# Atom Components + +最小単位のUIコンポーネントを格納するディレクトリです。アトミックデザインにおける「原子」に相当します。 + +Directory for the smallest UI components. Corresponds to "Atoms" in Atomic Design. + +## 概要 / Overview + +Atomは、これ以上分割できない最も基本的なUI要素です。ボタン、テキストフィールド、アイコンなどの基本パーツがここに含まれます。 + +Atoms are the most basic UI elements that cannot be divided further. Basic parts such as buttons, text fields, and icons are included here. + +## コンポーネント一覧 / Component List + +### ButtonAtom.ts + +ボタンの基本機能を提供するコンポーネントです。 + +Component that provides basic button functionality. + +```typescript +import { Sprite } from "@next2d/display"; + +export class ButtonAtom extends Sprite { + constructor() { + super(); + this.buttonMode = true; // マウスカーソルがポインターに変更 + } +} +``` + +**特徴 / Features:** +- マウスカーソルがポインター型に変更される +- ボタンとしての基本的な振る舞いを提供 +- Moleculeコンポーネントの基底クラスとして使用 + +### TextAtom.ts + +テキスト表示の基本機能を提供するコンポーネントです。 + +Component that provides basic text display functionality. + +```typescript +import { TextField } from "@next2d/display"; +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(); + // プロパティ設定とフォーマット適用 + } +} +``` + +**特徴 / Features:** +- 柔軟なテキストフォーマット設定 +- プロパティの動的設定が可能 +- `ITextField` インターフェースを実装 + +## 設計原則 / Design Principles + +### 1. 最小限の機能 / Minimal Functionality + +Atomは1つの明確な役割のみを持ちます。 + +Atoms have only one clear role. + +```typescript +// ✅ 良い例: ボタンの基本機能のみ +export class ButtonAtom extends Sprite { + constructor() { + super(); + this.buttonMode = true; + } +} + +// ❌ 悪い例: 複数の責務 +export class ButtonAtom extends Sprite { + async fetchData() { ... } // NG: データ取得は別層の責務 + navigate() { ... } // NG: ナビゲーションは別層の責務 +} +``` + +### 2. 汎用性 / Genericity + +特定の画面に依存せず、汎用的に使用できるように設計します。 + +Design to be used generically without depending on specific screens. + +```typescript +// ✅ 良い例: 汎用的なテキストコンポーネント +export class TextAtom extends TextField { + constructor(text: string, props: any = null) { + super(); + this.text = text; + } +} + +// ❌ 悪い例: 特定画面に依存 +export class HomeTextAtom extends TextField { // NG: 画面固有 + constructor() { + super(); + this.text = "Home Screen"; // NG: 固定値 + } +} +``` + +### 3. インターフェース実装 / Interface Implementation + +必要に応じてインターフェースを実装し、型安全性を確保します。 + +Implement interfaces as needed to ensure type safety. + +```typescript +import type { ITextField } from "@/interface/ITextField"; + +export class TextAtom extends TextField implements ITextField { + width: number; + x: number; +} +``` + +## 新しいAtomの追加方法 / Adding New Atoms + +### 手順 / Steps + +1. 基本クラスを継承(`Sprite`, `TextField`, `Shape` など) +2. コンストラクタで基本設定を行う +3. 必要に応じてインターフェースを実装 +4. JSDocコメントを追加 + +### テンプレート / Template + +```typescript +import { Sprite } from "@next2d/display"; + +/** + * @description [コンポーネントの説明] + * [Component description] + * + * @class + * @extends {Sprite} + */ +export class YourAtom extends Sprite +{ + /** + * @description コンストラクタ + * Constructor + * + * @constructor + * @public + */ + constructor () + { + super(); + + // 初期設定 + } +} +``` + +## ベストプラクティス / Best Practices + +1. **単一責任** - 1つのAtomは1つの責務のみ +2. **汎用性** - 特定の画面やデータに依存しない +3. **再利用性** - 異なるMoleculeから使用可能 +4. **インターフェース** - 型安全性のため必要に応じて実装 + +## 関連ドキュメント / Related Documentation + +- [../molecule/README.md](../molecule/README.md) - Moleculeコンポーネント +- [../README.md](../README.md) - コンポーネント全体の説明 +- [../../README.md](../../README.md) - UI全体の説明 +- [../../../interface/README.md](../../../interface/README.md) - インターフェース定義 diff --git a/template/src/ui/component/molecule/README.md b/template/src/ui/component/molecule/README.md new file mode 100644 index 0000000..a49496a --- /dev/null +++ b/template/src/ui/component/molecule/README.md @@ -0,0 +1,231 @@ +# Molecule Components + +複数のAtomを組み合わせた複合コンポーネントを格納するディレクトリです。アトミックデザインにおける「分子」に相当します。 + +Directory for composite components combining multiple Atoms. Corresponds to "Molecules" in Atomic Design. + +## 概要 / Overview + +Moleculeは、複数のAtomを組み合わせて、より具体的な機能を持つコンポーネントです。画面固有のボタンやフォームなどがここに含まれます。 + +Molecules are components with more specific functionality by combining multiple Atoms. Screen-specific buttons, forms, etc. are included here. + +## コンポーネント一覧 / Component List + +### HomeBtnMolecule.ts + +Home画面用のボタンコンポーネントです。ドラッグ&ドロップ機能を提供します。 + +Button component for the Home screen. Provides drag and drop functionality. + +```typescript +import { ButtonAtom } from "../atom/ButtonAtom"; +import type { IDraggable } from "@/interface/IDraggable"; +import { HomeContent } from "@/ui/content/HomeContent"; + +export class HomeBtnMolecule extends ButtonAtom implements IDraggable { + private readonly homeContent: HomeContent; + + constructor() { + super(); + this.homeContent = new HomeContent(); + this.addChild(this.homeContent); + } + + startDrag(): void { + // ドラッグ開始処理 + } + + stopDrag(): void { + // ドラッグ停止処理 + } +} +``` + +**特徴 / Features:** +- `ButtonAtom` を継承 +- `IDraggable` インターフェースを実装 +- `HomeContent` (Animation Toolコンテンツ) を含む +- ドラッグ&ドロップ機能を提供 + +### TopBtnMolecule.ts + +Top画面用のボタンコンポーネントです。入場アニメーション機能を提供します。 + +Button component for the Top screen. Provides entrance animation functionality. + +```typescript +import { ButtonAtom } from "../atom/ButtonAtom"; +import { TextAtom } from "../atom/TextAtom"; + +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:** +- `ButtonAtom` を継承 +- テキストはViewModelから引数で受け取る(データ取得はViewModelの責務) +- 入場アニメーション機能を提供 +- `TextAtom` を子要素として持つ + +## 設計原則 / Design Principles + +### 1. Atomの組み合わせ / Combining Atoms + +MoleculeはAtomを組み合わせて構成します。 + +Molecules are composed by combining Atoms. + +```typescript +// ✅ 良い例: Atomを組み合わせる +export class TopBtnMolecule extends ButtonAtom { + constructor(text: string) { + super(); + const textField = new TextAtom(text); // TextAtomを使用 + this.addChild(textField); + } +} +``` + +### 2. 画面固有の機能 / Screen-specific Functionality + +Moleculeは特定の画面の要件に合わせた機能を実装します。 + +Molecules implement functionality tailored to specific screen requirements. + +```typescript +// ✅ 良い例: Home画面用のドラッグ機能 +export class HomeBtnMolecule extends ButtonAtom implements IDraggable { + startDrag(): void { ... } + stopDrag(): void { ... } +} + +// ✅ 良い例: Top画面用のアニメーション +export class TopBtnMolecule extends ButtonAtom { + playEntrance(callback: () => void): void { ... } +} +``` + +### 3. データの受け取り / Receiving Data + +Moleculeはデータを自ら取得せず、ViewModelから引数として受け取ります。 + +Molecules don't fetch data themselves; they receive it as arguments from ViewModel. + +```typescript +// ✅ 良い例: 引数でデータを受け取る +export class TopBtnMolecule extends ButtonAtom { + constructor(text: string) { // ViewModelから受け取る + super(); + this.textField = new TextAtom(text); + } +} + +// ❌ 悪い例: 直接データ取得 +export class TopBtnMolecule extends ButtonAtom { + async constructor() { + const data = await Repository.get(); // NG + } +} +``` + +### 4. インターフェース実装 / Interface Implementation + +ビジネスロジック層との連携のためにインターフェースを実装します。 + +Implement interfaces for integration with the business logic layer. + +```typescript +import type { IDraggable } from "@/interface/IDraggable"; + +export class HomeBtnMolecule extends ButtonAtom implements IDraggable { + startDrag(): void { ... } + stopDrag(): void { ... } +} +``` + +## 新しいMoleculeの追加方法 / Adding New Molecules + +### 手順 / Steps + +1. 適切なAtomを継承(通常は `ButtonAtom`) +2. 必要なAtomを子要素として追加 +3. 画面固有の機能を実装 +4. 必要に応じてインターフェースを実装 +5. JSDocコメントを追加 + +### テンプレート / Template + +```typescript +import { ButtonAtom } from "../atom/ButtonAtom"; +import { TextAtom } from "../atom/TextAtom"; +import type { IYourInterface } from "@/interface/IYourInterface"; + +/** + * @description [コンポーネントの説明] + * [Component description] + * + * @class + * @extends {ButtonAtom} + * @implements {IYourInterface} + */ +export class YourMolecule extends ButtonAtom implements IYourInterface +{ + /** + * @description コンストラクタ + * Constructor + * + * @param {string} text - 表示テキスト / Display text + * @constructor + * @public + */ + constructor (text: string) + { + super(); + + // Atomを追加 + const textField = new TextAtom(text); + this.addChild(textField); + } + + /** + * @description [メソッドの説明] + * [Method description] + * + * @return {void} + * @method + * @public + */ + yourMethod (): void + { + // 実装 + } +} +``` + +## ベストプラクティス / Best Practices + +1. **Atom優先** - 可能な限りAtomを再利用 +2. **データ注入** - コンストラクタでデータを受け取る +3. **インターフェース** - ビジネスロジックとの連携に使用 +4. **単一画面** - 1つのMoleculeは通常1つの画面で使用 +5. **命名規則** - `{Screen}Btn/Form/ListMolecule` など明確に + +## 関連ドキュメント / Related Documentation + +- [../atom/README.md](../atom/README.md) - Atomコンポーネント +- [../README.md](../README.md) - コンポーネント全体の説明 +- [../../animation/README.md](../../animation/README.md) - アニメーション定義 +- [../../content/README.md](../../content/README.md) - Animation Toolコンテンツ +- [../../../interface/README.md](../../../interface/README.md) - インターフェース定義 diff --git a/template/src/ui/content/README.md b/template/src/ui/content/README.md new file mode 100644 index 0000000..e2fcb81 --- /dev/null +++ b/template/src/ui/content/README.md @@ -0,0 +1,211 @@ +# 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で設定したシンボル名 + } + + /** + * @description ドラッグを開始 + * Start dragging + */ + startDrag (): void + { + // ドラッグ処理 + } + + /** + * @description ドラッグを停止 + * Stop dragging + */ + stopDrag (): void + { + // ドラッグ停止処理 + } +} +``` + +### 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 { + startDrag(): void { ... } + stopDrag(): void { ... } +} +``` + +## 新しい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の使い方 From ce8277a9de9eff221c4e4c06a08c90d68036d30e Mon Sep 17 00:00:00 2001 From: ienaga Date: Wed, 3 Dec 2025 21:44:32 +0900 Subject: [PATCH 13/35] =?UTF-8?q?#56=20AI=E3=81=8C=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=E3=81=97=E3=82=84=E3=81=99=E3=81=84=E3=82=88=E3=81=86markdown?= =?UTF-8?q?=E3=81=ABmermaid=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/README.md | 83 ++++++++------ template/electron/README.md | 12 +- template/src/model/README.md | 117 ++++++++++++-------- template/src/model/application/README.md | 20 ++-- template/src/model/domain/README.md | 49 ++++---- template/src/model/infrastructure/README.md | 41 ++++--- template/src/ui/README.md | 17 +-- template/src/ui/animation/README.md | 11 +- template/src/ui/component/README.md | 20 ++-- template/src/ui/content/README.md | 10 +- template/src/view/README.md | 24 ++-- 11 files changed, 238 insertions(+), 166 deletions(-) diff --git a/template/README.md b/template/README.md index a80274e..a3a2bdf 100644 --- a/template/README.md +++ b/template/README.md @@ -72,25 +72,35 @@ Open [http://localhost:5173](http://localhost:5173) in your browser. This project adopts **MVVM + Clean Architecture + Atomic Design**. -``` -┌─────────────────────────────────────────────────────────┐ -│ 🎨 View Layer (view/, ui/) │ -│ - View: 画面の構造定義 / Screen structure │ -│ - ViewModel: ビジネスロジックとの橋渡し / Bridge │ -│ - UI Components: 再利用可能なUIパーツ / Reusable UI │ -├─────────────────────────────────────────────────────────┤ -│ 📋 Interface Layer (interface/) │ -│ - 型定義とインターフェース / Type definitions │ -├─────────────────────────────────────────────────────────┤ -│ ⚙️ Application Layer (model/application/) │ -│ - UseCase: ビジネスロジック実装 / Business logic │ -├─────────────────────────────────────────────────────────┤ -│ 💎 Domain Layer (model/domain/) │ -│ - コアビジネスルール / Core business rules │ -├─────────────────────────────────────────────────────────┤ -│ 🔧 Infrastructure Layer (model/infrastructure/) │ -│ - Repository: データアクセス / Data access │ -└─────────────────────────────────────────────────────────┘ +```mermaid +block-beta + columns 1 + block:view["🎨 View Layer (view/, ui/)"] + view_desc["View: 画面の構造定義 / Screen structure
ViewModel: ビジネスロジックとの橋渡し / Bridge
UI Components: 再利用可能なUIパーツ / Reusable UI"] + end + block:interface["📋 Interface Layer (interface/)"] + interface_desc["型定義とインターフェース / Type definitions"] + end + block:application["⚙️ Application Layer (model/application/)"] + application_desc["UseCase: ビジネスロジック実装 / Business logic"] + end + block:domain["💎 Domain Layer (model/domain/)"] + domain_desc["コアビジネスルール / Core business rules"] + end + block:infrastructure["🔧 Infrastructure Layer (model/infrastructure/)"] + infrastructure_desc["Repository: データアクセス / Data access"] + end + + view --> interface + interface --> application + application --> domain + application --> infrastructure + + style view fill:#e3f2fd + style interface fill:#fff9c4 + style application fill:#f3e5f5 + style domain fill:#e8f5e9 + style infrastructure fill:#fce4ec ``` 詳細は [ARCHITECTURE.md](./ARCHITECTURE.md) を参照してください。 @@ -191,21 +201,26 @@ 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/ -│ ├── component/ -│ │ ├── atom/ # 最小コンポーネント / Smallest components -│ │ └── molecule/# 複合コンポーネント / Composite components -│ ├── content/ # Animation Tool コンテンツ / Animation Tool content -│ └── animation/ # アニメーション定義 / Animation definitions -└── view/ # View & ViewModel +```mermaid +graph LR + subgraph src["src/"] + config["config/
設定ファイル"] + interface["interface/
インターフェース定義"] + subgraph model["model/"] + application["application/
ユースケース"] + domain["domain/
ドメインロジック"] + infrastructure["infrastructure/
リポジトリ"] + end + subgraph ui["ui/"] + subgraph component["component/"] + atom["atom/
最小コンポーネント"] + molecule["molecule/
複合コンポーネント"] + end + content["content/
Animation Tool"] + animation["animation/
アニメーション定義"] + end + view["view/
View & ViewModel"] + end ``` 各ディレクトリの詳細は、ディレクトリ内の `README.md` を参照してください。 diff --git a/template/electron/README.md b/template/electron/README.md index cd2ea0f..10d1cb3 100644 --- a/template/electron/README.md +++ b/template/electron/README.md @@ -6,11 +6,13 @@ Directory for Electron configuration used to build desktop applications for Wind ## ディレクトリ構造 / Directory Structure -``` -electron/ -├── icons/ # アプリケーションアイコン / Application icons -├── index.js # Electronメインプロセス / Electron main process -└── package.json # Electron依存関係 / Electron dependencies +```mermaid +graph LR + subgraph electron["electron/"] + icons["icons/
アプリケーションアイコン"] + index["index.js
メインプロセス"] + package["package.json
依存関係"] + end ``` ## ファイル説明 / File Description diff --git a/template/src/model/README.md b/template/src/model/README.md index c46a29d..767a839 100644 --- a/template/src/model/README.md +++ b/template/src/model/README.md @@ -6,29 +6,34 @@ This directory is responsible for business logic and data access. Based on Clean ## 📁 現在のディレクトリ構造 / Current Directory Structure -```sh -model/ -├── application/ # アプリケーション層 -│ ├── home/ -│ │ └── usecase/ # ビジネスロジック実装 -│ │ ├── StartDragUseCase.ts -│ │ ├── StopDragUseCase.ts -│ │ └── CenterTextFieldUseCase.ts -│ └── top/ -│ └── usecase/ -│ └── NavigateToViewUseCase.ts -│ -├── domain/ # ドメイン層 -│ └── callback/ -│ └── Background/ -│ ├── Background.ts -│ └── service/ -│ ├── BackgroundDrawService.ts -│ └── BackgroundChangeScaleService.ts -│ -└── infrastructure/ # インフラストラクチャ層 - └── repository/ - └── HomeTextRepository.ts +```mermaid +graph LR + subgraph model["model/"] + subgraph application["application/
アプリケーション層"] + subgraph home["home/"] + home_usecase["usecase/
StartDragUseCase.ts
StopDragUseCase.ts
CenterTextFieldUseCase.ts"] + end + subgraph top["top/"] + top_usecase["usecase/
NavigateToViewUseCase.ts"] + end + end + subgraph domain["domain/
ドメイン層"] + subgraph callback["callback/"] + subgraph Background["Background/"] + bg_ts["Background.ts"] + subgraph service["service/"] + draw["BackgroundDrawService.ts"] + scale["BackgroundChangeScaleService.ts"] + end + end + end + end + subgraph infrastructure["infrastructure/
インフラ層"] + subgraph repository["repository/"] + repo["HomeTextRepository.ts"] + end + end + end ``` ## 🎨 アーキテクチャ概要 / Architecture Overview @@ -77,16 +82,16 @@ Implements business logic corresponding to user actions. Creates a UseCase class ### ディレクトリ構造 / Directory Structure -```sh -application/ -├── home/ # Home画面のビジネスロジック -│ └── usecase/ -│ ├── StartDragUseCase.ts # ドラッグ開始 -│ ├── StopDragUseCase.ts # ドラッグ停止 -│ └── CenterTextFieldUseCase.ts # テキスト中央揃え -└── top/ # Top画面のビジネスロジック - └── usecase/ - └── NavigateToViewUseCase.ts # 画面遷移 +```mermaid +graph LR + subgraph application["application/"] + subgraph home["home/
Home画面"] + home_uc["usecase/
StartDragUseCase.ts
StopDragUseCase.ts
CenterTextFieldUseCase.ts"] + end + subgraph top["top/
Top画面"] + top_uc["usecase/
NavigateToViewUseCase.ts"] + end + end ``` ### 実装例 / Implementation Example @@ -146,14 +151,19 @@ Implements the core business rules of the application. Pure logic that doesn't d ### ディレクトリ構造 / Directory Structure -```sh -domain/ -└── callback/ - └── Background/ - ├── Background.ts # グラデーション背景クラス - └── service/ - ├── BackgroundDrawService.ts # 描画サービス - └── BackgroundChangeScaleService.ts # スケール変更サービス +```mermaid +graph LR + subgraph domain["domain/"] + subgraph callback["callback/"] + subgraph Background["Background/"] + bg["Background.ts
グラデーション背景"] + subgraph service["service/"] + draw["BackgroundDrawService.ts
描画サービス"] + scale["BackgroundChangeScaleService.ts
スケール変更"] + end + end + end + end ``` ### 実装例 / Implementation Example @@ -247,10 +257,13 @@ Integrates with external systems (APIs, databases, etc.). Implements data access ### ディレクトリ構造 / Directory Structure -```sh -infrastructure/ -└── repository/ - └── HomeTextRepository.ts # Home画面テキストデータアクセス +```mermaid +graph LR + subgraph infrastructure["infrastructure/"] + subgraph repository["repository/"] + repo["HomeTextRepository.ts
Home画面テキストデータ"] + end + end ``` ### 実装例 / Implementation Example @@ -328,10 +341,16 @@ sequenceDiagram ### 1. 依存関係の方向 / Dependency Direction -``` -View Layer → Application Layer → Domain Layer - ↓ - Infrastructure Layer +```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 ``` - **Application層** は **Domain層** と **Infrastructure層** に依存 diff --git a/template/src/model/application/README.md b/template/src/model/application/README.md index 6185855..4f18b05 100644 --- a/template/src/model/application/README.md +++ b/template/src/model/application/README.md @@ -19,16 +19,16 @@ The Application layer provides business logic corresponding to user actions. Thi ## ディレクトリ構造 / Directory Structure -``` -application/ -├── home/ -│ └── usecase/ -│ ├── StartDragUseCase.ts -│ ├── StopDragUseCase.ts -│ └── CenterTextFieldUseCase.ts -└── top/ - └── usecase/ - └── NavigateToViewUseCase.ts +```mermaid +graph LR + subgraph application["application/"] + subgraph home["home/"] + home_usecase["usecase/
StartDragUseCase.ts
StopDragUseCase.ts
CenterTextFieldUseCase.ts"] + end + subgraph top["top/"] + top_usecase["usecase/
NavigateToViewUseCase.ts"] + end + end ``` 各画面ごとにディレクトリを作成し、その中に `usecase` ディレクトリを配置します。 diff --git a/template/src/model/domain/README.md b/template/src/model/domain/README.md index 46d2f9a..da98026 100644 --- a/template/src/model/domain/README.md +++ b/template/src/model/domain/README.md @@ -20,31 +20,42 @@ The Domain layer holds the core business rules of the application. This layer ha ## ディレクトリ構造 / Directory Structure -``` -domain/ -└── callback/ - └── Background/ - ├── Background.ts - └── service/ - ├── BackgroundDrawService.ts - └── BackgroundChangeScaleService.ts +```mermaid +graph LR + subgraph domain["domain/"] + subgraph callback["callback/"] + subgraph Background["Background/"] + bg["Background.ts"] + subgraph service["service/"] + draw["BackgroundDrawService.ts"] + scale["BackgroundChangeScaleService.ts"] + end + end + end + end ``` 将来的に以下のような拡張も可能です: Future extensions are possible, such as: -``` -domain/ -├── callback/ # コールバック処理 -│ └── Background/ -├── service/ # ドメインサービス -│ ├── ValidationService.ts -│ └── CalculationService.ts -├── entity/ # エンティティ -│ └── User.ts -└── value-object/ # 値オブジェクト - └── Email.ts +```mermaid +graph LR + subgraph domain["domain/"] + subgraph callback["callback/
コールバック処理"] + bg["Background/"] + end + subgraph service["service/
ドメインサービス"] + validation["ValidationService.ts"] + calculation["CalculationService.ts"] + end + subgraph entity["entity/
エンティティ"] + user["User.ts"] + end + subgraph value["value-object/
値オブジェクト"] + email["Email.ts"] + end + end ``` ## ドメインの概念 / Domain Concepts diff --git a/template/src/model/infrastructure/README.md b/template/src/model/infrastructure/README.md index 9cd1ffa..7eb0da3 100644 --- a/template/src/model/infrastructure/README.md +++ b/template/src/model/infrastructure/README.md @@ -19,28 +19,37 @@ The Infrastructure layer is responsible for interactions with the outside of the ## ディレクトリ構造 / Directory Structure -``` -infrastructure/ -└── repository/ - └── HomeTextRepository.ts +```mermaid +graph LR + subgraph infrastructure["infrastructure/"] + subgraph repository["repository/"] + home["HomeTextRepository.ts"] + end + end ``` 将来的に以下のような拡張も可能です: Future extensions are possible, such as: -``` -infrastructure/ -├── repository/ # データアクセス層 -│ ├── HomeTextRepository.ts -│ ├── UserRepository.ts -│ └── ConfigRepository.ts -├── entity/ # データベースエンティティ -│ └── UserEntity.ts -├── dto/ # データ転送オブジェクト -│ └── ApiResponseDto.ts -└── external/ # 外部サービス連携 - └── AnalyticsService.ts +```mermaid +graph LR + subgraph infrastructure["infrastructure/"] + subgraph repository["repository/
データアクセス層"] + home["HomeTextRepository.ts"] + user["UserRepository.ts"] + config["ConfigRepository.ts"] + end + subgraph entity["entity/
DBエンティティ"] + userEntity["UserEntity.ts"] + end + subgraph dto["dto/
データ転送"] + apiDto["ApiResponseDto.ts"] + end + subgraph external["external/
外部サービス"] + analytics["AnalyticsService.ts"] + end + end ``` ## Repository Pattern diff --git a/template/src/ui/README.md b/template/src/ui/README.md index 8a92594..8878436 100644 --- a/template/src/ui/README.md +++ b/template/src/ui/README.md @@ -6,13 +6,16 @@ Directory for storing UI components, structured based on Atomic Design principle ## ディレクトリ構造 / Directory Structure -``` -ui/ -├── animation/ # アニメーション定義 -├── component/ # 再利用可能なコンポーネント -│ ├── atom/ # 最小単位のコンポーネント -│ └── molecule/ # Atomを組み合わせたコンポーネント -└── content/ # Animation Tool生成コンテンツ +```mermaid +graph LR + subgraph ui["ui/"] + animation["animation/
アニメーション定義"] + subgraph component["component/"] + atom["atom/
最小単位"] + molecule["molecule/
複合コンポーネント"] + end + content["content/
Animation Tool"] + end ``` ## アトミックデザインの階層 / Atomic Design Hierarchy diff --git a/template/src/ui/animation/README.md b/template/src/ui/animation/README.md index b71ba01..1b262ea 100644 --- a/template/src/ui/animation/README.md +++ b/template/src/ui/animation/README.md @@ -12,10 +12,13 @@ Separating animation definitions from components improves code reusability and m ## ディレクトリ構造 / Directory Structure -``` -animation/ -└── top/ - └── TopBtnEntranceAnimation.ts +```mermaid +graph LR + subgraph animation["animation/"] + subgraph top["top/"] + entrance["TopBtnEntranceAnimation.ts"] + end + end ``` 画面ごとにサブディレクトリを作成し、その中にアニメーション定義ファイルを配置します。 diff --git a/template/src/ui/component/README.md b/template/src/ui/component/README.md index 0dc1ff6..5165e7c 100644 --- a/template/src/ui/component/README.md +++ b/template/src/ui/component/README.md @@ -6,14 +6,18 @@ Directory for UI components based on Atomic Design principles. ## ディレクトリ構造 / Directory Structure -``` -component/ -├── atom/ # 最小単位のコンポーネント / Smallest components -│ ├── ButtonAtom.ts -│ └── TextAtom.ts -└── molecule/ # Atomを組み合わせたコンポーネント / Composite components - ├── HomeBtnMolecule.ts - └── TopBtnMolecule.ts +```mermaid +graph LR + subgraph component["component/"] + subgraph atom["atom/
最小単位"] + button["ButtonAtom.ts"] + text["TextAtom.ts"] + end + subgraph molecule["molecule/
複合コンポーネント"] + homeBtn["HomeBtnMolecule.ts"] + topBtn["TopBtnMolecule.ts"] + end + end ``` ## アトミックデザイン階層 / Atomic Design Hierarchy diff --git a/template/src/ui/content/README.md b/template/src/ui/content/README.md index e2fcb81..5aea7fb 100644 --- a/template/src/ui/content/README.md +++ b/template/src/ui/content/README.md @@ -12,10 +12,12 @@ Wraps animations created with the Animation Tool as TypeScript classes for use i ## ディレクトリ構造 / Directory Structure -``` -content/ -├── HomeContent.ts # Home画面用コンテンツ -└── TopContent.ts # Top画面用コンテンツ +```mermaid +graph LR + subgraph content["content/"] + home["HomeContent.ts
Home画面用"] + top["TopContent.ts
Top画面用"] + end ``` ## コンテンツの仕組み / How Content Works diff --git a/template/src/view/README.md b/template/src/view/README.md index ed0fb4b..ec26560 100644 --- a/template/src/view/README.md +++ b/template/src/view/README.md @@ -81,16 +81,20 @@ sequenceDiagram ## Example of directory structure -```sh -project -└── src - └── view - ├── top - │ ├── TopView.ts - │ └── TopViewModel.ts - └── home - ├── HomeView.ts - └── HomeViewModel.ts +```mermaid +graph LR + subgraph src["src/"] + subgraph view["view/"] + subgraph top["top/"] + topView["TopView.ts"] + topVM["TopViewModel.ts"] + end + subgraph home["home/"] + homeView["HomeView.ts"] + homeVM["HomeViewModel.ts"] + end + end + end ``` ## Generator From 9cd30a3fa0119d9d1b2906f26c809718a7ed1dd1 Mon Sep 17 00:00:00 2001 From: ienaga Date: Wed, 3 Dec 2025 22:09:55 +0900 Subject: [PATCH 14/35] =?UTF-8?q?#56=20=E3=83=A6=E3=83=8B=E3=83=83?= =?UTF-8?q?=E3=83=88=E3=83=86=E3=82=B9=E3=83=88=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../usecase/CenterTextFieldUseCase.test.ts | 108 +++++++++++ .../home/usecase/StartDragUseCase.test.ts | 70 +++++++ .../home/usecase/StopDragUseCase.test.ts | 70 +++++++ .../top/usecase/NavigateToViewUseCase.test.ts | 69 +++++++ .../model/domain/callback/Background.test.ts | 133 +++++++++++++ .../BackgroundChangeScaleService.test.ts | 114 ++++++++++++ .../service/BackgroundDrawService.test.ts | 94 ++++++++++ .../repository/HomeTextRepository.test.ts | 87 +++++++++ .../top/TopBtnEntranceAnimation.test.ts | 103 +++++++++++ .../src/ui/component/atom/ButtonAtom.test.ts | 61 ++++++ .../src/ui/component/atom/TextAtom.test.ts | 101 ++++++++++ .../molecule/HomeBtnMolecule.test.ts | 80 ++++++++ .../component/molecule/TopBtnMolecule.test.ts | 83 +++++++++ template/src/ui/content/HomeContent.test.ts | 82 ++++++++ template/src/ui/content/TopContent.test.ts | 55 ++++++ template/src/view/home/HomeView.test.ts | 132 +++++++++++++ template/src/view/home/HomeViewModel.test.ts | 175 ++++++++++++++++++ template/src/view/top/TopView.test.ts | 128 +++++++++++++ template/src/view/top/TopViewModel.test.ts | 102 ++++++++++ template/vite.config.ts | 1 - 20 files changed, 1847 insertions(+), 1 deletion(-) create mode 100644 template/src/model/application/home/usecase/CenterTextFieldUseCase.test.ts create mode 100644 template/src/model/application/home/usecase/StartDragUseCase.test.ts create mode 100644 template/src/model/application/home/usecase/StopDragUseCase.test.ts create mode 100644 template/src/model/application/top/usecase/NavigateToViewUseCase.test.ts create mode 100644 template/src/model/domain/callback/Background.test.ts create mode 100644 template/src/model/domain/callback/Background/service/BackgroundChangeScaleService.test.ts create mode 100644 template/src/model/domain/callback/Background/service/BackgroundDrawService.test.ts create mode 100644 template/src/model/infrastructure/repository/HomeTextRepository.test.ts create mode 100644 template/src/ui/animation/top/TopBtnEntranceAnimation.test.ts create mode 100644 template/src/ui/component/atom/ButtonAtom.test.ts create mode 100644 template/src/ui/component/atom/TextAtom.test.ts create mode 100644 template/src/ui/component/molecule/HomeBtnMolecule.test.ts create mode 100644 template/src/ui/component/molecule/TopBtnMolecule.test.ts create mode 100644 template/src/ui/content/HomeContent.test.ts create mode 100644 template/src/ui/content/TopContent.test.ts create mode 100644 template/src/view/home/HomeView.test.ts create mode 100644 template/src/view/home/HomeViewModel.test.ts create mode 100644 template/src/view/top/TopView.test.ts create mode 100644 template/src/view/top/TopViewModel.test.ts 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/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/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/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/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/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/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/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/ui/animation/top/TopBtnEntranceAnimation.test.ts b/template/src/ui/animation/top/TopBtnEntranceAnimation.test.ts new file mode 100644 index 0000000..6073555 --- /dev/null +++ b/template/src/ui/animation/top/TopBtnEntranceAnimation.test.ts @@ -0,0 +1,103 @@ +import { describe, it, expect, vi } from "vitest"; + +// 依存モジュールをモック +vi.mock("@next2d/ui", () => ({ + Tween: { + add: vi.fn().mockReturnValue({ + addEventListener: vi.fn(), + start: vi.fn() + }) + }, + Easing: { + inQuad: vi.fn() + } +})); + +vi.mock("@next2d/events", () => ({ + Event: { + COMPLETE: "complete" + } +})); + +import { TopBtnEntranceAnimation } from "./TopBtnEntranceAnimation"; +import { Tween } from "@next2d/ui"; + +/** + * @description TopBtnEntranceAnimation のテスト + * Tests for TopBtnEntranceAnimation + */ +describe("TopBtnEntranceAnimation", () => { + /** + * @description コンストラクタのテスト + * Test for constructor + */ + describe("Constructor / コンストラクタ", () => { + it("インスタンスが正常に生成されること", () => { + const mockSprite = { alpha: 1 }; + const callback = vi.fn(); + + const animation = new TopBtnEntranceAnimation(mockSprite as any, callback); + expect(animation).toBeInstanceOf(TopBtnEntranceAnimation); + }); + + it("sprite.alpha が 0 に設定されること", () => { + const mockSprite = { alpha: 1 }; + const callback = vi.fn(); + + new TopBtnEntranceAnimation(mockSprite as any, callback); + expect(mockSprite.alpha).toBe(0); + }); + + it("Tween.add が呼び出されること", () => { + const mockSprite = { alpha: 1 }; + const callback = vi.fn(); + + new TopBtnEntranceAnimation(mockSprite as any, callback); + expect(Tween.add).toHaveBeenCalled(); + }); + }); + + /** + * @description start メソッドのテスト + * Test for start method + */ + describe("start Method / start メソッド", () => { + it("start メソッドを持つこと", () => { + const mockSprite = { alpha: 1 }; + const callback = vi.fn(); + + const animation = new TopBtnEntranceAnimation(mockSprite as any, callback); + expect(typeof animation.start).toBe("function"); + }); + + it("start を呼び出せること", () => { + const mockSprite = { alpha: 1 }; + const callback = vi.fn(); + + const animation = new TopBtnEntranceAnimation(mockSprite as any, callback); + expect(() => animation.start()).not.toThrow(); + }); + }); + + /** + * @description アニメーション設定のテスト + * Test for animation settings + */ + describe("Animation Settings / アニメーション設定", () => { + it("alpha のアニメーションが設定されること", () => { + const mockSprite = { alpha: 1 }; + const callback = vi.fn(); + + new TopBtnEntranceAnimation(mockSprite as any, callback); + + expect(Tween.add).toHaveBeenCalledWith( + mockSprite, + expect.objectContaining({ alpha: 0 }), + expect.objectContaining({ alpha: 1 }), + expect.any(Number), + expect.any(Number), + expect.anything() + ); + }); + }); +}); diff --git a/template/src/ui/component/atom/ButtonAtom.test.ts b/template/src/ui/component/atom/ButtonAtom.test.ts new file mode 100644 index 0000000..72779b3 --- /dev/null +++ b/template/src/ui/component/atom/ButtonAtom.test.ts @@ -0,0 +1,61 @@ +import { describe, it, expect, vi } from "vitest"; + +// @next2d/display モジュールをモック +vi.mock("@next2d/display", () => ({ + Sprite: vi.fn().mockImplementation(function(this: any) { + this.buttonMode = false; + }) +})); + +import { ButtonAtom } from "./ButtonAtom"; + +/** + * @description ButtonAtom のテスト + * Tests for ButtonAtom + */ +describe("ButtonAtom", () => { + /** + * @description コンストラクタのテスト + * Test for constructor + */ + describe("Constructor / コンストラクタ", () => { + it("インスタンスが正常に生成されること", () => { + const button = new ButtonAtom(); + expect(button).toBeInstanceOf(ButtonAtom); + }); + + it("buttonMode が true に設定されること", () => { + const button = new ButtonAtom(); + expect(button.buttonMode).toBe(true); + }); + }); + + /** + * @description 継承関係のテスト + * Test for inheritance + */ + describe("Inheritance / 継承関係", () => { + it("Sprite を継承していること", () => { + const button = new ButtonAtom(); + // ButtonAtom は Sprite を継承している + expect(button).toBeInstanceOf(ButtonAtom); + }); + }); + + /** + * @description プロパティのテスト + * Test for properties + */ + describe("Properties / プロパティ", () => { + it("buttonMode プロパティが存在すること", () => { + const button = new ButtonAtom(); + expect("buttonMode" in button).toBe(true); + }); + + it("buttonMode を変更できること", () => { + const button = new ButtonAtom(); + button.buttonMode = false; + expect(button.buttonMode).toBe(false); + }); + }); +}); diff --git a/template/src/ui/component/atom/TextAtom.test.ts b/template/src/ui/component/atom/TextAtom.test.ts new file mode 100644 index 0000000..6acfbb8 --- /dev/null +++ b/template/src/ui/component/atom/TextAtom.test.ts @@ -0,0 +1,101 @@ +import { describe, it, expect, vi } from "vitest"; + +// @next2d/text モジュールをモック +vi.mock("@next2d/text", () => ({ + TextField: vi.fn().mockImplementation(function(this: any) { + this.text = ""; + this.width = 100; + this.x = 0; + this.defaultTextFormat = {}; + }) +})); + +import { TextAtom } from "./TextAtom"; + +/** + * @description TextAtom のテスト + * Tests for TextAtom + */ +describe("TextAtom", () => { + /** + * @description コンストラクタのテスト + * Test for constructor + */ + describe("Constructor / コンストラクタ", () => { + it("インスタンスが正常に生成されること", () => { + const textAtom = new TextAtom(); + expect(textAtom).toBeInstanceOf(TextAtom); + }); + + it("text 引数が設定されること", () => { + const textAtom = new TextAtom("Hello"); + expect(textAtom.text).toBe("Hello"); + }); + + it("デフォルトで空文字が設定されること", () => { + const textAtom = new TextAtom(); + expect(textAtom.text).toBe(""); + }); + }); + + /** + * @description props パラメータのテスト + * Test for props parameter + */ + describe("Props Parameter / props パラメータ", () => { + it("props が null でもエラーにならないこと", () => { + expect(() => new TextAtom("Test", null)).not.toThrow(); + }); + + it("props のプロパティが適用されること", () => { + const textAtom = new TextAtom("Test", { + autoSize: "center" + }); + // props が適用される(モックでは完全な検証は難しい) + expect(textAtom).toBeDefined(); + }); + }); + + /** + * @description format_object パラメータのテスト + * Test for format_object parameter + */ + describe("Format Object Parameter / format_object パラメータ", () => { + it("format_object が null でもエラーにならないこと", () => { + expect(() => new TextAtom("Test", null, null)).not.toThrow(); + }); + + it("format_object のプロパティが適用されること", () => { + const textAtom = new TextAtom("Test", null, { + align: "center", + bold: true, + color: 0x000000, + font: "Arial", + italic: false, + leading: 0, + leftMargin: 0, + letterSpacing: 0, + rightMargin: 0, + size: 16, + underline: false + }); + expect(textAtom).toBeDefined(); + }); + }); + + /** + * @description ITextField インターフェースのテスト + * Test for ITextField interface + */ + describe("ITextField Interface / ITextField インターフェース", () => { + it("width プロパティを持つこと", () => { + const textAtom = new TextAtom(); + expect("width" in textAtom).toBe(true); + }); + + it("x プロパティを持つこと", () => { + const textAtom = new TextAtom(); + expect("x" in textAtom).toBe(true); + }); + }); +}); diff --git a/template/src/ui/component/molecule/HomeBtnMolecule.test.ts b/template/src/ui/component/molecule/HomeBtnMolecule.test.ts new file mode 100644 index 0000000..4823ae0 --- /dev/null +++ b/template/src/ui/component/molecule/HomeBtnMolecule.test.ts @@ -0,0 +1,80 @@ +import { describe, it, expect, vi } from "vitest"; + +// 依存モジュールをモック +vi.mock("@/ui/content/HomeContent", () => ({ + HomeContent: vi.fn().mockImplementation(function(this: any) { + this.scaleX = 1; + this.scaleY = 1; + this.startDrag = vi.fn(); + this.stopDrag = vi.fn(); + }) +})); + +vi.mock("@next2d/display", () => ({ + Sprite: vi.fn().mockImplementation(function(this: any) { + this.buttonMode = false; + this.addChild = vi.fn(); + }) +})); + +import { HomeBtnMolecule } from "./HomeBtnMolecule"; + +/** + * @description HomeBtnMolecule のテスト + * Tests for HomeBtnMolecule + */ +describe("HomeBtnMolecule", () => { + /** + * @description コンストラクタのテスト + * Test for constructor + */ + describe("Constructor / コンストラクタ", () => { + it("インスタンスが正常に生成されること", () => { + const molecule = new HomeBtnMolecule(); + expect(molecule).toBeInstanceOf(HomeBtnMolecule); + }); + + it("buttonMode が true に設定されること", () => { + const molecule = new HomeBtnMolecule(); + expect(molecule.buttonMode).toBe(true); + }); + }); + + /** + * @description IDraggable インターフェースのテスト + * Test for IDraggable interface + */ + describe("IDraggable Interface / IDraggable インターフェース", () => { + it("startDrag メソッドを持つこと", () => { + const molecule = new HomeBtnMolecule(); + expect(typeof molecule.startDrag).toBe("function"); + }); + + it("stopDrag メソッドを持つこと", () => { + const molecule = new HomeBtnMolecule(); + expect(typeof molecule.stopDrag).toBe("function"); + }); + + it("startDrag を呼び出せること", () => { + const molecule = new HomeBtnMolecule(); + expect(() => molecule.startDrag()).not.toThrow(); + }); + + it("stopDrag を呼び出せること", () => { + const molecule = new HomeBtnMolecule(); + expect(() => molecule.stopDrag()).not.toThrow(); + }); + }); + + /** + * @description ButtonAtom 継承のテスト + * Test for ButtonAtom inheritance + */ + describe("ButtonAtom Inheritance / ButtonAtom 継承", () => { + it("ButtonAtom を継承していること", () => { + const molecule = new HomeBtnMolecule(); + // ButtonAtom の機能を持っていること + expect("buttonMode" in molecule).toBe(true); + }); + }); +}); diff --git a/template/src/ui/component/molecule/TopBtnMolecule.test.ts b/template/src/ui/component/molecule/TopBtnMolecule.test.ts new file mode 100644 index 0000000..f25dfd5 --- /dev/null +++ b/template/src/ui/component/molecule/TopBtnMolecule.test.ts @@ -0,0 +1,83 @@ +import { describe, it, expect, vi } from "vitest"; + +// 依存モジュールをモック +vi.mock("@/ui/animation/top/TopBtnEntranceAnimation", () => ({ + TopBtnEntranceAnimation: vi.fn().mockImplementation(function(this: any) { + this.start = vi.fn(); + }) +})); + +vi.mock("@/ui/component/atom/TextAtom", () => ({ + TextAtom: vi.fn().mockImplementation(function(this: any, text: string) { + this.text = text; + this.width = 100; + this.height = 20; + this.x = 0; + this.y = 0; + }) +})); + +vi.mock("@next2d/display", () => ({ + Sprite: vi.fn().mockImplementation(function(this: any) { + this.buttonMode = false; + this.addChild = vi.fn(); + }) +})); + +import { TopBtnMolecule } from "./TopBtnMolecule"; + +/** + * @description TopBtnMolecule のテスト + * Tests for TopBtnMolecule + */ +describe("TopBtnMolecule", () => { + /** + * @description コンストラクタのテスト + * Test for constructor + */ + describe("Constructor / コンストラクタ", () => { + it("インスタンスが正常に生成されること", () => { + const molecule = new TopBtnMolecule("Test"); + expect(molecule).toBeInstanceOf(TopBtnMolecule); + }); + + it("text 引数を受け取ること", () => { + const molecule = new TopBtnMolecule("Hello"); + expect(molecule).toBeDefined(); + }); + + it("buttonMode が true に設定されること", () => { + const molecule = new TopBtnMolecule("Test"); + expect(molecule.buttonMode).toBe(true); + }); + }); + + /** + * @description playEntrance メソッドのテスト + * Test for playEntrance method + */ + describe("playEntrance Method / playEntrance メソッド", () => { + it("playEntrance メソッドを持つこと", () => { + const molecule = new TopBtnMolecule("Test"); + expect(typeof molecule.playEntrance).toBe("function"); + }); + + it("playEntrance がコールバックを受け取ること", () => { + const molecule = new TopBtnMolecule("Test"); + const callback = vi.fn(); + + expect(() => molecule.playEntrance(callback)).not.toThrow(); + }); + }); + + /** + * @description ButtonAtom 継承のテスト + * Test for ButtonAtom inheritance + */ + describe("ButtonAtom Inheritance / ButtonAtom 継承", () => { + it("ButtonAtom を継承していること", () => { + const molecule = new TopBtnMolecule("Test"); + expect("buttonMode" in molecule).toBe(true); + }); + }); +}); 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/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/view/home/HomeView.test.ts b/template/src/view/home/HomeView.test.ts new file mode 100644 index 0000000..a0d4872 --- /dev/null +++ b/template/src/view/home/HomeView.test.ts @@ -0,0 +1,132 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; + +// 依存モジュールをモック +vi.mock("@next2d/framework", () => ({ + View: vi.fn().mockImplementation(function(this: any) { + this.addChild = vi.fn(); + }) +})); + +vi.mock("@/config/Config", () => ({ + config: { + stage: { + width: 240, + height: 240 + } + } +})); + +vi.mock("@/ui/component/molecule/HomeBtnMolecule", () => ({ + HomeBtnMolecule: vi.fn().mockImplementation(function(this: any) { + this.x = 0; + this.y = 0; + this.height = 100; + this.addEventListener = vi.fn(); + }) +})); + +vi.mock("@/ui/component/atom/TextAtom", () => ({ + TextAtom: vi.fn().mockImplementation(function(this: any, text: string) { + this.text = text; + this.x = 0; + this.y = 0; + this.width = 100; + this.height = 20; + this.addEventListener = vi.fn(); + }) +})); + +vi.mock("@next2d/events", () => ({ + PointerEvent: { + POINTER_DOWN: "pointerDown", + POINTER_UP: "pointerUp" + }, + Event: { + CHANGE: "change" + } +})); + +import { HomeView } from "./HomeView"; + +/** + * @description HomeView のテスト + * Tests for HomeView + */ +describe("HomeView", () => { + let mockViewModel: any; + + beforeEach(() => { + vi.clearAllMocks(); + mockViewModel = { + getHomeText: vi.fn().mockReturnValue("Hello"), + homeContentPointerDownEvent: vi.fn(), + homeContentPointerUpEvent: vi.fn(), + homeTextChangeEvent: vi.fn() + }; + }); + + /** + * @description コンストラクタのテスト + * Test for constructor + */ + describe("Constructor / コンストラクタ", () => { + it("インスタンスが正常に生成されること", () => { + const view = new HomeView(mockViewModel); + expect(view).toBeInstanceOf(HomeView); + }); + }); + + /** + * @description initialize メソッドのテスト + * Test for initialize method + */ + describe("initialize Method / initialize メソッド", () => { + it("initialize メソッドが存在すること", () => { + const view = new HomeView(mockViewModel); + expect(typeof view.initialize).toBe("function"); + }); + + it("initialize が非同期で実行されること", async () => { + const view = new HomeView(mockViewModel); + await expect(view.initialize()).resolves.toBeUndefined(); + }); + + it("ViewModel の getHomeText が呼び出されること", async () => { + const view = new HomeView(mockViewModel); + await view.initialize(); + expect(mockViewModel.getHomeText).toHaveBeenCalled(); + }); + }); + + /** + * @description onEnter メソッドのテスト + * Test for onEnter method + */ + describe("onEnter Method / onEnter メソッド", () => { + it("onEnter メソッドが存在すること", () => { + const view = new HomeView(mockViewModel); + expect(typeof view.onEnter).toBe("function"); + }); + + it("onEnter が非同期で実行されること", async () => { + const view = new HomeView(mockViewModel); + await expect(view.onEnter()).resolves.toBeUndefined(); + }); + }); + + /** + * @description onExit メソッドのテスト + * Test for onExit method + */ + describe("onExit Method / onExit メソッド", () => { + it("onExit メソッドが存在すること", () => { + const view = new HomeView(mockViewModel); + expect(typeof view.onExit).toBe("function"); + }); + + it("onExit が非同期で実行されること", async () => { + const view = new HomeView(mockViewModel); + await expect(view.onExit()).resolves.toBeUndefined(); + }); + }); +}); diff --git a/template/src/view/home/HomeViewModel.test.ts b/template/src/view/home/HomeViewModel.test.ts new file mode 100644 index 0000000..0600e1c --- /dev/null +++ b/template/src/view/home/HomeViewModel.test.ts @@ -0,0 +1,175 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; + +// 依存モジュールをモック +vi.mock("@next2d/framework", () => ({ + ViewModel: vi.fn().mockImplementation(function(this: any) {}), + app: { + getResponse: vi.fn().mockReturnValue({ + has: vi.fn().mockReturnValue(true), + get: vi.fn().mockReturnValue({ word: "Hello, Next2D!" }) + }) + } +})); + +vi.mock("@/model/application/home/usecase/StartDragUseCase", () => ({ + StartDragUseCase: vi.fn().mockImplementation(function(this: any) { + this.execute = vi.fn(); + }) +})); + +vi.mock("@/model/application/home/usecase/StopDragUseCase", () => ({ + StopDragUseCase: vi.fn().mockImplementation(function(this: any) { + this.execute = vi.fn(); + }) +})); + +vi.mock("@/model/application/home/usecase/CenterTextFieldUseCase", () => ({ + CenterTextFieldUseCase: vi.fn().mockImplementation(function(this: any) { + this.execute = vi.fn(); + }) +})); + +vi.mock("@/config/Config", () => ({ + config: { + stage: { + width: 240 + } + } +})); + +import { HomeViewModel } from "./HomeViewModel"; +import { app } from "@next2d/framework"; + +/** + * @description HomeViewModel のテスト + * Tests for HomeViewModel + */ +describe("HomeViewModel", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + /** + * @description コンストラクタのテスト + * Test for constructor + */ + describe("Constructor / コンストラクタ", () => { + it("インスタンスが正常に生成されること", () => { + const vm = new HomeViewModel(); + expect(vm).toBeInstanceOf(HomeViewModel); + }); + }); + + /** + * @description initialize メソッドのテスト + * Test for initialize method + */ + describe("initialize Method / initialize メソッド", () => { + it("initialize メソッドが存在すること", () => { + const vm = new HomeViewModel(); + expect(typeof vm.initialize).toBe("function"); + }); + + it("initialize が非同期で実行されること", async () => { + const vm = new HomeViewModel(); + await expect(vm.initialize()).resolves.toBeUndefined(); + }); + + it("app.getResponse が呼び出されること", async () => { + const vm = new HomeViewModel(); + await vm.initialize(); + expect(app.getResponse).toHaveBeenCalled(); + }); + }); + + /** + * @description getHomeText メソッドのテスト + * Test for getHomeText method + */ + describe("getHomeText Method / getHomeText メソッド", () => { + it("getHomeText メソッドが存在すること", () => { + const vm = new HomeViewModel(); + expect(typeof vm.getHomeText).toBe("function"); + }); + + it("初期化前は空文字を返すこと", () => { + const vm = new HomeViewModel(); + expect(vm.getHomeText()).toBe(""); + }); + + it("初期化後はテキストを返すこと", async () => { + const vm = new HomeViewModel(); + await vm.initialize(); + expect(vm.getHomeText()).toBe("Hello, Next2D!"); + }); + }); + + /** + * @description homeContentPointerDownEvent メソッドのテスト + * Test for homeContentPointerDownEvent method + */ + describe("homeContentPointerDownEvent Method", () => { + it("homeContentPointerDownEvent メソッドが存在すること", () => { + const vm = new HomeViewModel(); + expect(typeof vm.homeContentPointerDownEvent).toBe("function"); + }); + + it("イベントを受け取れること", () => { + const vm = new HomeViewModel(); + const mockEvent = { + currentTarget: { + startDrag: vi.fn(), + stopDrag: vi.fn() + } + }; + + expect(() => vm.homeContentPointerDownEvent(mockEvent as any)).not.toThrow(); + }); + }); + + /** + * @description homeContentPointerUpEvent メソッドのテスト + * Test for homeContentPointerUpEvent method + */ + describe("homeContentPointerUpEvent Method", () => { + it("homeContentPointerUpEvent メソッドが存在すること", () => { + const vm = new HomeViewModel(); + expect(typeof vm.homeContentPointerUpEvent).toBe("function"); + }); + + it("イベントを受け取れること", () => { + const vm = new HomeViewModel(); + const mockEvent = { + currentTarget: { + startDrag: vi.fn(), + stopDrag: vi.fn() + } + }; + + expect(() => vm.homeContentPointerUpEvent(mockEvent as any)).not.toThrow(); + }); + }); + + /** + * @description homeTextChangeEvent メソッドのテスト + * Test for homeTextChangeEvent method + */ + describe("homeTextChangeEvent Method", () => { + it("homeTextChangeEvent メソッドが存在すること", () => { + const vm = new HomeViewModel(); + expect(typeof vm.homeTextChangeEvent).toBe("function"); + }); + + it("イベントを受け取れること", () => { + const vm = new HomeViewModel(); + const mockEvent = { + currentTarget: { + width: 100, + x: 0 + } + }; + + expect(() => vm.homeTextChangeEvent(mockEvent as any)).not.toThrow(); + }); + }); +}); diff --git a/template/src/view/top/TopView.test.ts b/template/src/view/top/TopView.test.ts new file mode 100644 index 0000000..307dd79 --- /dev/null +++ b/template/src/view/top/TopView.test.ts @@ -0,0 +1,128 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; + +// 依存モジュールをモック +vi.mock("@next2d/framework", () => ({ + View: vi.fn().mockImplementation(function(this: any) { + this.addChild = vi.fn(); + this.getChildByName = vi.fn(); + }) +})); + +vi.mock("@/config/Config", () => ({ + config: { + stage: { + width: 240, + height: 240 + } + } +})); + +vi.mock("@/ui/component/molecule/TopBtnMolecule", () => ({ + TopBtnMolecule: vi.fn().mockImplementation(function(this: any, text: string) { + this.name = ""; + this.x = 0; + this.y = 0; + this.height = 20; + this.mouseChildren = true; + this.mouseEnabled = true; + this.addEventListener = vi.fn(); + this.playEntrance = vi.fn(); + }) +})); + +vi.mock("@/ui/content/TopContent", () => ({ + TopContent: vi.fn().mockImplementation(function(this: any) { + this.x = 0; + this.y = 0; + this.height = 100; + }) +})); + +vi.mock("@next2d/events", () => ({ + PointerEvent: { + POINTER_UP: "pointerUp" + } +})); + +import { TopView } from "./TopView"; + +/** + * @description TopView のテスト + * Tests for TopView + */ +describe("TopView", () => { + let mockViewModel: any; + + beforeEach(() => { + vi.clearAllMocks(); + mockViewModel = { + getTopText: vi.fn().mockReturnValue("Start"), + onClickStartButton: vi.fn().mockResolvedValue(undefined) + }; + }); + + /** + * @description コンストラクタのテスト + * Test for constructor + */ + describe("Constructor / コンストラクタ", () => { + it("インスタンスが正常に生成されること", () => { + const view = new TopView(mockViewModel); + expect(view).toBeInstanceOf(TopView); + }); + }); + + /** + * @description initialize メソッドのテスト + * Test for initialize method + */ + describe("initialize Method / initialize メソッド", () => { + it("initialize メソッドが存在すること", () => { + const view = new TopView(mockViewModel); + expect(typeof view.initialize).toBe("function"); + }); + + it("initialize が非同期で実行されること", async () => { + const view = new TopView(mockViewModel); + await expect(view.initialize()).resolves.toBeUndefined(); + }); + + it("ViewModel の getTopText が呼び出されること", async () => { + const view = new TopView(mockViewModel); + await view.initialize(); + expect(mockViewModel.getTopText).toHaveBeenCalled(); + }); + }); + + /** + * @description onEnter メソッドのテスト + * Test for onEnter method + */ + describe("onEnter Method / onEnter メソッド", () => { + it("onEnter メソッドが存在すること", () => { + const view = new TopView(mockViewModel); + expect(typeof view.onEnter).toBe("function"); + }); + + it("onEnter が非同期で実行されること", async () => { + const view = new TopView(mockViewModel); + await expect(view.onEnter()).resolves.toBeUndefined(); + }); + }); + + /** + * @description onExit メソッドのテスト + * Test for onExit method + */ + describe("onExit Method / onExit メソッド", () => { + it("onExit メソッドが存在すること", () => { + const view = new TopView(mockViewModel); + expect(typeof view.onExit).toBe("function"); + }); + + it("onExit が非同期で実行されること", async () => { + const view = new TopView(mockViewModel); + await expect(view.onExit()).resolves.toBeUndefined(); + }); + }); +}); diff --git a/template/src/view/top/TopViewModel.test.ts b/template/src/view/top/TopViewModel.test.ts new file mode 100644 index 0000000..373a78d --- /dev/null +++ b/template/src/view/top/TopViewModel.test.ts @@ -0,0 +1,102 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; + +// 依存モジュールをモック +vi.mock("@next2d/framework", () => ({ + ViewModel: vi.fn().mockImplementation(function(this: any) {}), + app: { + getResponse: vi.fn().mockReturnValue({ + has: vi.fn().mockReturnValue(true), + get: vi.fn().mockReturnValue({ word: "Start" }) + }) + } +})); + +vi.mock("@/model/application/top/usecase/NavigateToViewUseCase", () => ({ + NavigateToViewUseCase: vi.fn().mockImplementation(function(this: any) { + this.execute = vi.fn().mockResolvedValue(undefined); + }) +})); + +import { TopViewModel } from "./TopViewModel"; +import { app } from "@next2d/framework"; + +/** + * @description TopViewModel のテスト + * Tests for TopViewModel + */ +describe("TopViewModel", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + /** + * @description コンストラクタのテスト + * Test for constructor + */ + describe("Constructor / コンストラクタ", () => { + it("インスタンスが正常に生成されること", () => { + const vm = new TopViewModel(); + expect(vm).toBeInstanceOf(TopViewModel); + }); + }); + + /** + * @description initialize メソッドのテスト + * Test for initialize method + */ + describe("initialize Method / initialize メソッド", () => { + it("initialize メソッドが存在すること", () => { + const vm = new TopViewModel(); + expect(typeof vm.initialize).toBe("function"); + }); + + it("initialize が非同期で実行されること", async () => { + const vm = new TopViewModel(); + await expect(vm.initialize()).resolves.toBeUndefined(); + }); + + it("app.getResponse が呼び出されること", async () => { + const vm = new TopViewModel(); + await vm.initialize(); + expect(app.getResponse).toHaveBeenCalled(); + }); + }); + + /** + * @description getTopText メソッドのテスト + * Test for getTopText method + */ + describe("getTopText Method / getTopText メソッド", () => { + it("getTopText メソッドが存在すること", () => { + const vm = new TopViewModel(); + expect(typeof vm.getTopText).toBe("function"); + }); + + it("初期化前は空文字を返すこと", () => { + const vm = new TopViewModel(); + expect(vm.getTopText()).toBe(""); + }); + + it("初期化後はテキストを返すこと", async () => { + const vm = new TopViewModel(); + await vm.initialize(); + expect(vm.getTopText()).toBe("Start"); + }); + }); + + /** + * @description onClickStartButton メソッドのテスト + * Test for onClickStartButton method + */ + describe("onClickStartButton Method / onClickStartButton メソッド", () => { + it("onClickStartButton メソッドが存在すること", () => { + const vm = new TopViewModel(); + expect(typeof vm.onClickStartButton).toBe("function"); + }); + + it("onClickStartButton が非同期で実行されること", async () => { + const vm = new TopViewModel(); + await expect(vm.onClickStartButton()).resolves.toBeUndefined(); + }); + }); +}); diff --git a/template/vite.config.ts b/template/vite.config.ts index 25a1156..3e41b2b 100644 --- a/template/vite.config.ts +++ b/template/vite.config.ts @@ -44,7 +44,6 @@ export default defineConfig({ "globals": true, "environment": "jsdom", "setupFiles": [ - "test.setup.ts", "@vitest/web-worker", "vitest-webgl-canvas-mock" ], From 9fef69b4f589a85c6eaf80f1d576793aa9f00c35 Mon Sep 17 00:00:00 2001 From: ienaga Date: Thu, 4 Dec 2025 07:00:00 +0900 Subject: [PATCH 15/35] #56 update README --- template/README.md | 35 +++++++++++++++-------------------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/template/README.md b/template/README.md index a3a2bdf..1ddafc1 100644 --- a/template/README.md +++ b/template/README.md @@ -201,26 +201,21 @@ Environment-specific settings are managed in the `src/config/` directory. ## ディレクトリ構成 / Directory Structure -```mermaid -graph LR - subgraph src["src/"] - config["config/
設定ファイル"] - interface["interface/
インターフェース定義"] - subgraph model["model/"] - application["application/
ユースケース"] - domain["domain/
ドメインロジック"] - infrastructure["infrastructure/
リポジトリ"] - end - subgraph ui["ui/"] - subgraph component["component/"] - atom["atom/
最小コンポーネント"] - molecule["molecule/
複合コンポーネント"] - end - content["content/
Animation Tool"] - animation["animation/
アニメーション定義"] - end - view["view/
View & ViewModel"] - end +``` +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` を参照してください。 From 3694a8139ed6cdeb7ee7ac6f37a37b74003c24cd Mon Sep 17 00:00:00 2001 From: ienaga Date: Thu, 4 Dec 2025 07:29:44 +0900 Subject: [PATCH 16/35] #56 update README --- template/README.md | 42 +++++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/template/README.md b/template/README.md index 1ddafc1..dfb17c0 100644 --- a/template/README.md +++ b/template/README.md @@ -73,22 +73,30 @@ Open [http://localhost:5173](http://localhost:5173) in your browser. This project adopts **MVVM + Clean Architecture + Atomic Design**. ```mermaid -block-beta - columns 1 - block:view["🎨 View Layer (view/, ui/)"] - view_desc["View: 画面の構造定義 / Screen structure
ViewModel: ビジネスロジックとの橋渡し / Bridge
UI Components: 再利用可能なUIパーツ / Reusable UI"] +flowchart TB + subgraph view["🎨 View Layer"] + view_path["view/, ui/"] + view_desc["View・ViewModel・UI Components"] end - block:interface["📋 Interface Layer (interface/)"] - interface_desc["型定義とインターフェース / Type definitions"] + + subgraph interface["📋 Interface Layer"] + interface_path["interface/"] + interface_desc["型定義・インターフェース"] end - block:application["⚙️ Application Layer (model/application/)"] - application_desc["UseCase: ビジネスロジック実装 / Business logic"] + + subgraph application["⚙️ Application Layer"] + app_path["model/application/"] + app_desc["UseCase: ビジネスロジック"] end - block:domain["💎 Domain Layer (model/domain/)"] - domain_desc["コアビジネスルール / Core business rules"] + + subgraph domain["💎 Domain Layer"] + domain_path["model/domain/"] + domain_desc["コアビジネスルール"] end - block:infrastructure["🔧 Infrastructure Layer (model/infrastructure/)"] - infrastructure_desc["Repository: データアクセス / Data access"] + + subgraph infrastructure["🔧 Infrastructure Layer"] + infra_path["model/infrastructure/"] + infra_desc["Repository: データアクセス"] end view --> interface @@ -96,11 +104,11 @@ block-beta application --> domain application --> infrastructure - style view fill:#e3f2fd - style interface fill:#fff9c4 - style application fill:#f3e5f5 - style domain fill:#e8f5e9 - style infrastructure fill:#fce4ec + 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) を参照してください。 From 2f44885d63198a196994401f2288c7eab1f3382b Mon Sep 17 00:00:00 2001 From: ienaga Date: Thu, 4 Dec 2025 07:38:11 +0900 Subject: [PATCH 17/35] #56 update README --- template/mock/api/README.md | 4 +- template/mock/content/README.md | 97 ++++++++++++++++++++++++++------- 2 files changed, 78 insertions(+), 23 deletions(-) diff --git a/template/mock/api/README.md b/template/mock/api/README.md index cf2ef00..fbeab51 100644 --- a/template/mock/api/README.md +++ b/template/mock/api/README.md @@ -20,7 +20,7 @@ Mock API response for the Home screen. ```json { - "word": "Hello, Next2D!" + "word": "Hello, World." } ``` @@ -34,7 +34,7 @@ Mock API response for the Top screen. ```json { - "title": "Welcome" + "title": "Click Me." } ``` diff --git a/template/mock/content/README.md b/template/mock/content/README.md index 14dff2d..df294ea 100644 --- a/template/mock/content/README.md +++ b/template/mock/content/README.md @@ -6,59 +6,114 @@ Directory for storing mock content data for local development. ## 概要 / Overview -Animation Toolからエクスポートされたコンテンツのモックデータを配置します。`http://localhost:5173/content/` でアクセス可能です。 +Next2D Animation Toolからエクスポートされたコンテンツのモックデータを配置します。`http://localhost:5173/content/` でアクセス可能です。 -Place mock data for content exported from the Animation Tool. Accessible at `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に登録されます。 -Sample content data. +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` -## 使用方法 / Usage +## 読み込みの仕組み / 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 +``` -### 1. コンテンツデータの配置 +### Framework側の処理 -Animation Toolからエクスポートしたデータ、または開発用のダミーデータを配置します。 +Frameworkの[RequestContentRepository](https://github.com/Next2D/framework/blob/develop/src/infrastructure/Request/repository/RequestContentRepository.ts)で以下の処理が行われます: -Place data exported from the Animation Tool or dummy data for development. +The following processing is performed in the Framework's [RequestContentRepository](https://github.com/Next2D/framework/blob/develop/src/infrastructure/Request/repository/RequestContentRepository.ts): -### 2. アクセス +1. **JSONの読み込み**: `Loader`クラスでJSONを非同期取得 +2. **シンボル登録**: Animation Toolで設定したシンボル名を`LoaderInfoMap`に登録 +3. **コンテンツ返却**: `ui/content/`のクラスから参照可能に -開発サーバー経由でコンテンツにアクセスします。 +## 設定例 / Configuration Example -Access content through the development server. +`src/config/Config.ts`での設定例: ```typescript -const response = await fetch("http://localhost:5173/content/sample.json"); -const data = await response.json(); +{ + "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. コンテンツデータ(JSON形式)を作成 -2. このディレクトリに配置 -3. 開発サーバーからアクセスをテスト +1. Animation ToolでMovieClip等を作成 +2. JSONとしてエクスポート +3. このディレクトリに配置 +4. `src/ui/content/`にコンテンツクラスを作成 +5. `Config.ts`のroutingに設定を追加 ## 注意事項 / Notes - モックコンテンツは開発環境でのみ使用してください - 本番環境では実際のコンテンツサーバーを設定してください -- `routing.json` のパス設定と重複しないように注意してください +- `cache: true`を設定すると、同じコンテンツの再読み込みを防げます - Use mock content only in development environments - Set actual content server in production environments -- Be careful not to conflict with path settings in `routing.json` +- Setting `cache: true` prevents reloading the same content ## 関連ドキュメント / Related Documentation -- [../README.md](../README.md) - Mockディレクトリの説明 -- [../../file/README.md](../../file/README.md) - n2dファイルの格納場所 -- [../../src/ui/content/README.md](../../src/ui/content/README.md) - Animation Toolコンテンツ +- [../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 From 8debb1c1805c3ec31babafc261983f03c8da8389 Mon Sep 17 00:00:00 2001 From: ienaga Date: Thu, 4 Dec 2025 10:35:46 +0900 Subject: [PATCH 18/35] #56 update README --- template/src/view/README.md | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/template/src/view/README.md b/template/src/view/README.md index ec26560..a75ec83 100644 --- a/template/src/view/README.md +++ b/template/src/view/README.md @@ -81,20 +81,15 @@ sequenceDiagram ## Example of directory structure -```mermaid -graph LR - subgraph src["src/"] - subgraph view["view/"] - subgraph top["top/"] - topView["TopView.ts"] - topVM["TopViewModel.ts"] - end - subgraph home["home/"] - homeView["HomeView.ts"] - homeVM["HomeViewModel.ts"] - end - end - end +``` +src/ +└── view/ + ├── top/ + │ ├── TopView.ts + │ └── TopViewModel.ts + └── home/ + ├── HomeView.ts + └── HomeViewModel.ts ``` ## Generator From 4e726084fb733e54f58146f837256805d70cb021 Mon Sep 17 00:00:00 2001 From: ienaga Date: Sun, 7 Dec 2025 09:26:36 +0900 Subject: [PATCH 19/35] #56 update README --- template/src/assets/README.md | 64 ++++++++++++++++++++++++++++++++++- template/src/model/README.md | 41 ++++++++++------------ 2 files changed, 81 insertions(+), 24 deletions(-) diff --git a/template/src/assets/README.md b/template/src/assets/README.md index 8da25f2..16bc77a 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"; + +// 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/model/README.md b/template/src/model/README.md index 767a839..6df2c8a 100644 --- a/template/src/model/README.md +++ b/template/src/model/README.md @@ -82,16 +82,16 @@ Implements business logic corresponding to user actions. Creates a UseCase class ### ディレクトリ構造 / Directory Structure -```mermaid -graph LR - subgraph application["application/"] - subgraph home["home/
Home画面"] - home_uc["usecase/
StartDragUseCase.ts
StopDragUseCase.ts
CenterTextFieldUseCase.ts"] - end - subgraph top["top/
Top画面"] - top_uc["usecase/
NavigateToViewUseCase.ts"] - end - end +``` +application/ +├── home/ # Home画面 +│ └── usecase/ +│ ├── StartDragUseCase.ts +│ ├── StopDragUseCase.ts +│ └── CenterTextFieldUseCase.ts +└── top/ # Top画面 + └── usecase/ + └── NavigateToViewUseCase.ts ``` ### 実装例 / Implementation Example @@ -151,19 +151,14 @@ Implements the core business rules of the application. Pure logic that doesn't d ### ディレクトリ構造 / Directory Structure -```mermaid -graph LR - subgraph domain["domain/"] - subgraph callback["callback/"] - subgraph Background["Background/"] - bg["Background.ts
グラデーション背景"] - subgraph service["service/"] - draw["BackgroundDrawService.ts
描画サービス"] - scale["BackgroundChangeScaleService.ts
スケール変更"] - end - end - end - end +``` +domain/ +└── callback/ + └── Background/ + ├── Background.ts # グラデーション背景 + └── service/ + ├── BackgroundDrawService.ts # 描画サービス + └── BackgroundChangeScaleService.ts # スケール変更 ``` ### 実装例 / Implementation Example From 8cbfa5a51a41cf0cfff85372092785ed9bd8a44b Mon Sep 17 00:00:00 2001 From: ienaga Date: Sun, 7 Dec 2025 09:27:53 +0900 Subject: [PATCH 20/35] #56 update README --- template/src/model/README.md | 49 ++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 28 deletions(-) diff --git a/template/src/model/README.md b/template/src/model/README.md index 6df2c8a..bcb9aa6 100644 --- a/template/src/model/README.md +++ b/template/src/model/README.md @@ -6,34 +6,27 @@ This directory is responsible for business logic and data access. Based on Clean ## 📁 現在のディレクトリ構造 / Current Directory Structure -```mermaid -graph LR - subgraph model["model/"] - subgraph application["application/
アプリケーション層"] - subgraph home["home/"] - home_usecase["usecase/
StartDragUseCase.ts
StopDragUseCase.ts
CenterTextFieldUseCase.ts"] - end - subgraph top["top/"] - top_usecase["usecase/
NavigateToViewUseCase.ts"] - end - end - subgraph domain["domain/
ドメイン層"] - subgraph callback["callback/"] - subgraph Background["Background/"] - bg_ts["Background.ts"] - subgraph service["service/"] - draw["BackgroundDrawService.ts"] - scale["BackgroundChangeScaleService.ts"] - end - end - end - end - subgraph infrastructure["infrastructure/
インフラ層"] - subgraph repository["repository/"] - repo["HomeTextRepository.ts"] - end - end - end +``` +model/ +├── application/ # アプリケーション層 +│ ├── home/ +│ │ └── usecase/ +│ │ ├── StartDragUseCase.ts +│ │ ├── StopDragUseCase.ts +│ │ └── CenterTextFieldUseCase.ts +│ └── top/ +│ └── usecase/ +│ └── NavigateToViewUseCase.ts +├── domain/ # ドメイン層 +│ └── callback/ +│ └── Background/ +│ ├── Background.ts +│ └── service/ +│ ├── BackgroundDrawService.ts +│ └── BackgroundChangeScaleService.ts +└── infrastructure/ # インフラ層 + └── repository/ + └── HomeTextRepository.ts ``` ## 🎨 アーキテクチャ概要 / Architecture Overview From 6b31ff0acfc9d0d42c3d0ddeb82894cd8f076579 Mon Sep 17 00:00:00 2001 From: ienaga Date: Sun, 7 Dec 2025 09:35:29 +0900 Subject: [PATCH 21/35] #56 update README --- template/src/model/README.md | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/template/src/model/README.md b/template/src/model/README.md index bcb9aa6..48001fc 100644 --- a/template/src/model/README.md +++ b/template/src/model/README.md @@ -245,13 +245,10 @@ Integrates with external systems (APIs, databases, etc.). Implements data access ### ディレクトリ構造 / Directory Structure -```mermaid -graph LR - subgraph infrastructure["infrastructure/"] - subgraph repository["repository/"] - repo["HomeTextRepository.ts
Home画面テキストデータ"] - end - end +``` +infrastructure/ +└── repository/ + └── HomeTextRepository.ts # Home画面テキストデータ ``` ### 実装例 / Implementation Example From 458be297ecb15193f4e2150410c708c44a757c10 Mon Sep 17 00:00:00 2001 From: ienaga Date: Sun, 7 Dec 2025 13:30:21 +0900 Subject: [PATCH 22/35] #56 update README --- template/electron/README.md | 12 +++-- template/src/model/application/README.md | 20 ++++----- template/src/model/domain/README.md | 49 ++++++++------------- template/src/model/infrastructure/README.md | 41 +++++++---------- template/src/ui/README.md | 17 +++---- template/src/ui/animation/README.md | 11 ++--- template/src/ui/component/README.md | 20 ++++----- template/src/ui/content/README.md | 10 ++--- 8 files changed, 73 insertions(+), 107 deletions(-) diff --git a/template/electron/README.md b/template/electron/README.md index 10d1cb3..ff659ff 100644 --- a/template/electron/README.md +++ b/template/electron/README.md @@ -6,13 +6,11 @@ Directory for Electron configuration used to build desktop applications for Wind ## ディレクトリ構造 / Directory Structure -```mermaid -graph LR - subgraph electron["electron/"] - icons["icons/
アプリケーションアイコン"] - index["index.js
メインプロセス"] - package["package.json
依存関係"] - end +``` +electron/ +├── icons/ # アプリケーションアイコン +├── index.js # メインプロセス +└── package.json # 依存関係 ``` ## ファイル説明 / File Description diff --git a/template/src/model/application/README.md b/template/src/model/application/README.md index 4f18b05..6185855 100644 --- a/template/src/model/application/README.md +++ b/template/src/model/application/README.md @@ -19,16 +19,16 @@ The Application layer provides business logic corresponding to user actions. Thi ## ディレクトリ構造 / Directory Structure -```mermaid -graph LR - subgraph application["application/"] - subgraph home["home/"] - home_usecase["usecase/
StartDragUseCase.ts
StopDragUseCase.ts
CenterTextFieldUseCase.ts"] - end - subgraph top["top/"] - top_usecase["usecase/
NavigateToViewUseCase.ts"] - end - end +``` +application/ +├── home/ +│ └── usecase/ +│ ├── StartDragUseCase.ts +│ ├── StopDragUseCase.ts +│ └── CenterTextFieldUseCase.ts +└── top/ + └── usecase/ + └── NavigateToViewUseCase.ts ``` 各画面ごとにディレクトリを作成し、その中に `usecase` ディレクトリを配置します。 diff --git a/template/src/model/domain/README.md b/template/src/model/domain/README.md index da98026..03a9f88 100644 --- a/template/src/model/domain/README.md +++ b/template/src/model/domain/README.md @@ -20,42 +20,31 @@ The Domain layer holds the core business rules of the application. This layer ha ## ディレクトリ構造 / Directory Structure -```mermaid -graph LR - subgraph domain["domain/"] - subgraph callback["callback/"] - subgraph Background["Background/"] - bg["Background.ts"] - subgraph service["service/"] - draw["BackgroundDrawService.ts"] - scale["BackgroundChangeScaleService.ts"] - end - end - end - end +``` +domain/ +└── callback/ + └── Background/ + ├── Background.ts + └── service/ + ├── BackgroundDrawService.ts + └── BackgroundChangeScaleService.ts ``` 将来的に以下のような拡張も可能です: Future extensions are possible, such as: -```mermaid -graph LR - subgraph domain["domain/"] - subgraph callback["callback/
コールバック処理"] - bg["Background/"] - end - subgraph service["service/
ドメインサービス"] - validation["ValidationService.ts"] - calculation["CalculationService.ts"] - end - subgraph entity["entity/
エンティティ"] - user["User.ts"] - end - subgraph value["value-object/
値オブジェクト"] - email["Email.ts"] - end - end +``` +domain/ +├── callback/ # コールバック処理 +│ └── Background/ +├── service/ # ドメインサービス +│ ├── ValidationService.ts +│ └── CalculationService.ts +├── entity/ # エンティティ +│ └── User.ts +└── value-object/ # 値オブジェクト + └── Email.ts ``` ## ドメインの概念 / Domain Concepts diff --git a/template/src/model/infrastructure/README.md b/template/src/model/infrastructure/README.md index 7eb0da3..40a2a92 100644 --- a/template/src/model/infrastructure/README.md +++ b/template/src/model/infrastructure/README.md @@ -19,37 +19,28 @@ The Infrastructure layer is responsible for interactions with the outside of the ## ディレクトリ構造 / Directory Structure -```mermaid -graph LR - subgraph infrastructure["infrastructure/"] - subgraph repository["repository/"] - home["HomeTextRepository.ts"] - end - end +``` +infrastructure/ +└── repository/ + └── HomeTextRepository.ts ``` 将来的に以下のような拡張も可能です: Future extensions are possible, such as: -```mermaid -graph LR - subgraph infrastructure["infrastructure/"] - subgraph repository["repository/
データアクセス層"] - home["HomeTextRepository.ts"] - user["UserRepository.ts"] - config["ConfigRepository.ts"] - end - subgraph entity["entity/
DBエンティティ"] - userEntity["UserEntity.ts"] - end - subgraph dto["dto/
データ転送"] - apiDto["ApiResponseDto.ts"] - end - subgraph external["external/
外部サービス"] - analytics["AnalyticsService.ts"] - end - end +``` +infrastructure/ +├── repository/ # データアクセス層 +│ ├── HomeTextRepository.ts +│ ├── UserRepository.ts +│ └── ConfigRepository.ts +├── entity/ # DBエンティティ +│ └── UserEntity.ts +├── dto/ # データ転送 +│ └── ApiResponseDto.ts +└── external/ # 外部サービス + └── AnalyticsService.ts ``` ## Repository Pattern diff --git a/template/src/ui/README.md b/template/src/ui/README.md index 8878436..18b4ede 100644 --- a/template/src/ui/README.md +++ b/template/src/ui/README.md @@ -6,16 +6,13 @@ Directory for storing UI components, structured based on Atomic Design principle ## ディレクトリ構造 / Directory Structure -```mermaid -graph LR - subgraph ui["ui/"] - animation["animation/
アニメーション定義"] - subgraph component["component/"] - atom["atom/
最小単位"] - molecule["molecule/
複合コンポーネント"] - end - content["content/
Animation Tool"] - end +``` +ui/ +├── animation/ # アニメーション定義 +├── component/ +│ ├── atom/ # 最小単位 +│ └── molecule/ # 複合コンポーネント +└── content/ # Animation Tool ``` ## アトミックデザインの階層 / Atomic Design Hierarchy diff --git a/template/src/ui/animation/README.md b/template/src/ui/animation/README.md index 1b262ea..b71ba01 100644 --- a/template/src/ui/animation/README.md +++ b/template/src/ui/animation/README.md @@ -12,13 +12,10 @@ Separating animation definitions from components improves code reusability and m ## ディレクトリ構造 / Directory Structure -```mermaid -graph LR - subgraph animation["animation/"] - subgraph top["top/"] - entrance["TopBtnEntranceAnimation.ts"] - end - end +``` +animation/ +└── top/ + └── TopBtnEntranceAnimation.ts ``` 画面ごとにサブディレクトリを作成し、その中にアニメーション定義ファイルを配置します。 diff --git a/template/src/ui/component/README.md b/template/src/ui/component/README.md index 5165e7c..d57b2bc 100644 --- a/template/src/ui/component/README.md +++ b/template/src/ui/component/README.md @@ -6,18 +6,14 @@ Directory for UI components based on Atomic Design principles. ## ディレクトリ構造 / Directory Structure -```mermaid -graph LR - subgraph component["component/"] - subgraph atom["atom/
最小単位"] - button["ButtonAtom.ts"] - text["TextAtom.ts"] - end - subgraph molecule["molecule/
複合コンポーネント"] - homeBtn["HomeBtnMolecule.ts"] - topBtn["TopBtnMolecule.ts"] - end - end +``` +component/ +├── atom/ # 最小単位 +│ ├── ButtonAtom.ts +│ └── TextAtom.ts +└── molecule/ # 複合コンポーネント + ├── HomeBtnMolecule.ts + └── TopBtnMolecule.ts ``` ## アトミックデザイン階層 / Atomic Design Hierarchy diff --git a/template/src/ui/content/README.md b/template/src/ui/content/README.md index 5aea7fb..a6502ae 100644 --- a/template/src/ui/content/README.md +++ b/template/src/ui/content/README.md @@ -12,12 +12,10 @@ Wraps animations created with the Animation Tool as TypeScript classes for use i ## ディレクトリ構造 / Directory Structure -```mermaid -graph LR - subgraph content["content/"] - home["HomeContent.ts
Home画面用"] - top["TopContent.ts
Top画面用"] - end +``` +content/ +├── HomeContent.ts # Home画面用 +└── TopContent.ts # Top画面用 ``` ## コンテンツの仕組み / How Content Works From 36ab248e78d43e559a122557f8ccc63a351dd2bd Mon Sep 17 00:00:00 2001 From: ienaga Date: Sun, 7 Dec 2025 22:15:13 +0900 Subject: [PATCH 23/35] =?UTF-8?q?#56=20framework=E3=81=AE=E5=AE=9F?= =?UTF-8?q?=E8=A3=85=E3=81=AB=E5=90=88=E3=82=8F=E3=81=9B=E3=81=A6=E3=83=86?= =?UTF-8?q?=E3=83=B3=E3=83=97=E3=83=AC=E3=83=BC=E3=83=88=E3=82=92=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/src/view/home/HomeView.ts | 9 ++++----- template/src/view/top/TopView.ts | 9 ++++----- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/template/src/view/home/HomeView.ts b/template/src/view/home/HomeView.ts index e141de7..dc87156 100644 --- a/template/src/view/home/HomeView.ts +++ b/template/src/view/home/HomeView.ts @@ -9,17 +9,16 @@ import { PointerEvent, Event } from "@next2d/events"; * @class * @extends {View} */ -export class HomeView extends View +export class HomeView extends View { /** * @param {HomeViewModel} vm * @constructor * @public */ - constructor ( - private readonly vm: HomeViewModel - ) { - super(); + constructor (vm: HomeViewModel) + { + super(vm); } /** diff --git a/template/src/view/top/TopView.ts b/template/src/view/top/TopView.ts index 6c51ef8..2771808 100644 --- a/template/src/view/top/TopView.ts +++ b/template/src/view/top/TopView.ts @@ -9,17 +9,16 @@ import { TopContent } from "@/ui/content/TopContent"; * @class * @extends {View} */ -export class TopView extends View +export class TopView extends View { /** * @param {TopViewModel} vm * @constructor * @public */ - constructor ( - private readonly vm: TopViewModel - ) { - super(); + constructor (vm: TopViewModel) + { + super(vm); } /** From ab3ab4904225c050816d27659d14195385d34ec1 Mon Sep 17 00:00:00 2001 From: ienaga Date: Mon, 8 Dec 2025 07:13:16 +0900 Subject: [PATCH 24/35] =?UTF-8?q?#56=20Lint=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../home/usecase/CenterTextFieldUseCase.ts | 3 +- .../home/usecase/StartDragUseCase.ts | 3 +- .../home/usecase/StopDragUseCase.ts | 3 +- .../top/usecase/NavigateToViewUseCase.ts | 3 +- .../src/model/domain/callback/Background.ts | 3 +- .../service/BackgroundChangeScaleService.ts | 3 +- .../service/BackgroundDrawService.ts | 3 +- .../repository/HomeTextRepository.ts | 3 +- .../animation/top/TopBtnEntranceAnimation.ts | 4 +-- .../ui/component/molecule/HomeBtnMolecule.ts | 30 ++----------------- .../ui/component/molecule/TopBtnMolecule.ts | 4 +-- template/src/ui/content/HomeContent.ts | 3 +- template/src/ui/content/TopContent.ts | 3 +- template/src/view/home/HomeView.ts | 3 +- template/src/view/home/HomeViewModel.ts | 8 +++-- template/src/view/top/TopView.ts | 4 +-- template/src/view/top/TopViewModel.ts | 8 +++-- 17 files changed, 29 insertions(+), 62 deletions(-) diff --git a/template/src/model/application/home/usecase/CenterTextFieldUseCase.ts b/template/src/model/application/home/usecase/CenterTextFieldUseCase.ts index 88f1710..2be55cd 100644 --- a/template/src/model/application/home/usecase/CenterTextFieldUseCase.ts +++ b/template/src/model/application/home/usecase/CenterTextFieldUseCase.ts @@ -6,8 +6,7 @@ import type { ITextField } from "@/interface/ITextField"; * * @class */ -export class CenterTextFieldUseCase -{ +export class CenterTextFieldUseCase { /** * @description テキストフィールドを画面中央に配置する * Center the text field on the screen diff --git a/template/src/model/application/home/usecase/StartDragUseCase.ts b/template/src/model/application/home/usecase/StartDragUseCase.ts index 300addb..9db4570 100644 --- a/template/src/model/application/home/usecase/StartDragUseCase.ts +++ b/template/src/model/application/home/usecase/StartDragUseCase.ts @@ -6,8 +6,7 @@ import type { IDraggable } from "@/interface/IDraggable"; * * @class */ -export class StartDragUseCase -{ +export class StartDragUseCase { /** * @description ドラッグ可能なオブジェクトのドラッグを開始する * Start dragging a draggable object diff --git a/template/src/model/application/home/usecase/StopDragUseCase.ts b/template/src/model/application/home/usecase/StopDragUseCase.ts index b5fcb32..9065b54 100644 --- a/template/src/model/application/home/usecase/StopDragUseCase.ts +++ b/template/src/model/application/home/usecase/StopDragUseCase.ts @@ -6,8 +6,7 @@ import type { IDraggable } from "@/interface/IDraggable"; * * @class */ -export class StopDragUseCase -{ +export class StopDragUseCase { /** * @description ドラッグ可能なオブジェクトのドラッグを停止する * Stop dragging a draggable object diff --git a/template/src/model/application/top/usecase/NavigateToViewUseCase.ts b/template/src/model/application/top/usecase/NavigateToViewUseCase.ts index 0b03567..f77a203 100644 --- a/template/src/model/application/top/usecase/NavigateToViewUseCase.ts +++ b/template/src/model/application/top/usecase/NavigateToViewUseCase.ts @@ -7,8 +7,7 @@ import { app } from "@next2d/framework"; * * @class */ -export class NavigateToViewUseCase -{ +export class NavigateToViewUseCase { /** * @description 指定された画面に遷移する * Navigate to the specified view 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.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.ts b/template/src/model/domain/callback/Background/service/BackgroundDrawService.ts index aadb151..dbd384c 100644 --- a/template/src/model/domain/callback/Background/service/BackgroundDrawService.ts +++ b/template/src/model/domain/callback/Background/service/BackgroundDrawService.ts @@ -11,8 +11,7 @@ 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; diff --git a/template/src/model/infrastructure/repository/HomeTextRepository.ts b/template/src/model/infrastructure/repository/HomeTextRepository.ts index fbcd05c..e92382f 100644 --- a/template/src/model/infrastructure/repository/HomeTextRepository.ts +++ b/template/src/model/infrastructure/repository/HomeTextRepository.ts @@ -4,8 +4,7 @@ import { config } from "@/config/Config"; /** * @class */ -export class HomeTextRepository -{ +export class HomeTextRepository { /** * @description Home画面のテキストデータを取得 * Get text data for Home screen diff --git a/template/src/ui/animation/top/TopBtnEntranceAnimation.ts b/template/src/ui/animation/top/TopBtnEntranceAnimation.ts index 003739f..948600d 100644 --- a/template/src/ui/animation/top/TopBtnEntranceAnimation.ts +++ b/template/src/ui/animation/top/TopBtnEntranceAnimation.ts @@ -9,8 +9,8 @@ import { Event } from "@next2d/events"; * @class * @public */ -export class TopBtnEntranceAnimation -{ +export class TopBtnEntranceAnimation { + private readonly _entranceJob: Job; /** diff --git a/template/src/ui/component/molecule/HomeBtnMolecule.ts b/template/src/ui/component/molecule/HomeBtnMolecule.ts index 9b7757e..e35f4bb 100644 --- a/template/src/ui/component/molecule/HomeBtnMolecule.ts +++ b/template/src/ui/component/molecule/HomeBtnMolecule.ts @@ -11,8 +11,8 @@ import { ButtonAtom } from "../atom/ButtonAtom"; * @implements {IDraggable} * @public */ -export class HomeBtnMolecule extends ButtonAtom implements IDraggable -{ +export class HomeBtnMolecule extends ButtonAtom implements IDraggable { + private readonly homeContent: HomeContent; /** @@ -29,30 +29,4 @@ export class HomeBtnMolecule extends ButtonAtom implements IDraggable this.addChild(this.homeContent); } - - /** - * @description ドラッグを開始する - * Start dragging - * - * @return {void} - * @method - * @public - */ - startDrag (): void - { - this.homeContent.startDrag(); - } - - /** - * @description ドラッグを停止する - * Stop dragging - * - * @return {void} - * @method - * @public - */ - stopDrag (): void - { - this.homeContent.stopDrag(); - } } \ No newline at end of file diff --git a/template/src/ui/component/molecule/TopBtnMolecule.ts b/template/src/ui/component/molecule/TopBtnMolecule.ts index 5d79bfd..8a76914 100644 --- a/template/src/ui/component/molecule/TopBtnMolecule.ts +++ b/template/src/ui/component/molecule/TopBtnMolecule.ts @@ -10,8 +10,8 @@ import { TextAtom } from "../atom/TextAtom"; * @extends {ButtonAtom} * @public */ -export class TopBtnMolecule extends ButtonAtom -{ +export class TopBtnMolecule extends ButtonAtom { + /** * @param {string} text - ボタンに表示するテキスト / Text to display on the button * @constructor diff --git a/template/src/ui/content/HomeContent.ts b/template/src/ui/content/HomeContent.ts index 0069be3..9a0d3a3 100644 --- a/template/src/ui/content/HomeContent.ts +++ b/template/src/ui/content/HomeContent.ts @@ -7,8 +7,7 @@ import { MovieClipContent } from "@next2d/framework"; * @extends {MovieClipContent} * @implements {IDraggable} */ -export class HomeContent extends MovieClipContent implements IDraggable -{ +export class HomeContent extends MovieClipContent implements IDraggable { /** * @return {string} * @readonly diff --git a/template/src/ui/content/TopContent.ts b/template/src/ui/content/TopContent.ts index d6564d4..90f05be 100644 --- a/template/src/ui/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/home/HomeView.ts b/template/src/view/home/HomeView.ts index dc87156..1f93df7 100644 --- a/template/src/view/home/HomeView.ts +++ b/template/src/view/home/HomeView.ts @@ -9,8 +9,7 @@ import { PointerEvent, Event } from "@next2d/events"; * @class * @extends {View} */ -export class HomeView extends View -{ +export class HomeView extends View { /** * @param {HomeViewModel} vm * @constructor diff --git a/template/src/view/home/HomeViewModel.ts b/template/src/view/home/HomeViewModel.ts index 3dab1b1..f5da420 100644 --- a/template/src/view/home/HomeViewModel.ts +++ b/template/src/view/home/HomeViewModel.ts @@ -11,8 +11,8 @@ 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; @@ -39,7 +39,9 @@ export class HomeViewModel extends ViewModel async initialize (): Promise { const response = app.getResponse(); - this.homeText = response.has("HomeText") ? response.get("HomeText").word : ""; + this.homeText = response.has("HomeText") + ? (response.get("HomeText") as { word: string }).word + : ""; } /** diff --git a/template/src/view/top/TopView.ts b/template/src/view/top/TopView.ts index 2771808..c2c68e3 100644 --- a/template/src/view/top/TopView.ts +++ b/template/src/view/top/TopView.ts @@ -9,8 +9,8 @@ import { TopContent } from "@/ui/content/TopContent"; * @class * @extends {View} */ -export class TopView extends View -{ +export class TopView extends View { + /** * @param {TopViewModel} vm * @constructor diff --git a/template/src/view/top/TopViewModel.ts b/template/src/view/top/TopViewModel.ts index 9a7a89e..716e4f3 100644 --- a/template/src/view/top/TopViewModel.ts +++ b/template/src/view/top/TopViewModel.ts @@ -5,8 +5,8 @@ import { NavigateToViewUseCase } from "@/model/application/top/usecase/NavigateT * @class * @extends {ViewModel} */ -export class TopViewModel extends ViewModel -{ +export class TopViewModel extends ViewModel { + private readonly navigateToViewUseCase: NavigateToViewUseCase; private topText: string = ""; @@ -29,7 +29,9 @@ export class TopViewModel extends ViewModel async initialize (): Promise { const response = app.getResponse(); - this.topText = response.has("TopText") ? response.get("TopText").word : ""; + this.topText = response.has("TopText") + ? (response.get("TopText") as { word: string }).word + : ""; } /** From 09b2083e845428cb1bb4ad166bca97b104d23bdb Mon Sep 17 00:00:00 2001 From: ienaga Date: Mon, 8 Dec 2025 07:26:08 +0900 Subject: [PATCH 25/35] #56 update github actions --- .github/workflows/lint.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index d70a3b5..6fd04c1 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -23,9 +23,8 @@ jobs: node-version: 24 registry-url: "https://registry.npmjs.org" - run: npm install -g npm@latest - - run: | - npm install - npx eslint ./src/**/*.ts + - run: npm install + - run: npx eslint ./src/**/*.ts working-directory: ./template windows-browser-test: @@ -40,8 +39,7 @@ jobs: node-version: 24 registry-url: "https://registry.npmjs.org" - run: npm install -g npm@latest - - run: | - npm install - npx eslint ./src/**/*.ts + - run: npm install + - run: npx eslint ./src/**/*.ts working-directory: ./template \ No newline at end of file From 9d5b8edf5fc2b1afd02890cc7b73a7bb57a9302e Mon Sep 17 00:00:00 2001 From: ienaga Date: Mon, 8 Dec 2025 07:40:54 +0900 Subject: [PATCH 26/35] #56 update github actions --- .github/workflows/lint.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 6fd04c1..cf62122 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -24,8 +24,9 @@ jobs: registry-url: "https://registry.npmjs.org" - run: npm install -g npm@latest - run: npm install - - run: npx eslint ./src/**/*.ts - working-directory: ./template + - run: | + cd template + npx eslint ./src/**/*.ts windows-browser-test: runs-on: windows-latest @@ -40,6 +41,7 @@ jobs: registry-url: "https://registry.npmjs.org" - run: npm install -g npm@latest - run: npm install - - run: npx eslint ./src/**/*.ts - working-directory: ./template + - run: | + cd template + npx eslint ./src/**/*.ts \ No newline at end of file From 95f3005bce7af21f1858b9e51fe45985d7d920a9 Mon Sep 17 00:00:00 2001 From: ienaga Date: Mon, 8 Dec 2025 07:42:59 +0900 Subject: [PATCH 27/35] #56 update github actions --- .github/workflows/lint.yml | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index cf62122..7bfad26 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -22,11 +22,7 @@ jobs: with: node-version: 24 registry-url: "https://registry.npmjs.org" - - run: npm install -g npm@latest - - run: npm install - - run: | - cd template - npx eslint ./src/**/*.ts + - run: cd template && npx eslint ./src/**/*.ts windows-browser-test: runs-on: windows-latest @@ -39,9 +35,5 @@ jobs: with: node-version: 24 registry-url: "https://registry.npmjs.org" - - run: npm install -g npm@latest - - run: npm install - - run: | - cd template - npx eslint ./src/**/*.ts + - run: cd template && npx eslint ./src/**/*.ts \ No newline at end of file From b64d2726d7b549b4ee57f1b0830b64dd7000213d Mon Sep 17 00:00:00 2001 From: ienaga Date: Mon, 8 Dec 2025 07:47:11 +0900 Subject: [PATCH 28/35] #56 update github actions --- .github/workflows/lint.yml | 10 ++++++++-- template/package.json | 34 +++++++++++++++++----------------- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 7bfad26..8ca924c 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -22,7 +22,10 @@ jobs: with: node-version: 24 registry-url: "https://registry.npmjs.org" - - run: cd template && npx eslint ./src/**/*.ts + - run: npm install -D eslint-plugin-unused-imports + working-directory: ./template + - run: npx eslint ./src/**/*.ts + working-directory: ./template windows-browser-test: runs-on: windows-latest @@ -35,5 +38,8 @@ jobs: with: node-version: 24 registry-url: "https://registry.npmjs.org" - - run: cd template && npx eslint ./src/**/*.ts + - 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/template/package.json b/template/package.json index 273429e..31c8987 100644 --- a/template/package.json +++ b/template/package.json @@ -4,23 +4,23 @@ "description": "Next2D Framework TypeScript template.", "type": "module", "scripts": { - "start": "vite --host", - "preview:ios": "npx @next2d/builder --platform ios --preview", - "preview:android": "npx @next2d/builder --platform android --preview", - "preview:macos": "npx @next2d/builder --platform macos --preview", - "preview:windows": "npx @next2d/builder --platform windows --preview", - "preview:linux": "npx @next2d/builder --platform linux --preview", - "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" + "start": "vite --host", + "preview:ios": "npx @next2d/builder --platform ios --preview", + "preview:android": "npx @next2d/builder --platform android --preview", + "preview:macos": "npx @next2d/builder --platform macos --preview", + "preview:windows": "npx @next2d/builder --platform windows --preview", + "preview:linux": "npx @next2d/builder --platform linux --preview", + "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", From 3454cb0fafde11f6ded48df19cad9b45b5c771a1 Mon Sep 17 00:00:00 2001 From: ienaga Date: Mon, 8 Dec 2025 07:48:29 +0900 Subject: [PATCH 29/35] #56 update github actions --- .github/workflows/lint.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 8ca924c..4d54eac 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -23,9 +23,9 @@ jobs: node-version: 24 registry-url: "https://registry.npmjs.org" - run: npm install -D eslint-plugin-unused-imports - working-directory: ./template + working-directory: ./template - run: npx eslint ./src/**/*.ts - working-directory: ./template + working-directory: ./template windows-browser-test: runs-on: windows-latest @@ -39,7 +39,7 @@ jobs: node-version: 24 registry-url: "https://registry.npmjs.org" - run: npm install -D eslint-plugin-unused-imports - working-directory: ./template + working-directory: ./template - run: npx eslint ./src/**/*.ts - working-directory: ./template + working-directory: ./template \ No newline at end of file From 188605a819555f3879c3376beb37aa8d4ef81f87 Mon Sep 17 00:00:00 2001 From: ienaga Date: Mon, 8 Dec 2025 07:53:09 +0900 Subject: [PATCH 30/35] #56 update packages --- template/package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/template/package.json b/template/package.json index 31c8987..25f0c80 100644 --- a/template/package.json +++ b/template/package.json @@ -31,9 +31,9 @@ "@eslint/js": "^9.39.1", "@next2d/vite-plugin-next2d-auto-loader": "^3.1.10", "@types/node": "^24.10.1", - "@typescript-eslint/eslint-plugin": "^8.48.0", - "@typescript-eslint/parser": "^8.48.0", - "@vitest/web-worker": "^4.0.14", + "@typescript-eslint/eslint-plugin": "^8.48.1", + "@typescript-eslint/parser": "^8.48.1", + "@vitest/web-worker": "^4.0.15", "eslint": "^9.39.1", "eslint-plugin-unused-imports": "^4.3.0", "globals": "^16.5.0", @@ -41,7 +41,7 @@ "typescript": "^5.9.3", "vite": "^7.2.6", "vite-tsconfig-paths": "^5.1.4", - "vitest": "^4.0.14", + "vitest": "^4.0.15", "vitest-webgl-canvas-mock": "^1.1.0" }, "peerDependencies": { From 631bb23dfd6c92904ae14833007e4c0e729763b9 Mon Sep 17 00:00:00 2001 From: ienaga Date: Wed, 21 Jan 2026 22:41:20 +0900 Subject: [PATCH 31/35] =?UTF-8?q?#56=20=E6=A7=8B=E9=80=A0=E3=82=92v4.0.0?= =?UTF-8?q?=20=E5=BF=9C=20(WIP)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/.gitignore | 3 +- template/package.json | 32 +-- template/src/index.ts | 3 +- template/src/interface/IConfig.ts | 21 -- template/src/interface/IGotoView.ts | 15 -- template/src/interface/IRequest.ts | 14 -- template/src/interface/IRequestType.ts | 1 - template/src/interface/IRouting.ts | 7 - template/src/interface/IStage.ts | 8 - template/src/interface/ITextFormatObject.ts | 22 +- template/src/ui/component/atom/README.md | 179 -------------- template/src/ui/component/molecule/README.md | 231 ------------------ template/src/ui/component/page/top/TopPage.ts | 0 template/tsconfig.json | 3 +- 14 files changed, 32 insertions(+), 507 deletions(-) delete mode 100644 template/src/interface/IConfig.ts delete mode 100644 template/src/interface/IGotoView.ts delete mode 100644 template/src/interface/IRequest.ts delete mode 100644 template/src/interface/IRequestType.ts delete mode 100644 template/src/interface/IRouting.ts delete mode 100644 template/src/interface/IStage.ts delete mode 100644 template/src/ui/component/atom/README.md delete mode 100644 template/src/ui/component/molecule/README.md create mode 100644 template/src/ui/component/page/top/TopPage.ts 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/package.json b/template/package.json index 25f0c80..2a819c7 100644 --- a/template/package.json +++ b/template/package.json @@ -23,25 +23,25 @@ "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", + "@capacitor/android": "^8.0.1", + "@capacitor/cli": "^8.0.1", + "@capacitor/core": "^8.0.1", + "@capacitor/ios": "^8.0.1", "@eslint/eslintrc": "^3.3.3", - "@eslint/js": "^9.39.1", - "@next2d/vite-plugin-next2d-auto-loader": "^3.1.10", - "@types/node": "^24.10.1", - "@typescript-eslint/eslint-plugin": "^8.48.1", - "@typescript-eslint/parser": "^8.48.1", - "@vitest/web-worker": "^4.0.15", - "eslint": "^9.39.1", + "@eslint/js": "^9.39.2", + "@next2d/vite-plugin-next2d-auto-loader": "^3.1.12", + "@types/node": "^25.0.9", + "@typescript-eslint/eslint-plugin": "^8.53.1", + "@typescript-eslint/parser": "^8.53.1", + "@vitest/web-worker": "^4.0.17", + "eslint": "^9.39.2", "eslint-plugin-unused-imports": "^4.3.0", - "globals": "^16.5.0", - "jsdom": "^27.2.0", + "globals": "^17.0.0", + "jsdom": "^27.4.0", "typescript": "^5.9.3", - "vite": "^7.2.6", - "vite-tsconfig-paths": "^5.1.4", - "vitest": "^4.0.15", + "vite": "^7.3.1", + "vite-tsconfig-paths": "^6.0.4", + "vitest": "^4.0.17", "vitest-webgl-canvas-mock": "^1.1.0" }, "peerDependencies": { 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/IGotoView.ts b/template/src/interface/IGotoView.ts deleted file mode 100644 index 9ba75c5..0000000 --- a/template/src/interface/IGotoView.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * @description 画面遷移オプションのインターフェース - * Interface for view navigation options - * - * @interface - */ -export interface IGotoView { - /** - * @description 画面遷移後に実行するコールバック関数名 - * Callback function name(s) to execute after view transition - * - * @type {string | string[]} - */ - callback: string | string[]; -} \ No newline at end of file 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/ITextFormatObject.ts b/template/src/interface/ITextFormatObject.ts index df67bab..a52ffa8 100644 --- a/template/src/interface/ITextFormatObject.ts +++ b/template/src/interface/ITextFormatObject.ts @@ -1,15 +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; + 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/ui/component/atom/README.md b/template/src/ui/component/atom/README.md deleted file mode 100644 index c66fc1b..0000000 --- a/template/src/ui/component/atom/README.md +++ /dev/null @@ -1,179 +0,0 @@ -# Atom Components - -最小単位のUIコンポーネントを格納するディレクトリです。アトミックデザインにおける「原子」に相当します。 - -Directory for the smallest UI components. Corresponds to "Atoms" in Atomic Design. - -## 概要 / Overview - -Atomは、これ以上分割できない最も基本的なUI要素です。ボタン、テキストフィールド、アイコンなどの基本パーツがここに含まれます。 - -Atoms are the most basic UI elements that cannot be divided further. Basic parts such as buttons, text fields, and icons are included here. - -## コンポーネント一覧 / Component List - -### ButtonAtom.ts - -ボタンの基本機能を提供するコンポーネントです。 - -Component that provides basic button functionality. - -```typescript -import { Sprite } from "@next2d/display"; - -export class ButtonAtom extends Sprite { - constructor() { - super(); - this.buttonMode = true; // マウスカーソルがポインターに変更 - } -} -``` - -**特徴 / Features:** -- マウスカーソルがポインター型に変更される -- ボタンとしての基本的な振る舞いを提供 -- Moleculeコンポーネントの基底クラスとして使用 - -### TextAtom.ts - -テキスト表示の基本機能を提供するコンポーネントです。 - -Component that provides basic text display functionality. - -```typescript -import { TextField } from "@next2d/display"; -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(); - // プロパティ設定とフォーマット適用 - } -} -``` - -**特徴 / Features:** -- 柔軟なテキストフォーマット設定 -- プロパティの動的設定が可能 -- `ITextField` インターフェースを実装 - -## 設計原則 / Design Principles - -### 1. 最小限の機能 / Minimal Functionality - -Atomは1つの明確な役割のみを持ちます。 - -Atoms have only one clear role. - -```typescript -// ✅ 良い例: ボタンの基本機能のみ -export class ButtonAtom extends Sprite { - constructor() { - super(); - this.buttonMode = true; - } -} - -// ❌ 悪い例: 複数の責務 -export class ButtonAtom extends Sprite { - async fetchData() { ... } // NG: データ取得は別層の責務 - navigate() { ... } // NG: ナビゲーションは別層の責務 -} -``` - -### 2. 汎用性 / Genericity - -特定の画面に依存せず、汎用的に使用できるように設計します。 - -Design to be used generically without depending on specific screens. - -```typescript -// ✅ 良い例: 汎用的なテキストコンポーネント -export class TextAtom extends TextField { - constructor(text: string, props: any = null) { - super(); - this.text = text; - } -} - -// ❌ 悪い例: 特定画面に依存 -export class HomeTextAtom extends TextField { // NG: 画面固有 - constructor() { - super(); - this.text = "Home Screen"; // NG: 固定値 - } -} -``` - -### 3. インターフェース実装 / Interface Implementation - -必要に応じてインターフェースを実装し、型安全性を確保します。 - -Implement interfaces as needed to ensure type safety. - -```typescript -import type { ITextField } from "@/interface/ITextField"; - -export class TextAtom extends TextField implements ITextField { - width: number; - x: number; -} -``` - -## 新しいAtomの追加方法 / Adding New Atoms - -### 手順 / Steps - -1. 基本クラスを継承(`Sprite`, `TextField`, `Shape` など) -2. コンストラクタで基本設定を行う -3. 必要に応じてインターフェースを実装 -4. JSDocコメントを追加 - -### テンプレート / Template - -```typescript -import { Sprite } from "@next2d/display"; - -/** - * @description [コンポーネントの説明] - * [Component description] - * - * @class - * @extends {Sprite} - */ -export class YourAtom extends Sprite -{ - /** - * @description コンストラクタ - * Constructor - * - * @constructor - * @public - */ - constructor () - { - super(); - - // 初期設定 - } -} -``` - -## ベストプラクティス / Best Practices - -1. **単一責任** - 1つのAtomは1つの責務のみ -2. **汎用性** - 特定の画面やデータに依存しない -3. **再利用性** - 異なるMoleculeから使用可能 -4. **インターフェース** - 型安全性のため必要に応じて実装 - -## 関連ドキュメント / Related Documentation - -- [../molecule/README.md](../molecule/README.md) - Moleculeコンポーネント -- [../README.md](../README.md) - コンポーネント全体の説明 -- [../../README.md](../../README.md) - UI全体の説明 -- [../../../interface/README.md](../../../interface/README.md) - インターフェース定義 diff --git a/template/src/ui/component/molecule/README.md b/template/src/ui/component/molecule/README.md deleted file mode 100644 index a49496a..0000000 --- a/template/src/ui/component/molecule/README.md +++ /dev/null @@ -1,231 +0,0 @@ -# Molecule Components - -複数のAtomを組み合わせた複合コンポーネントを格納するディレクトリです。アトミックデザインにおける「分子」に相当します。 - -Directory for composite components combining multiple Atoms. Corresponds to "Molecules" in Atomic Design. - -## 概要 / Overview - -Moleculeは、複数のAtomを組み合わせて、より具体的な機能を持つコンポーネントです。画面固有のボタンやフォームなどがここに含まれます。 - -Molecules are components with more specific functionality by combining multiple Atoms. Screen-specific buttons, forms, etc. are included here. - -## コンポーネント一覧 / Component List - -### HomeBtnMolecule.ts - -Home画面用のボタンコンポーネントです。ドラッグ&ドロップ機能を提供します。 - -Button component for the Home screen. Provides drag and drop functionality. - -```typescript -import { ButtonAtom } from "../atom/ButtonAtom"; -import type { IDraggable } from "@/interface/IDraggable"; -import { HomeContent } from "@/ui/content/HomeContent"; - -export class HomeBtnMolecule extends ButtonAtom implements IDraggable { - private readonly homeContent: HomeContent; - - constructor() { - super(); - this.homeContent = new HomeContent(); - this.addChild(this.homeContent); - } - - startDrag(): void { - // ドラッグ開始処理 - } - - stopDrag(): void { - // ドラッグ停止処理 - } -} -``` - -**特徴 / Features:** -- `ButtonAtom` を継承 -- `IDraggable` インターフェースを実装 -- `HomeContent` (Animation Toolコンテンツ) を含む -- ドラッグ&ドロップ機能を提供 - -### TopBtnMolecule.ts - -Top画面用のボタンコンポーネントです。入場アニメーション機能を提供します。 - -Button component for the Top screen. Provides entrance animation functionality. - -```typescript -import { ButtonAtom } from "../atom/ButtonAtom"; -import { TextAtom } from "../atom/TextAtom"; - -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:** -- `ButtonAtom` を継承 -- テキストはViewModelから引数で受け取る(データ取得はViewModelの責務) -- 入場アニメーション機能を提供 -- `TextAtom` を子要素として持つ - -## 設計原則 / Design Principles - -### 1. Atomの組み合わせ / Combining Atoms - -MoleculeはAtomを組み合わせて構成します。 - -Molecules are composed by combining Atoms. - -```typescript -// ✅ 良い例: Atomを組み合わせる -export class TopBtnMolecule extends ButtonAtom { - constructor(text: string) { - super(); - const textField = new TextAtom(text); // TextAtomを使用 - this.addChild(textField); - } -} -``` - -### 2. 画面固有の機能 / Screen-specific Functionality - -Moleculeは特定の画面の要件に合わせた機能を実装します。 - -Molecules implement functionality tailored to specific screen requirements. - -```typescript -// ✅ 良い例: Home画面用のドラッグ機能 -export class HomeBtnMolecule extends ButtonAtom implements IDraggable { - startDrag(): void { ... } - stopDrag(): void { ... } -} - -// ✅ 良い例: Top画面用のアニメーション -export class TopBtnMolecule extends ButtonAtom { - playEntrance(callback: () => void): void { ... } -} -``` - -### 3. データの受け取り / Receiving Data - -Moleculeはデータを自ら取得せず、ViewModelから引数として受け取ります。 - -Molecules don't fetch data themselves; they receive it as arguments from ViewModel. - -```typescript -// ✅ 良い例: 引数でデータを受け取る -export class TopBtnMolecule extends ButtonAtom { - constructor(text: string) { // ViewModelから受け取る - super(); - this.textField = new TextAtom(text); - } -} - -// ❌ 悪い例: 直接データ取得 -export class TopBtnMolecule extends ButtonAtom { - async constructor() { - const data = await Repository.get(); // NG - } -} -``` - -### 4. インターフェース実装 / Interface Implementation - -ビジネスロジック層との連携のためにインターフェースを実装します。 - -Implement interfaces for integration with the business logic layer. - -```typescript -import type { IDraggable } from "@/interface/IDraggable"; - -export class HomeBtnMolecule extends ButtonAtom implements IDraggable { - startDrag(): void { ... } - stopDrag(): void { ... } -} -``` - -## 新しいMoleculeの追加方法 / Adding New Molecules - -### 手順 / Steps - -1. 適切なAtomを継承(通常は `ButtonAtom`) -2. 必要なAtomを子要素として追加 -3. 画面固有の機能を実装 -4. 必要に応じてインターフェースを実装 -5. JSDocコメントを追加 - -### テンプレート / Template - -```typescript -import { ButtonAtom } from "../atom/ButtonAtom"; -import { TextAtom } from "../atom/TextAtom"; -import type { IYourInterface } from "@/interface/IYourInterface"; - -/** - * @description [コンポーネントの説明] - * [Component description] - * - * @class - * @extends {ButtonAtom} - * @implements {IYourInterface} - */ -export class YourMolecule extends ButtonAtom implements IYourInterface -{ - /** - * @description コンストラクタ - * Constructor - * - * @param {string} text - 表示テキスト / Display text - * @constructor - * @public - */ - constructor (text: string) - { - super(); - - // Atomを追加 - const textField = new TextAtom(text); - this.addChild(textField); - } - - /** - * @description [メソッドの説明] - * [Method description] - * - * @return {void} - * @method - * @public - */ - yourMethod (): void - { - // 実装 - } -} -``` - -## ベストプラクティス / Best Practices - -1. **Atom優先** - 可能な限りAtomを再利用 -2. **データ注入** - コンストラクタでデータを受け取る -3. **インターフェース** - ビジネスロジックとの連携に使用 -4. **単一画面** - 1つのMoleculeは通常1つの画面で使用 -5. **命名規則** - `{Screen}Btn/Form/ListMolecule` など明確に - -## 関連ドキュメント / Related Documentation - -- [../atom/README.md](../atom/README.md) - Atomコンポーネント -- [../README.md](../README.md) - コンポーネント全体の説明 -- [../../animation/README.md](../../animation/README.md) - アニメーション定義 -- [../../content/README.md](../../content/README.md) - Animation Toolコンテンツ -- [../../../interface/README.md](../../../interface/README.md) - インターフェース定義 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..e69de29 diff --git a/template/tsconfig.json b/template/tsconfig.json index fc69ee5..747296e 100644 --- a/template/tsconfig.json +++ b/template/tsconfig.json @@ -32,6 +32,7 @@ "@types/**/*.ts" ], "exclude": [ - "node_modules/**" + "node_modules/**", + "src/**/*.test.ts" ] } \ No newline at end of file From ab7cf0fe40dee65e1f96747ebe53f69da9b88e8c Mon Sep 17 00:00:00 2001 From: ienaga Date: Sat, 24 Jan 2026 23:20:55 +0900 Subject: [PATCH 32/35] =?UTF-8?q?#57=20View-ViewModel=E3=81=AE=E3=83=AB?= =?UTF-8?q?=E3=83=BC=E3=83=AB=E3=81=AB=E5=90=88=E3=82=8F=E3=81=9B=E3=81=A6?= =?UTF-8?q?=E3=82=B5=E3=83=B3=E3=83=97=E3=83=AB=E3=82=92=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/package.json | 10 +- template/src/assets/README.md | 2 +- template/src/ui/animation/README.md | 221 ++++++++++++------ .../top/TopBtnEntranceAnimation.test.ts | 103 -------- ...nceAnimation.ts => TopBtnShowAnimation.ts} | 10 +- .../src/ui/component/atom/ButtonAtom.test.ts | 61 ----- template/src/ui/component/atom/ButtonAtom.ts | 27 +++ .../src/ui/component/atom/TextAtom.test.ts | 101 -------- .../molecule/HomeBtnMolecule.test.ts | 80 ------- .../ui/component/molecule/HomeBtnMolecule.ts | 2 +- .../component/molecule/TopBtnMolecule.test.ts | 83 ------- .../ui/component/molecule/TopBtnMolecule.ts | 7 +- .../src/ui/component/page/home/HomePage.ts | 84 +++++++ template/src/ui/component/page/top/TopPage.ts | 89 +++++++ template/src/view/home/HomeView.test.ts | 132 ----------- template/src/view/home/HomeView.ts | 71 +----- template/src/view/home/HomeViewModel.test.ts | 175 -------------- template/src/view/top/TopView.test.ts | 128 ---------- template/src/view/top/TopView.ts | 68 +----- template/src/view/top/TopViewModel.test.ts | 102 -------- 20 files changed, 391 insertions(+), 1165 deletions(-) delete mode 100644 template/src/ui/animation/top/TopBtnEntranceAnimation.test.ts rename template/src/ui/animation/top/{TopBtnEntranceAnimation.ts => TopBtnShowAnimation.ts} (82%) delete mode 100644 template/src/ui/component/atom/ButtonAtom.test.ts delete mode 100644 template/src/ui/component/atom/TextAtom.test.ts delete mode 100644 template/src/ui/component/molecule/HomeBtnMolecule.test.ts delete mode 100644 template/src/ui/component/molecule/TopBtnMolecule.test.ts create mode 100644 template/src/ui/component/page/home/HomePage.ts delete mode 100644 template/src/view/home/HomeView.test.ts delete mode 100644 template/src/view/home/HomeViewModel.test.ts delete mode 100644 template/src/view/top/TopView.test.ts delete mode 100644 template/src/view/top/TopViewModel.test.ts diff --git a/template/package.json b/template/package.json index 2a819c7..83b527b 100644 --- a/template/package.json +++ b/template/package.json @@ -30,18 +30,18 @@ "@eslint/eslintrc": "^3.3.3", "@eslint/js": "^9.39.2", "@next2d/vite-plugin-next2d-auto-loader": "^3.1.12", - "@types/node": "^25.0.9", + "@types/node": "^25.0.10", "@typescript-eslint/eslint-plugin": "^8.53.1", "@typescript-eslint/parser": "^8.53.1", - "@vitest/web-worker": "^4.0.17", + "@vitest/web-worker": "^4.0.18", "eslint": "^9.39.2", "eslint-plugin-unused-imports": "^4.3.0", - "globals": "^17.0.0", + "globals": "^17.1.0", "jsdom": "^27.4.0", "typescript": "^5.9.3", "vite": "^7.3.1", - "vite-tsconfig-paths": "^6.0.4", - "vitest": "^4.0.17", + "vite-tsconfig-paths": "^6.0.5", + "vitest": "^4.0.18", "vitest-webgl-canvas-mock": "^1.1.0" }, "peerDependencies": { diff --git a/template/src/assets/README.md b/template/src/assets/README.md index 16bc77a..e4f6bea 100644 --- a/template/src/assets/README.md +++ b/template/src/assets/README.md @@ -15,7 +15,7 @@ Place static assets that you want to include in the bundle at build time. You ca ### 画像のインポート / Importing Images ```typescript -import logoImage from "@/assets/logo.png"; +import logoImage from "@/assets/logo.png?inline"; // logoImageはビルド時に解決されたURLになります // logoImage will be a resolved URL at build time diff --git a/template/src/ui/animation/README.md b/template/src/ui/animation/README.md index b71ba01..40df209 100644 --- a/template/src/ui/animation/README.md +++ b/template/src/ui/animation/README.md @@ -15,7 +15,7 @@ Separating animation definitions from components improves code reusability and m ``` animation/ └── top/ - └── TopBtnEntranceAnimation.ts + └── TopBtnShowAnimation.ts ``` 画面ごとにサブディレクトリを作成し、その中にアニメーション定義ファイルを配置します。 @@ -24,7 +24,7 @@ Create subdirectories for each screen and place animation definition files withi ## アニメーションの種類 / Animation Types -### 入場アニメーション / Entrance Animation +### 登場アニメーション / Show Animation 画面表示時のアニメーションです。 @@ -44,39 +44,65 @@ Animation in response to user actions. ## 実装例 / Implementation Example -### TopBtnEntranceAnimation.ts +### TopBtnShowAnimation.ts ```typescript import type { TopBtnMolecule } from "@/ui/component/molecule/TopBtnMolecule"; -import { Tween } from "@next2d/framework"; +import { Tween, Easing, type Job } from "@next2d/ui"; +import { Event } from "@next2d/events"; /** - * @description Topボタンの入場アニメーション - * Entrance animation for Top button + * @description Topボタンの登場アニメーション + * Top Button Entrance Animation * - * @param {TopBtnMolecule} target - * @param {() => void} callback - * @return {void} + * @class + * @public */ -export const playEntrance = ( - target: TopBtnMolecule, - callback: () => void -): void => { - // 初期状態 - target.alpha = 0; - target.scaleX = 0.5; - target.scaleY = 0.5; - - // アニメーション - Tween.to(target, { - alpha: 1, - scaleX: 1, - scaleY: 1, - duration: 0.5, - ease: "easeOutBack", - onComplete: callback - }); -}; +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 @@ -89,15 +115,15 @@ Separate animation logic from components. ```typescript // ✅ 良い例: アニメーションを別ファイルに分離 -// animation/top/TopBtnEntranceAnimation.ts -export const playEntrance = (target, callback) => { ... }; +// animation/top/TopBtnShowAnimation.ts +export class TopBtnShowAnimation { ... } // component/molecule/TopBtnMolecule.ts -import { playEntrance } from "@/ui/animation/top/TopBtnEntranceAnimation"; +import { TopBtnShowAnimation } from "@/ui/animation/top/TopBtnShowAnimation"; export class TopBtnMolecule extends ButtonAtom { - playEntrance(callback: () => void): void { - playEntrance(this, callback); + playShow(callback: () => void): void { + new TopBtnShowAnimation(this, callback).start(); } } ``` @@ -109,11 +135,26 @@ export class TopBtnMolecule extends ButtonAtom { Make the same animation usable across multiple components. ```typescript -// ✅ 良い例: 汎用的なフェードインアニメーション -export const fadeIn = (target: DisplayObject, duration: number = 0.3): void => { - target.alpha = 0; - Tween.to(target, { alpha: 1, duration }); -}; +// ✅ 良い例: 汎用的なフェードインアニメーションクラス +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 @@ -123,15 +164,24 @@ export const fadeIn = (target: DisplayObject, duration: number = 0.3): void => { Support callbacks for when animation completes. ```typescript -export const playEntrance = ( - target: DisplayObject, - callback?: () => void -): void => { - Tween.to(target, { - // ... - onComplete: callback - }); -}; +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 @@ -146,40 +196,71 @@ export const playEntrance = ( ### テンプレート / Template ```typescript -import type { DisplayObject } from "@next2d/display"; -import { Tween } from "@next2d/framework"; +import type { Sprite } from "@next2d/display"; +import { Tween, Easing, type Job } from "@next2d/ui"; +import { Event } from "@next2d/events"; /** * @description [アニメーションの説明] * [Animation description] * - * @param {DisplayObject} target - アニメーション対象 - * @param {() => void} callback - 完了時コールバック - * @return {void} + * @class + * @public */ -export const yourAnimation = ( - target: DisplayObject, - callback?: () => void -): void => { - // 初期状態設定 - target.alpha = 0; - - // アニメーション実行 - Tween.to(target, { - alpha: 1, - duration: 0.5, - ease: "easeOutQuad", - onComplete: callback - }); -}; +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` の形式で命名 -3. **コールバック** - 完了時の処理をサポート -4. **再利用** - 汎用的なアニメーションは共通化 +2. **命名** - `{Component}{Action}Animation.ts` の形式で命名(例: TopBtnShowAnimation.ts) +3. **コールバック** - Event.COMPLETEで完了時の処理をサポート +4. **再利用** - 汎用的なアニメーションクラスは共通化 +5. **クラスベース** - Jobインスタンスを保持し、start()メソッドで開始 ## 関連ドキュメント / Related Documentation diff --git a/template/src/ui/animation/top/TopBtnEntranceAnimation.test.ts b/template/src/ui/animation/top/TopBtnEntranceAnimation.test.ts deleted file mode 100644 index 6073555..0000000 --- a/template/src/ui/animation/top/TopBtnEntranceAnimation.test.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { describe, it, expect, vi } from "vitest"; - -// 依存モジュールをモック -vi.mock("@next2d/ui", () => ({ - Tween: { - add: vi.fn().mockReturnValue({ - addEventListener: vi.fn(), - start: vi.fn() - }) - }, - Easing: { - inQuad: vi.fn() - } -})); - -vi.mock("@next2d/events", () => ({ - Event: { - COMPLETE: "complete" - } -})); - -import { TopBtnEntranceAnimation } from "./TopBtnEntranceAnimation"; -import { Tween } from "@next2d/ui"; - -/** - * @description TopBtnEntranceAnimation のテスト - * Tests for TopBtnEntranceAnimation - */ -describe("TopBtnEntranceAnimation", () => { - /** - * @description コンストラクタのテスト - * Test for constructor - */ - describe("Constructor / コンストラクタ", () => { - it("インスタンスが正常に生成されること", () => { - const mockSprite = { alpha: 1 }; - const callback = vi.fn(); - - const animation = new TopBtnEntranceAnimation(mockSprite as any, callback); - expect(animation).toBeInstanceOf(TopBtnEntranceAnimation); - }); - - it("sprite.alpha が 0 に設定されること", () => { - const mockSprite = { alpha: 1 }; - const callback = vi.fn(); - - new TopBtnEntranceAnimation(mockSprite as any, callback); - expect(mockSprite.alpha).toBe(0); - }); - - it("Tween.add が呼び出されること", () => { - const mockSprite = { alpha: 1 }; - const callback = vi.fn(); - - new TopBtnEntranceAnimation(mockSprite as any, callback); - expect(Tween.add).toHaveBeenCalled(); - }); - }); - - /** - * @description start メソッドのテスト - * Test for start method - */ - describe("start Method / start メソッド", () => { - it("start メソッドを持つこと", () => { - const mockSprite = { alpha: 1 }; - const callback = vi.fn(); - - const animation = new TopBtnEntranceAnimation(mockSprite as any, callback); - expect(typeof animation.start).toBe("function"); - }); - - it("start を呼び出せること", () => { - const mockSprite = { alpha: 1 }; - const callback = vi.fn(); - - const animation = new TopBtnEntranceAnimation(mockSprite as any, callback); - expect(() => animation.start()).not.toThrow(); - }); - }); - - /** - * @description アニメーション設定のテスト - * Test for animation settings - */ - describe("Animation Settings / アニメーション設定", () => { - it("alpha のアニメーションが設定されること", () => { - const mockSprite = { alpha: 1 }; - const callback = vi.fn(); - - new TopBtnEntranceAnimation(mockSprite as any, callback); - - expect(Tween.add).toHaveBeenCalledWith( - mockSprite, - expect.objectContaining({ alpha: 0 }), - expect.objectContaining({ alpha: 1 }), - expect.any(Number), - expect.any(Number), - expect.anything() - ); - }); - }); -}); diff --git a/template/src/ui/animation/top/TopBtnEntranceAnimation.ts b/template/src/ui/animation/top/TopBtnShowAnimation.ts similarity index 82% rename from template/src/ui/animation/top/TopBtnEntranceAnimation.ts rename to template/src/ui/animation/top/TopBtnShowAnimation.ts index 948600d..4642639 100644 --- a/template/src/ui/animation/top/TopBtnEntranceAnimation.ts +++ b/template/src/ui/animation/top/TopBtnShowAnimation.ts @@ -9,9 +9,9 @@ import { Event } from "@next2d/events"; * @class * @public */ -export class TopBtnEntranceAnimation { +export class TopBtnShowAnimation { - private readonly _entranceJob: Job; + private readonly _job: Job; /** * @param {TopBtnMolecule} sprite @@ -27,7 +27,7 @@ export class TopBtnEntranceAnimation { // アニメーションの初期値に設定 sprite.alpha = 0; - this._entranceJob = Tween.add(sprite, + this._job = Tween.add(sprite, { "alpha": 0 }, @@ -37,7 +37,7 @@ export class TopBtnEntranceAnimation { ); // 終了アニメーションが完了したら、完了イベントを発行 - this._entranceJob.addEventListener(Event.COMPLETE, (): void => + this._job.addEventListener(Event.COMPLETE, (): void => { callback(); }); @@ -51,6 +51,6 @@ export class TopBtnEntranceAnimation { * @public */ start(): void { - this._entranceJob.start(); + this._job.start(); } } \ No newline at end of file diff --git a/template/src/ui/component/atom/ButtonAtom.test.ts b/template/src/ui/component/atom/ButtonAtom.test.ts deleted file mode 100644 index 72779b3..0000000 --- a/template/src/ui/component/atom/ButtonAtom.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { describe, it, expect, vi } from "vitest"; - -// @next2d/display モジュールをモック -vi.mock("@next2d/display", () => ({ - Sprite: vi.fn().mockImplementation(function(this: any) { - this.buttonMode = false; - }) -})); - -import { ButtonAtom } from "./ButtonAtom"; - -/** - * @description ButtonAtom のテスト - * Tests for ButtonAtom - */ -describe("ButtonAtom", () => { - /** - * @description コンストラクタのテスト - * Test for constructor - */ - describe("Constructor / コンストラクタ", () => { - it("インスタンスが正常に生成されること", () => { - const button = new ButtonAtom(); - expect(button).toBeInstanceOf(ButtonAtom); - }); - - it("buttonMode が true に設定されること", () => { - const button = new ButtonAtom(); - expect(button.buttonMode).toBe(true); - }); - }); - - /** - * @description 継承関係のテスト - * Test for inheritance - */ - describe("Inheritance / 継承関係", () => { - it("Sprite を継承していること", () => { - const button = new ButtonAtom(); - // ButtonAtom は Sprite を継承している - expect(button).toBeInstanceOf(ButtonAtom); - }); - }); - - /** - * @description プロパティのテスト - * Test for properties - */ - describe("Properties / プロパティ", () => { - it("buttonMode プロパティが存在すること", () => { - const button = new ButtonAtom(); - expect("buttonMode" in button).toBe(true); - }); - - it("buttonMode を変更できること", () => { - const button = new ButtonAtom(); - button.buttonMode = false; - expect(button.buttonMode).toBe(false); - }); - }); -}); diff --git a/template/src/ui/component/atom/ButtonAtom.ts b/template/src/ui/component/atom/ButtonAtom.ts index 90c73dd..160e2fa 100644 --- a/template/src/ui/component/atom/ButtonAtom.ts +++ b/template/src/ui/component/atom/ButtonAtom.ts @@ -9,6 +9,7 @@ import { Sprite } from "@next2d/display"; * @public */ export class ButtonAtom extends Sprite { + /** * @description ボタンアトムを生成する * Create a button atom @@ -22,4 +23,30 @@ export class ButtonAtom extends Sprite { // ボタンモードを有効化する 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.test.ts b/template/src/ui/component/atom/TextAtom.test.ts deleted file mode 100644 index 6acfbb8..0000000 --- a/template/src/ui/component/atom/TextAtom.test.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { describe, it, expect, vi } from "vitest"; - -// @next2d/text モジュールをモック -vi.mock("@next2d/text", () => ({ - TextField: vi.fn().mockImplementation(function(this: any) { - this.text = ""; - this.width = 100; - this.x = 0; - this.defaultTextFormat = {}; - }) -})); - -import { TextAtom } from "./TextAtom"; - -/** - * @description TextAtom のテスト - * Tests for TextAtom - */ -describe("TextAtom", () => { - /** - * @description コンストラクタのテスト - * Test for constructor - */ - describe("Constructor / コンストラクタ", () => { - it("インスタンスが正常に生成されること", () => { - const textAtom = new TextAtom(); - expect(textAtom).toBeInstanceOf(TextAtom); - }); - - it("text 引数が設定されること", () => { - const textAtom = new TextAtom("Hello"); - expect(textAtom.text).toBe("Hello"); - }); - - it("デフォルトで空文字が設定されること", () => { - const textAtom = new TextAtom(); - expect(textAtom.text).toBe(""); - }); - }); - - /** - * @description props パラメータのテスト - * Test for props parameter - */ - describe("Props Parameter / props パラメータ", () => { - it("props が null でもエラーにならないこと", () => { - expect(() => new TextAtom("Test", null)).not.toThrow(); - }); - - it("props のプロパティが適用されること", () => { - const textAtom = new TextAtom("Test", { - autoSize: "center" - }); - // props が適用される(モックでは完全な検証は難しい) - expect(textAtom).toBeDefined(); - }); - }); - - /** - * @description format_object パラメータのテスト - * Test for format_object parameter - */ - describe("Format Object Parameter / format_object パラメータ", () => { - it("format_object が null でもエラーにならないこと", () => { - expect(() => new TextAtom("Test", null, null)).not.toThrow(); - }); - - it("format_object のプロパティが適用されること", () => { - const textAtom = new TextAtom("Test", null, { - align: "center", - bold: true, - color: 0x000000, - font: "Arial", - italic: false, - leading: 0, - leftMargin: 0, - letterSpacing: 0, - rightMargin: 0, - size: 16, - underline: false - }); - expect(textAtom).toBeDefined(); - }); - }); - - /** - * @description ITextField インターフェースのテスト - * Test for ITextField interface - */ - describe("ITextField Interface / ITextField インターフェース", () => { - it("width プロパティを持つこと", () => { - const textAtom = new TextAtom(); - expect("width" in textAtom).toBe(true); - }); - - it("x プロパティを持つこと", () => { - const textAtom = new TextAtom(); - expect("x" in textAtom).toBe(true); - }); - }); -}); diff --git a/template/src/ui/component/molecule/HomeBtnMolecule.test.ts b/template/src/ui/component/molecule/HomeBtnMolecule.test.ts deleted file mode 100644 index 4823ae0..0000000 --- a/template/src/ui/component/molecule/HomeBtnMolecule.test.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { describe, it, expect, vi } from "vitest"; - -// 依存モジュールをモック -vi.mock("@/ui/content/HomeContent", () => ({ - HomeContent: vi.fn().mockImplementation(function(this: any) { - this.scaleX = 1; - this.scaleY = 1; - this.startDrag = vi.fn(); - this.stopDrag = vi.fn(); - }) -})); - -vi.mock("@next2d/display", () => ({ - Sprite: vi.fn().mockImplementation(function(this: any) { - this.buttonMode = false; - this.addChild = vi.fn(); - }) -})); - -import { HomeBtnMolecule } from "./HomeBtnMolecule"; - -/** - * @description HomeBtnMolecule のテスト - * Tests for HomeBtnMolecule - */ -describe("HomeBtnMolecule", () => { - /** - * @description コンストラクタのテスト - * Test for constructor - */ - describe("Constructor / コンストラクタ", () => { - it("インスタンスが正常に生成されること", () => { - const molecule = new HomeBtnMolecule(); - expect(molecule).toBeInstanceOf(HomeBtnMolecule); - }); - - it("buttonMode が true に設定されること", () => { - const molecule = new HomeBtnMolecule(); - expect(molecule.buttonMode).toBe(true); - }); - }); - - /** - * @description IDraggable インターフェースのテスト - * Test for IDraggable interface - */ - describe("IDraggable Interface / IDraggable インターフェース", () => { - it("startDrag メソッドを持つこと", () => { - const molecule = new HomeBtnMolecule(); - expect(typeof molecule.startDrag).toBe("function"); - }); - - it("stopDrag メソッドを持つこと", () => { - const molecule = new HomeBtnMolecule(); - expect(typeof molecule.stopDrag).toBe("function"); - }); - - it("startDrag を呼び出せること", () => { - const molecule = new HomeBtnMolecule(); - expect(() => molecule.startDrag()).not.toThrow(); - }); - - it("stopDrag を呼び出せること", () => { - const molecule = new HomeBtnMolecule(); - expect(() => molecule.stopDrag()).not.toThrow(); - }); - }); - - /** - * @description ButtonAtom 継承のテスト - * Test for ButtonAtom inheritance - */ - describe("ButtonAtom Inheritance / ButtonAtom 継承", () => { - it("ButtonAtom を継承していること", () => { - const molecule = new HomeBtnMolecule(); - // ButtonAtom の機能を持っていること - expect("buttonMode" in molecule).toBe(true); - }); - }); -}); diff --git a/template/src/ui/component/molecule/HomeBtnMolecule.ts b/template/src/ui/component/molecule/HomeBtnMolecule.ts index e35f4bb..6bf69f1 100644 --- a/template/src/ui/component/molecule/HomeBtnMolecule.ts +++ b/template/src/ui/component/molecule/HomeBtnMolecule.ts @@ -1,6 +1,6 @@ import type { IDraggable } from "@/interface/IDraggable"; import { HomeContent } from "@/ui/content/HomeContent"; -import { ButtonAtom } from "../atom/ButtonAtom"; +import { ButtonAtom } from "@/ui/component/atom/ButtonAtom"; /** * @description Home画面のボタン分子 diff --git a/template/src/ui/component/molecule/TopBtnMolecule.test.ts b/template/src/ui/component/molecule/TopBtnMolecule.test.ts deleted file mode 100644 index f25dfd5..0000000 --- a/template/src/ui/component/molecule/TopBtnMolecule.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { describe, it, expect, vi } from "vitest"; - -// 依存モジュールをモック -vi.mock("@/ui/animation/top/TopBtnEntranceAnimation", () => ({ - TopBtnEntranceAnimation: vi.fn().mockImplementation(function(this: any) { - this.start = vi.fn(); - }) -})); - -vi.mock("@/ui/component/atom/TextAtom", () => ({ - TextAtom: vi.fn().mockImplementation(function(this: any, text: string) { - this.text = text; - this.width = 100; - this.height = 20; - this.x = 0; - this.y = 0; - }) -})); - -vi.mock("@next2d/display", () => ({ - Sprite: vi.fn().mockImplementation(function(this: any) { - this.buttonMode = false; - this.addChild = vi.fn(); - }) -})); - -import { TopBtnMolecule } from "./TopBtnMolecule"; - -/** - * @description TopBtnMolecule のテスト - * Tests for TopBtnMolecule - */ -describe("TopBtnMolecule", () => { - /** - * @description コンストラクタのテスト - * Test for constructor - */ - describe("Constructor / コンストラクタ", () => { - it("インスタンスが正常に生成されること", () => { - const molecule = new TopBtnMolecule("Test"); - expect(molecule).toBeInstanceOf(TopBtnMolecule); - }); - - it("text 引数を受け取ること", () => { - const molecule = new TopBtnMolecule("Hello"); - expect(molecule).toBeDefined(); - }); - - it("buttonMode が true に設定されること", () => { - const molecule = new TopBtnMolecule("Test"); - expect(molecule.buttonMode).toBe(true); - }); - }); - - /** - * @description playEntrance メソッドのテスト - * Test for playEntrance method - */ - describe("playEntrance Method / playEntrance メソッド", () => { - it("playEntrance メソッドを持つこと", () => { - const molecule = new TopBtnMolecule("Test"); - expect(typeof molecule.playEntrance).toBe("function"); - }); - - it("playEntrance がコールバックを受け取ること", () => { - const molecule = new TopBtnMolecule("Test"); - const callback = vi.fn(); - - expect(() => molecule.playEntrance(callback)).not.toThrow(); - }); - }); - - /** - * @description ButtonAtom 継承のテスト - * Test for ButtonAtom inheritance - */ - describe("ButtonAtom Inheritance / ButtonAtom 継承", () => { - it("ButtonAtom を継承していること", () => { - const molecule = new TopBtnMolecule("Test"); - expect("buttonMode" in molecule).toBe(true); - }); - }); -}); diff --git a/template/src/ui/component/molecule/TopBtnMolecule.ts b/template/src/ui/component/molecule/TopBtnMolecule.ts index 8a76914..5a139be 100644 --- a/template/src/ui/component/molecule/TopBtnMolecule.ts +++ b/template/src/ui/component/molecule/TopBtnMolecule.ts @@ -1,4 +1,4 @@ -import { TopBtnEntranceAnimation } from "@/ui/animation/top/TopBtnEntranceAnimation"; +import { TopBtnShowAnimation } from "@/ui/animation/top/TopBtnShowAnimation"; import { ButtonAtom } from "../atom/ButtonAtom"; import { TextAtom } from "../atom/TextAtom"; @@ -35,12 +35,13 @@ export class TopBtnMolecule extends ButtonAtom { * @description ボタンのアニメーションを再生 * Play button entrance animation * + * @param {() => void} callback * @return {void} * @method * @public */ - playEntrance (callback: () => void): void + show (callback: () => void): void { - new TopBtnEntranceAnimation(this, callback).start(); + new TopBtnShowAnimation(this, callback).start(); } } \ No newline at end of file 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 index e69de29..0c1edc4 100644 --- a/template/src/ui/component/page/top/TopPage.ts +++ 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/view/home/HomeView.test.ts b/template/src/view/home/HomeView.test.ts deleted file mode 100644 index a0d4872..0000000 --- a/template/src/view/home/HomeView.test.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; - -// 依存モジュールをモック -vi.mock("@next2d/framework", () => ({ - View: vi.fn().mockImplementation(function(this: any) { - this.addChild = vi.fn(); - }) -})); - -vi.mock("@/config/Config", () => ({ - config: { - stage: { - width: 240, - height: 240 - } - } -})); - -vi.mock("@/ui/component/molecule/HomeBtnMolecule", () => ({ - HomeBtnMolecule: vi.fn().mockImplementation(function(this: any) { - this.x = 0; - this.y = 0; - this.height = 100; - this.addEventListener = vi.fn(); - }) -})); - -vi.mock("@/ui/component/atom/TextAtom", () => ({ - TextAtom: vi.fn().mockImplementation(function(this: any, text: string) { - this.text = text; - this.x = 0; - this.y = 0; - this.width = 100; - this.height = 20; - this.addEventListener = vi.fn(); - }) -})); - -vi.mock("@next2d/events", () => ({ - PointerEvent: { - POINTER_DOWN: "pointerDown", - POINTER_UP: "pointerUp" - }, - Event: { - CHANGE: "change" - } -})); - -import { HomeView } from "./HomeView"; - -/** - * @description HomeView のテスト - * Tests for HomeView - */ -describe("HomeView", () => { - let mockViewModel: any; - - beforeEach(() => { - vi.clearAllMocks(); - mockViewModel = { - getHomeText: vi.fn().mockReturnValue("Hello"), - homeContentPointerDownEvent: vi.fn(), - homeContentPointerUpEvent: vi.fn(), - homeTextChangeEvent: vi.fn() - }; - }); - - /** - * @description コンストラクタのテスト - * Test for constructor - */ - describe("Constructor / コンストラクタ", () => { - it("インスタンスが正常に生成されること", () => { - const view = new HomeView(mockViewModel); - expect(view).toBeInstanceOf(HomeView); - }); - }); - - /** - * @description initialize メソッドのテスト - * Test for initialize method - */ - describe("initialize Method / initialize メソッド", () => { - it("initialize メソッドが存在すること", () => { - const view = new HomeView(mockViewModel); - expect(typeof view.initialize).toBe("function"); - }); - - it("initialize が非同期で実行されること", async () => { - const view = new HomeView(mockViewModel); - await expect(view.initialize()).resolves.toBeUndefined(); - }); - - it("ViewModel の getHomeText が呼び出されること", async () => { - const view = new HomeView(mockViewModel); - await view.initialize(); - expect(mockViewModel.getHomeText).toHaveBeenCalled(); - }); - }); - - /** - * @description onEnter メソッドのテスト - * Test for onEnter method - */ - describe("onEnter Method / onEnter メソッド", () => { - it("onEnter メソッドが存在すること", () => { - const view = new HomeView(mockViewModel); - expect(typeof view.onEnter).toBe("function"); - }); - - it("onEnter が非同期で実行されること", async () => { - const view = new HomeView(mockViewModel); - await expect(view.onEnter()).resolves.toBeUndefined(); - }); - }); - - /** - * @description onExit メソッドのテスト - * Test for onExit method - */ - describe("onExit Method / onExit メソッド", () => { - it("onExit メソッドが存在すること", () => { - const view = new HomeView(mockViewModel); - expect(typeof view.onExit).toBe("function"); - }); - - it("onExit が非同期で実行されること", async () => { - const view = new HomeView(mockViewModel); - await expect(view.onExit()).resolves.toBeUndefined(); - }); - }); -}); diff --git a/template/src/view/home/HomeView.ts b/template/src/view/home/HomeView.ts index 1f93df7..eb9057b 100644 --- a/template/src/view/home/HomeView.ts +++ b/template/src/view/home/HomeView.ts @@ -1,15 +1,19 @@ import type { HomeViewModel } from "./HomeViewModel"; import { View } from "@next2d/framework"; -import { config } from "@/config/Config"; -import { HomeBtnMolecule } from "@/ui/component/molecule/HomeBtnMolecule"; -import { TextAtom } from "@/ui/component/atom/TextAtom"; -import { PointerEvent, Event } from "@next2d/events"; +import { HomePage } from "@/ui/component/page/home/HomePage"; /** * @class * @extends {View} */ export class HomeView extends View { + + /** + * @private + * @readonly + */ + private readonly _homePage: HomePage; + /** * @param {HomeViewModel} vm * @constructor @@ -18,6 +22,9 @@ export class HomeView extends View { constructor (vm: HomeViewModel) { super(vm); + + this._homePage = new HomePage(); + this.addChild(this._homePage); } /** @@ -28,61 +35,7 @@ export class HomeView extends View { */ async initialize (): Promise { - /** - * ホームコンテンツの座標をセット - * 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, - this.vm.homeContentPointerDownEvent.bind(this.vm) - ); - homeContent.addEventListener(PointerEvent.POINTER_UP, - this.vm.homeContentPointerUpEvent.bind(this.vm) - ); - - /** - * ホームコンテンツを追加 - * Add home content - */ - this.addChild(homeContent); - - /** - * ホームテキストをViewModelから取得 - * Get home text from ViewModel - */ - const text = this.vm.getHomeText(); - const textField = new TextAtom(text, { - "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, - this.vm.homeTextChangeEvent.bind(this.vm) - ); - - /** - * ホームテキストを追加 - * Add home text - */ - this.addChild(textField); + this._homePage.initialize(this.vm); } /** diff --git a/template/src/view/home/HomeViewModel.test.ts b/template/src/view/home/HomeViewModel.test.ts deleted file mode 100644 index 0600e1c..0000000 --- a/template/src/view/home/HomeViewModel.test.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; - -// 依存モジュールをモック -vi.mock("@next2d/framework", () => ({ - ViewModel: vi.fn().mockImplementation(function(this: any) {}), - app: { - getResponse: vi.fn().mockReturnValue({ - has: vi.fn().mockReturnValue(true), - get: vi.fn().mockReturnValue({ word: "Hello, Next2D!" }) - }) - } -})); - -vi.mock("@/model/application/home/usecase/StartDragUseCase", () => ({ - StartDragUseCase: vi.fn().mockImplementation(function(this: any) { - this.execute = vi.fn(); - }) -})); - -vi.mock("@/model/application/home/usecase/StopDragUseCase", () => ({ - StopDragUseCase: vi.fn().mockImplementation(function(this: any) { - this.execute = vi.fn(); - }) -})); - -vi.mock("@/model/application/home/usecase/CenterTextFieldUseCase", () => ({ - CenterTextFieldUseCase: vi.fn().mockImplementation(function(this: any) { - this.execute = vi.fn(); - }) -})); - -vi.mock("@/config/Config", () => ({ - config: { - stage: { - width: 240 - } - } -})); - -import { HomeViewModel } from "./HomeViewModel"; -import { app } from "@next2d/framework"; - -/** - * @description HomeViewModel のテスト - * Tests for HomeViewModel - */ -describe("HomeViewModel", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - /** - * @description コンストラクタのテスト - * Test for constructor - */ - describe("Constructor / コンストラクタ", () => { - it("インスタンスが正常に生成されること", () => { - const vm = new HomeViewModel(); - expect(vm).toBeInstanceOf(HomeViewModel); - }); - }); - - /** - * @description initialize メソッドのテスト - * Test for initialize method - */ - describe("initialize Method / initialize メソッド", () => { - it("initialize メソッドが存在すること", () => { - const vm = new HomeViewModel(); - expect(typeof vm.initialize).toBe("function"); - }); - - it("initialize が非同期で実行されること", async () => { - const vm = new HomeViewModel(); - await expect(vm.initialize()).resolves.toBeUndefined(); - }); - - it("app.getResponse が呼び出されること", async () => { - const vm = new HomeViewModel(); - await vm.initialize(); - expect(app.getResponse).toHaveBeenCalled(); - }); - }); - - /** - * @description getHomeText メソッドのテスト - * Test for getHomeText method - */ - describe("getHomeText Method / getHomeText メソッド", () => { - it("getHomeText メソッドが存在すること", () => { - const vm = new HomeViewModel(); - expect(typeof vm.getHomeText).toBe("function"); - }); - - it("初期化前は空文字を返すこと", () => { - const vm = new HomeViewModel(); - expect(vm.getHomeText()).toBe(""); - }); - - it("初期化後はテキストを返すこと", async () => { - const vm = new HomeViewModel(); - await vm.initialize(); - expect(vm.getHomeText()).toBe("Hello, Next2D!"); - }); - }); - - /** - * @description homeContentPointerDownEvent メソッドのテスト - * Test for homeContentPointerDownEvent method - */ - describe("homeContentPointerDownEvent Method", () => { - it("homeContentPointerDownEvent メソッドが存在すること", () => { - const vm = new HomeViewModel(); - expect(typeof vm.homeContentPointerDownEvent).toBe("function"); - }); - - it("イベントを受け取れること", () => { - const vm = new HomeViewModel(); - const mockEvent = { - currentTarget: { - startDrag: vi.fn(), - stopDrag: vi.fn() - } - }; - - expect(() => vm.homeContentPointerDownEvent(mockEvent as any)).not.toThrow(); - }); - }); - - /** - * @description homeContentPointerUpEvent メソッドのテスト - * Test for homeContentPointerUpEvent method - */ - describe("homeContentPointerUpEvent Method", () => { - it("homeContentPointerUpEvent メソッドが存在すること", () => { - const vm = new HomeViewModel(); - expect(typeof vm.homeContentPointerUpEvent).toBe("function"); - }); - - it("イベントを受け取れること", () => { - const vm = new HomeViewModel(); - const mockEvent = { - currentTarget: { - startDrag: vi.fn(), - stopDrag: vi.fn() - } - }; - - expect(() => vm.homeContentPointerUpEvent(mockEvent as any)).not.toThrow(); - }); - }); - - /** - * @description homeTextChangeEvent メソッドのテスト - * Test for homeTextChangeEvent method - */ - describe("homeTextChangeEvent Method", () => { - it("homeTextChangeEvent メソッドが存在すること", () => { - const vm = new HomeViewModel(); - expect(typeof vm.homeTextChangeEvent).toBe("function"); - }); - - it("イベントを受け取れること", () => { - const vm = new HomeViewModel(); - const mockEvent = { - currentTarget: { - width: 100, - x: 0 - } - }; - - expect(() => vm.homeTextChangeEvent(mockEvent as any)).not.toThrow(); - }); - }); -}); diff --git a/template/src/view/top/TopView.test.ts b/template/src/view/top/TopView.test.ts deleted file mode 100644 index 307dd79..0000000 --- a/template/src/view/top/TopView.test.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; - -// 依存モジュールをモック -vi.mock("@next2d/framework", () => ({ - View: vi.fn().mockImplementation(function(this: any) { - this.addChild = vi.fn(); - this.getChildByName = vi.fn(); - }) -})); - -vi.mock("@/config/Config", () => ({ - config: { - stage: { - width: 240, - height: 240 - } - } -})); - -vi.mock("@/ui/component/molecule/TopBtnMolecule", () => ({ - TopBtnMolecule: vi.fn().mockImplementation(function(this: any, text: string) { - this.name = ""; - this.x = 0; - this.y = 0; - this.height = 20; - this.mouseChildren = true; - this.mouseEnabled = true; - this.addEventListener = vi.fn(); - this.playEntrance = vi.fn(); - }) -})); - -vi.mock("@/ui/content/TopContent", () => ({ - TopContent: vi.fn().mockImplementation(function(this: any) { - this.x = 0; - this.y = 0; - this.height = 100; - }) -})); - -vi.mock("@next2d/events", () => ({ - PointerEvent: { - POINTER_UP: "pointerUp" - } -})); - -import { TopView } from "./TopView"; - -/** - * @description TopView のテスト - * Tests for TopView - */ -describe("TopView", () => { - let mockViewModel: any; - - beforeEach(() => { - vi.clearAllMocks(); - mockViewModel = { - getTopText: vi.fn().mockReturnValue("Start"), - onClickStartButton: vi.fn().mockResolvedValue(undefined) - }; - }); - - /** - * @description コンストラクタのテスト - * Test for constructor - */ - describe("Constructor / コンストラクタ", () => { - it("インスタンスが正常に生成されること", () => { - const view = new TopView(mockViewModel); - expect(view).toBeInstanceOf(TopView); - }); - }); - - /** - * @description initialize メソッドのテスト - * Test for initialize method - */ - describe("initialize Method / initialize メソッド", () => { - it("initialize メソッドが存在すること", () => { - const view = new TopView(mockViewModel); - expect(typeof view.initialize).toBe("function"); - }); - - it("initialize が非同期で実行されること", async () => { - const view = new TopView(mockViewModel); - await expect(view.initialize()).resolves.toBeUndefined(); - }); - - it("ViewModel の getTopText が呼び出されること", async () => { - const view = new TopView(mockViewModel); - await view.initialize(); - expect(mockViewModel.getTopText).toHaveBeenCalled(); - }); - }); - - /** - * @description onEnter メソッドのテスト - * Test for onEnter method - */ - describe("onEnter Method / onEnter メソッド", () => { - it("onEnter メソッドが存在すること", () => { - const view = new TopView(mockViewModel); - expect(typeof view.onEnter).toBe("function"); - }); - - it("onEnter が非同期で実行されること", async () => { - const view = new TopView(mockViewModel); - await expect(view.onEnter()).resolves.toBeUndefined(); - }); - }); - - /** - * @description onExit メソッドのテスト - * Test for onExit method - */ - describe("onExit Method / onExit メソッド", () => { - it("onExit メソッドが存在すること", () => { - const view = new TopView(mockViewModel); - expect(typeof view.onExit).toBe("function"); - }); - - it("onExit が非同期で実行されること", async () => { - const view = new TopView(mockViewModel); - await expect(view.onExit()).resolves.toBeUndefined(); - }); - }); -}); diff --git a/template/src/view/top/TopView.ts b/template/src/view/top/TopView.ts index c2c68e3..8cc2457 100644 --- a/template/src/view/top/TopView.ts +++ b/template/src/view/top/TopView.ts @@ -1,9 +1,6 @@ import type { TopViewModel } from "./TopViewModel"; import { View } from "@next2d/framework"; -import { TopBtnMolecule } from "@/ui/component/molecule/TopBtnMolecule"; -import { config } from "@/config/Config"; -import { PointerEvent } from "@next2d/events"; -import { TopContent } from "@/ui/content/TopContent"; +import { TopPage } from "@/ui/component/page/top/TopPage"; /** * @class @@ -11,6 +8,12 @@ import { TopContent } from "@/ui/content/TopContent"; */ export class TopView extends View { + /** + * @private + * @readonly + */ + private readonly _topPage: TopPage; + /** * @param {TopViewModel} vm * @constructor @@ -19,6 +22,9 @@ export class TopView extends View { constructor (vm: TopViewModel) { super(vm); + + this._topPage = new TopPage(); + this.addChild(this._topPage); } /** @@ -29,47 +35,7 @@ export class TopView extends View { */ async initialize (): Promise { - /** - * ロゴアニメーションを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; - - this.addChild(topContent); - - /** - * Topボタンを生成して、座標をセット - * Create Top button and set coordinates - */ - const topBtn = new TopBtnMolecule(this.vm.getTopText()); - topBtn.name = "topBtn"; - topBtn.x = config.stage.width / 2; - topBtn.y = config.stage.height / 2 + topContent.height / 2 + topBtn.height; - - /** - * アニメーションが完了するまでボタンを無効化 - * Disable button until animation is complete - */ - topBtn.mouseChildren = false; - topBtn.mouseEnabled = false; - - /** - * ボタンのクリックイベントをViewModelに送信 - * Send button click event to ViewModel - */ - topBtn.addEventListener(PointerEvent.POINTER_UP, async (): Promise => - { - await this.vm.onClickStartButton(); - }); - - /** - * Topボタンを画面に追加 - * Add Top button to the screen - */ - this.addChild(topBtn); + this._topPage.initialize(this.vm); } /** @@ -80,17 +46,7 @@ export class TopView extends View { */ async onEnter (): Promise { - const topBtn = this.getChildByName("topBtn"); - if (!topBtn) { - return; - } - topBtn.playEntrance(() => - { - // アニメーション完了後にボタンを有効化 - // Enable button after animation is complete - topBtn.mouseChildren = true; - topBtn.mouseEnabled = true; - }); + await this._topPage.onEnter(); } /** diff --git a/template/src/view/top/TopViewModel.test.ts b/template/src/view/top/TopViewModel.test.ts deleted file mode 100644 index 373a78d..0000000 --- a/template/src/view/top/TopViewModel.test.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; - -// 依存モジュールをモック -vi.mock("@next2d/framework", () => ({ - ViewModel: vi.fn().mockImplementation(function(this: any) {}), - app: { - getResponse: vi.fn().mockReturnValue({ - has: vi.fn().mockReturnValue(true), - get: vi.fn().mockReturnValue({ word: "Start" }) - }) - } -})); - -vi.mock("@/model/application/top/usecase/NavigateToViewUseCase", () => ({ - NavigateToViewUseCase: vi.fn().mockImplementation(function(this: any) { - this.execute = vi.fn().mockResolvedValue(undefined); - }) -})); - -import { TopViewModel } from "./TopViewModel"; -import { app } from "@next2d/framework"; - -/** - * @description TopViewModel のテスト - * Tests for TopViewModel - */ -describe("TopViewModel", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - /** - * @description コンストラクタのテスト - * Test for constructor - */ - describe("Constructor / コンストラクタ", () => { - it("インスタンスが正常に生成されること", () => { - const vm = new TopViewModel(); - expect(vm).toBeInstanceOf(TopViewModel); - }); - }); - - /** - * @description initialize メソッドのテスト - * Test for initialize method - */ - describe("initialize Method / initialize メソッド", () => { - it("initialize メソッドが存在すること", () => { - const vm = new TopViewModel(); - expect(typeof vm.initialize).toBe("function"); - }); - - it("initialize が非同期で実行されること", async () => { - const vm = new TopViewModel(); - await expect(vm.initialize()).resolves.toBeUndefined(); - }); - - it("app.getResponse が呼び出されること", async () => { - const vm = new TopViewModel(); - await vm.initialize(); - expect(app.getResponse).toHaveBeenCalled(); - }); - }); - - /** - * @description getTopText メソッドのテスト - * Test for getTopText method - */ - describe("getTopText Method / getTopText メソッド", () => { - it("getTopText メソッドが存在すること", () => { - const vm = new TopViewModel(); - expect(typeof vm.getTopText).toBe("function"); - }); - - it("初期化前は空文字を返すこと", () => { - const vm = new TopViewModel(); - expect(vm.getTopText()).toBe(""); - }); - - it("初期化後はテキストを返すこと", async () => { - const vm = new TopViewModel(); - await vm.initialize(); - expect(vm.getTopText()).toBe("Start"); - }); - }); - - /** - * @description onClickStartButton メソッドのテスト - * Test for onClickStartButton method - */ - describe("onClickStartButton Method / onClickStartButton メソッド", () => { - it("onClickStartButton メソッドが存在すること", () => { - const vm = new TopViewModel(); - expect(typeof vm.onClickStartButton).toBe("function"); - }); - - it("onClickStartButton が非同期で実行されること", async () => { - const vm = new TopViewModel(); - await expect(vm.onClickStartButton()).resolves.toBeUndefined(); - }); - }); -}); From 873bfc52b4a3614155ec4e711dc8b9c481f48247 Mon Sep 17 00:00:00 2001 From: ienaga Date: Sun, 25 Jan 2026 00:06:45 +0900 Subject: [PATCH 33/35] =?UTF-8?q?#56=20*.md=E3=82=92=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/README.md | 41 +++++++++++++++++++ template/src/model/README.md | 2 +- template/src/ui/README.md | 17 ++++---- template/src/ui/component/README.md | 44 +++++++++++++++++---- template/src/ui/component/organism/.gitkeep | 0 template/src/ui/component/template/.gitkeep | 0 template/src/ui/content/README.md | 24 +++-------- 7 files changed, 92 insertions(+), 36 deletions(-) create mode 100644 template/src/ui/component/organism/.gitkeep create mode 100644 template/src/ui/component/template/.gitkeep diff --git a/template/README.md b/template/README.md index dfb17c0..fac6a0c 100644 --- a/template/README.md +++ b/template/README.md @@ -17,6 +17,7 @@ This project was bootstrapped with [Create Next2D App](https://github.com/Next2D - [ユニットテスト / Unit Test](#ユニットテスト--unit-test) - [ビルド / Build](#ビルド--build) - [ディレクトリ構成 / Directory Structure](#ディレクトリ構成--directory-structure) +- [📚 詳細ドキュメント / Detailed Documentation](#-詳細ドキュメント--detailed-documentation) - [ライセンス / License](#ライセンス--license) --- @@ -232,6 +233,46 @@ 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 MIT License diff --git a/template/src/model/README.md b/template/src/model/README.md index 48001fc..6be37b1 100644 --- a/template/src/model/README.md +++ b/template/src/model/README.md @@ -19,8 +19,8 @@ model/ │ └── NavigateToViewUseCase.ts ├── domain/ # ドメイン層 │ └── callback/ +│ ├── Background.ts # コールバッククラス本体 │ └── Background/ -│ ├── Background.ts │ └── service/ │ ├── BackgroundDrawService.ts │ └── BackgroundChangeScaleService.ts diff --git a/template/src/ui/README.md b/template/src/ui/README.md index 18b4ede..da522d2 100644 --- a/template/src/ui/README.md +++ b/template/src/ui/README.md @@ -11,7 +11,10 @@ ui/ ├── animation/ # アニメーション定義 ├── component/ │ ├── atom/ # 最小単位 -│ └── molecule/ # 複合コンポーネント +│ ├── molecule/ # 複合コンポーネント +│ ├── organism/ # 複数Moleculeの組み合わせ(将来の拡張用) +│ ├── page/ # ページコンポーネント +│ └── template/ # ページテンプレート(将来の拡張用) └── content/ # Animation Tool ``` @@ -83,15 +86,13 @@ export class HomeBtnMolecule extends ButtonAtom implements IDraggable { this.homeContent = new HomeContent(); this.addChild(this.homeContent); } - - startDrag(): void { ... } - stopDrag(): void { ... } + // IDraggableメソッド(startDrag/stopDrag)はMovieClipContentの親クラスから継承 } ``` **特徴 / Features:** - `ButtonAtom` を継承 -- `IDraggable` インターフェースを実装 +- `IDraggable` インターフェースを実装(メソッドは`MovieClipContent`親クラスから継承) - ドラッグ&ドロップ機能を提供 #### component/molecule/TopBtnMolecule.ts @@ -195,10 +196,10 @@ export class TextAtom extends TextField { Depend on abstractions, not concretions. ```typescript -// ✅ 良い例: インターフェースを実装 +// ✅ 良い例: インターフェースを実装し、内部のContentに委譲 export class HomeBtnMolecule extends ButtonAtom implements IDraggable { - startDrag(): void { ... } - stopDrag(): void { ... } + private readonly homeContent: HomeContent; + // IDraggableメソッド(startDrag/stopDrag)はMovieClipContentの親クラスから継承 } ``` diff --git a/template/src/ui/component/README.md b/template/src/ui/component/README.md index d57b2bc..c3bfeeb 100644 --- a/template/src/ui/component/README.md +++ b/template/src/ui/component/README.md @@ -11,9 +11,16 @@ component/ ├── atom/ # 最小単位 │ ├── ButtonAtom.ts │ └── TextAtom.ts -└── molecule/ # 複合コンポーネント - ├── HomeBtnMolecule.ts - └── TopBtnMolecule.ts +├── molecule/ # 複合コンポーネント +│ ├── HomeBtnMolecule.ts +│ └── TopBtnMolecule.ts +├── organism/ # 複数Moleculeの組み合わせ(将来の拡張用) +├── page/ # ページコンポーネント +│ ├── home/ +│ │ └── HomePage.ts +│ └── top/ +│ └── TopPage.ts +└── template/ # ページテンプレート(将来の拡張用) ``` ## アトミックデザイン階層 / Atomic Design Hierarchy @@ -33,9 +40,30 @@ The most basic UI elements. The smallest components that cannot be divided furth Components with more complex functionality, combining multiple Atoms. -- `HomeBtnMolecule` - Home画面用のボタン(ドラッグ機能付き) +- `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 @@ -61,10 +89,10 @@ export class TextAtom extends TextField { Depend on abstractions, not concretions. ```typescript -// ✅ 良い例 -export class HomeBtnMolecule implements IDraggable { - startDrag(): void { ... } - stopDrag(): void { ... } +// ✅ 良い例: IDraggableを実装し、内部のContentに委譲 +export class HomeBtnMolecule extends ButtonAtom implements IDraggable { + private readonly homeContent: HomeContent; + // IDraggableメソッドはMovieClipContent(HomeContent)の親クラスから継承 } ``` 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/template/.gitkeep b/template/src/ui/component/template/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/template/src/ui/content/README.md b/template/src/ui/content/README.md index a6502ae..1210ab6 100644 --- a/template/src/ui/content/README.md +++ b/template/src/ui/content/README.md @@ -68,23 +68,10 @@ export class HomeContent extends MovieClipContent implements IDraggable return "HomeContent"; // Animation Toolで設定したシンボル名 } - /** - * @description ドラッグを開始 - * Start dragging - */ - startDrag (): void - { - // ドラッグ処理 - } - - /** - * @description ドラッグを停止 - * Stop dragging - */ - stopDrag (): void - { - // ドラッグ停止処理 - } + // IDraggableのメソッド(startDrag/stopDrag)は + // MovieClipContentの親クラス(MovieClip)から継承されます + // The IDraggable methods (startDrag/stopDrag) are inherited + // from MovieClipContent's parent class (MovieClip) } ``` @@ -154,8 +141,7 @@ Implement interfaces as needed to integrate with the business logic layer. ```typescript export class HomeContent extends MovieClipContent implements IDraggable { - startDrag(): void { ... } - stopDrag(): void { ... } + // IDraggableメソッドはMovieClipContentの親クラスから継承 } ``` From 1667615ec675033dcffa4b8148f9ec17539ebd33 Mon Sep 17 00:00:00 2001 From: ienaga Date: Fri, 6 Feb 2026 16:35:27 +0900 Subject: [PATCH 34/35] #56 update packages --- .github/workflows/publish.yml | 14 +++++++------- template/package.json | 18 +++++++++--------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e2e4ac1..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@v6 - uses: actions/setup-node@v6 with: 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/template/package.json b/template/package.json index 83b527b..399b10c 100644 --- a/template/package.json +++ b/template/package.json @@ -23,21 +23,21 @@ "generate": "npx @next2d/view-generator" }, "devDependencies": { - "@capacitor/android": "^8.0.1", - "@capacitor/cli": "^8.0.1", - "@capacitor/core": "^8.0.1", - "@capacitor/ios": "^8.0.1", + "@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.0.10", - "@typescript-eslint/eslint-plugin": "^8.53.1", - "@typescript-eslint/parser": "^8.53.1", + "@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": "^17.1.0", - "jsdom": "^27.4.0", + "globals": "^17.3.0", + "jsdom": "^28.0.0", "typescript": "^5.9.3", "vite": "^7.3.1", "vite-tsconfig-paths": "^6.0.5", From 35d84d72e6f9c0182c8e9af0af5be2ce9b7b7694 Mon Sep 17 00:00:00 2001 From: ienaga Date: Fri, 6 Feb 2026 17:05:06 +0900 Subject: [PATCH 35/35] =?UTF-8?q?#56=20AI=E3=81=AESkills=E3=81=AB=E5=90=91?= =?UTF-8?q?=E3=81=91=E3=81=9Fspecs=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/specs/commands.md | 49 ++++ template/specs/config.md | 198 ++++++++++++++++ template/specs/interface.md | 133 +++++++++++ template/specs/model.md | 374 +++++++++++++++++++++++++++++++ template/specs/overview.md | 94 ++++++++ template/specs/ui.md | 328 +++++++++++++++++++++++++++ template/specs/view-viewmodel.md | 216 ++++++++++++++++++ 7 files changed, 1392 insertions(+) create mode 100644 template/specs/commands.md create mode 100644 template/specs/config.md create mode 100644 template/specs/interface.md create mode 100644 template/specs/model.md create mode 100644 template/specs/overview.md create mode 100644 template/specs/ui.md create mode 100644 template/specs/view-viewmodel.md 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); +} +```