diff --git a/frontend/apps/artcraft/app/public/resources/images/floating-cubes.webp b/frontend/apps/artcraft/app/public/resources/images/floating-cubes.webp deleted file mode 100644 index 93249331..00000000 Binary files a/frontend/apps/artcraft/app/public/resources/images/floating-cubes.webp and /dev/null differ diff --git a/frontend/apps/artcraft/app/public/resources/images/room-of-items.webp b/frontend/apps/artcraft/app/public/resources/images/room-of-items.webp deleted file mode 100644 index 9e3c0892..00000000 Binary files a/frontend/apps/artcraft/app/public/resources/images/room-of-items.webp and /dev/null differ diff --git a/frontend/apps/artcraft/app/public/resources/logo/artcraft-logo-color-black.svg b/frontend/apps/artcraft/app/public/resources/logo/artcraft-logo-color-black.svg deleted file mode 100644 index e28a3ff4..00000000 --- a/frontend/apps/artcraft/app/public/resources/logo/artcraft-logo-color-black.svg +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/frontend/apps/artcraft/app/public/resources/logo/artcraft-logo-color-white.svg b/frontend/apps/artcraft/app/public/resources/logo/artcraft-logo-color-white.svg deleted file mode 100644 index 7906a3eb..00000000 --- a/frontend/apps/artcraft/app/public/resources/logo/artcraft-logo-color-white.svg +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/frontend/apps/artcraft/app/public/resources/pose/7.vpd b/frontend/apps/artcraft/app/public/resources/pose/7.vpd deleted file mode 100644 index b55ef36d..00000000 --- a/frontend/apps/artcraft/app/public/resources/pose/7.vpd +++ /dev/null @@ -1,305 +0,0 @@ -Vocaloid Pose Data file - -–؂̐lŒ`.osm; // �eƒtƒ@ƒCƒ‹–¼ -60; // ‘�ƒ|�[ƒYƒ{�[ƒ“�” - -Bone0{‘S‚Ă̐e - 0.000000,0.000000,0.000000; // trans x,y,z - 0.000000,0.000000,0.000000,1.000000; // Quaternion x,y,z,w -} - -Bone1{ƒZƒ“ƒ^�[ - 0.000000,0.000000,0.000000; // trans x,y,z - 0.000000,0.000000,0.000000,1.000000; // Quaternion x,y,z,w -} - -Bone2{�㔼�g - 0.000000,0.000000,0.000000; // trans x,y,z - 0.000000,0.000000,0.000000,1.000000; // Quaternion x,y,z,w -} - -Bone3{Žñ - 0.000000,0.000000,0.000000; // trans x,y,z - -0.000286,0.002004,-0.028579,0.999590; // Quaternion x,y,z,w -} - -Bone4{“ª - 0.000000,0.000000,0.000000; // trans x,y,z - 0.003903,-0.047796,-0.030391,0.998387; // Quaternion x,y,z,w -} - -Bone5{‰º”¼�g - 0.000000,0.000000,0.000000; // trans x,y,z - -0.000950,0.006664,-0.095038,0.995451; // Quaternion x,y,z,w -} - -Bone6{�¶Œ¨P - -0.000000,0.000000,0.000000; // trans x,y,z - 0.000000,-0.000000,-0.000000,1.000000; // Quaternion x,y,z,w -} - -Bone7{�¶Œ¨ - -0.000000,0.000000,0.000000; // trans x,y,z - 0.003159,-0.000000,0.105260,0.994440; // Quaternion x,y,z,w -} - -Bone8{�¶˜r - -0.000000,0.000000,0.000000; // trans x,y,z - 0.034463,0.034156,-0.396750,0.916644; // Quaternion x,y,z,w -} - -Bone9{�¶˜r�€ - 0.000000,0.000000,0.000000; // trans x,y,z - 0.000000,0.000000,0.000000,1.000000; // Quaternion x,y,z,w -} - -Bone10{�¶‚Ђ¶ - -0.000000,0.000000,0.000000; // trans x,y,z - 0.014712,-0.013995,0.076793,0.996840; // Quaternion x,y,z,w -} - -Bone11{�¶Ž蝀 - 0.000000,0.000000,0.000000; // trans x,y,z - 0.000000,0.000000,0.000000,1.000000; // Quaternion x,y,z,w -} - -Bone12{�¶ŽèŽñ - -0.000000,0.000000,0.000000; // trans x,y,z - 0.028156,-0.026783,0.146962,0.988378; // Quaternion x,y,z,w -} - -Bone13{�¶�eŽw‚P - 0.000000,0.000000,0.000000; // trans x,y,z - 0.002261,-0.000573,-0.306446,0.951885; // Quaternion x,y,z,w -} - -Bone14{�¶�eŽw‚Q - 0.000000,0.000000,0.000000; // trans x,y,z - 0.047385,-0.118953,0.062146,0.989819; // Quaternion x,y,z,w -} - -Bone15{�¶�lŽw‚P - 0.000000,0.000000,0.000000; // trans x,y,z - 0.020755,0.017947,0.064537,0.997538; // Quaternion x,y,z,w -} - -Bone16{�¶�lŽw‚Q - 0.000000,0.000000,0.000000; // trans x,y,z - 0.000000,0.000000,0.024997,0.999687; // Quaternion x,y,z,w -} - -Bone17{�¶�lŽw‚R - 0.000000,0.000000,0.000000; // trans x,y,z - 0.000000,0.000000,0.029995,0.999550; // Quaternion x,y,z,w -} - -Bone18{�¶’†Žw‚P - 0.000000,0.000000,0.000000; // trans x,y,z - -0.000000,-0.000000,-0.064954,0.997888; // Quaternion x,y,z,w -} - -Bone19{�¶’†Žw‚Q - 0.000000,0.000000,0.000000; // trans x,y,z - -0.000000,-0.000000,-0.054972,0.998488; // Quaternion x,y,z,w -} - -Bone20{�¶’†Žw‚R - 0.000000,0.000000,0.000000; // trans x,y,z - -0.000000,0.000000,0.104807,0.994492; // Quaternion x,y,z,w -} - -Bone21{�¶–òŽw‚P - 0.000000,0.000000,0.000000; // trans x,y,z - 0.000000,-0.000000,-0.232843,0.972514; // Quaternion x,y,z,w -} - -Bone22{�¶–òŽw‚Q - 0.000000,0.000000,0.000000; // trans x,y,z - 0.000000,-0.000000,-0.188859,0.982004; // Quaternion x,y,z,w -} - -Bone23{�¶–òŽw‚R - 0.000000,0.000000,0.000000; // trans x,y,z - -0.000000,0.000000,0.124675,0.992197; // Quaternion x,y,z,w -} - -Bone24{�¶�¬Žw‚P - 0.000000,0.000000,0.000000; // trans x,y,z - -0.000000,-0.000000,-0.094857,0.995490; // Quaternion x,y,z,w -} - -Bone25{�¶�¬Žw‚Q - 0.000000,0.000000,0.000000; // trans x,y,z - 0.000000,-0.000000,-0.496880,0.867819; // Quaternion x,y,z,w -} - -Bone26{�¶�¬Žw‚R - 0.000000,0.000000,0.000000; // trans x,y,z - -0.000000,-0.000000,0.010000,0.999949; // Quaternion x,y,z,w -} - -Bone27{‰EŒ¨P - 0.000000,0.000000,0.000000; // trans x,y,z - 0.000000,0.000000,0.000000,1.000000; // Quaternion x,y,z,w -} - -Bone28{‰EŒ¨ - 0.000000,0.000000,0.000000; // trans x,y,z - 0.003159,0.000000,-0.105260,0.994440; // Quaternion x,y,z,w -} - -Bone29{‰E˜r - 0.000000,0.000000,0.000000; // trans x,y,z - 0.034463,-0.034156,0.396750,0.916644; // Quaternion x,y,z,w -} - -Bone30{‰E˜r�€ - 0.000000,0.000000,0.000000; // trans x,y,z - 0.000000,0.000000,0.000000,1.000000; // Quaternion x,y,z,w -} - -Bone31{‰E‚Ђ¶ - 0.000000,0.000000,0.000000; // trans x,y,z - 0.014712,0.013995,-0.076793,0.996840; // Quaternion x,y,z,w -} - -Bone32{‰EŽ蝀 - 0.000000,0.000000,0.000000; // trans x,y,z - 0.000000,0.000000,0.000000,1.000000; // Quaternion x,y,z,w -} - -Bone33{‰EŽèŽñ - 0.000000,0.000000,0.000000; // trans x,y,z - 0.028156,0.026783,-0.146962,0.988378; // Quaternion x,y,z,w -} - -Bone34{‰E�eŽw‚P - -0.000000,0.000000,0.000000; // trans x,y,z - 0.002261,0.000573,0.306446,0.951885; // Quaternion x,y,z,w -} - -Bone35{‰E�eŽw‚Q - -0.000000,0.000000,0.000000; // trans x,y,z - 0.047385,0.118953,-0.062146,0.989819; // Quaternion x,y,z,w -} - -Bone36{‰E�lŽw‚P - -0.000000,0.000000,0.000000; // trans x,y,z - 0.020755,-0.017947,-0.064537,0.997538; // Quaternion x,y,z,w -} - -Bone37{‰E�lŽw‚Q - -0.000000,0.000000,0.000000; // trans x,y,z - 0.000000,-0.000000,-0.024997,0.999687; // Quaternion x,y,z,w -} - -Bone38{‰E�lŽw‚R - -0.000000,0.000000,0.000000; // trans x,y,z - 0.000000,-0.000000,-0.029995,0.999550; // Quaternion x,y,z,w -} - -Bone39{‰E’†Žw‚P - -0.000000,0.000000,0.000000; // trans x,y,z - -0.000000,0.000000,0.064954,0.997888; // Quaternion x,y,z,w -} - -Bone40{‰E’†Žw‚Q - -0.000000,0.000000,0.000000; // trans x,y,z - -0.000000,0.000000,0.054972,0.998488; // Quaternion x,y,z,w -} - -Bone41{‰E’†Žw‚R - -0.000000,0.000000,0.000000; // trans x,y,z - -0.000000,-0.000000,-0.104807,0.994492; // Quaternion x,y,z,w -} - -Bone42{‰E–òŽw‚P - -0.000000,0.000000,0.000000; // trans x,y,z - 0.000000,0.000000,0.232843,0.972514; // Quaternion x,y,z,w -} - -Bone43{‰E–òŽw‚Q - -0.000000,0.000000,0.000000; // trans x,y,z - 0.000000,0.000000,0.188859,0.982004; // Quaternion x,y,z,w -} - -Bone44{‰E–òŽw‚R - -0.000000,0.000000,0.000000; // trans x,y,z - -0.000000,-0.000000,-0.124675,0.992197; // Quaternion x,y,z,w -} - -Bone45{‰E�¬Žw‚P - -0.000000,0.000000,0.000000; // trans x,y,z - -0.000000,0.000000,0.094857,0.995490; // Quaternion x,y,z,w -} - -Bone46{‰E�¬Žw‚Q - -0.000000,0.000000,0.000000; // trans x,y,z - 0.000000,0.000000,0.496880,0.867819; // Quaternion x,y,z,w -} - -Bone47{‰E�¬Žw‚R - -0.000000,0.000000,0.000000; // trans x,y,z - -0.000000,0.000000,-0.010000,0.999949; // Quaternion x,y,z,w -} - -Bone48{�¶‘« - 0.000000,0.000000,0.000000; // trans x,y,z - 0.000000,0.000000,0.000000,1.000000; // Quaternion x,y,z,w -} - -Bone49{�¶‚Ђ´ - 0.000000,0.000000,0.000000; // trans x,y,z - 0.000000,0.000000,0.000000,1.000000; // Quaternion x,y,z,w -} - -Bone50{�¶‘«Žñ - 0.000000,0.000000,0.000000; // trans x,y,z - 0.000000,0.000000,0.000000,1.000000; // Quaternion x,y,z,w -} - -Bone51{‰E‘« - 0.000000,0.000000,0.000000; // trans x,y,z - 0.000000,0.000000,0.000000,1.000000; // Quaternion x,y,z,w -} - -Bone52{‰E‚Ђ´ - 0.000000,0.000000,0.000000; // trans x,y,z - 0.000000,0.000000,0.000000,1.000000; // Quaternion x,y,z,w -} - -Bone53{‰E‘«Žñ - 0.000000,0.000000,0.000000; // trans x,y,z - 0.000000,0.000000,0.000000,1.000000; // Quaternion x,y,z,w -} - -Bone54{�¶‚‚ܐæ - 0.000000,0.000000,0.000000; // trans x,y,z - 0.000000,0.000000,0.000000,1.000000; // Quaternion x,y,z,w -} - -Bone55{‰E‚‚ܐæ - 0.000000,0.000000,0.000000; // trans x,y,z - 0.000000,0.000000,0.000000,1.000000; // Quaternion x,y,z,w -} - -Bone56{�¶‘«‚h‚j - -0.132786,0.336300,0.033801; // trans x,y,z - 0.000000,0.000000,0.000000,1.000000; // Quaternion x,y,z,w -} - -Bone57{‰E‘«‚h‚j - 0.148176,0.000000,0.001482; // trans x,y,z - 0.000000,0.000000,0.000000,1.000000; // Quaternion x,y,z,w -} - -Bone58{�¶‚‚ܐæ‚h‚j - -0.419457,-0.807707,-0.002797; // trans x,y,z - 0.000000,0.000000,0.000000,1.000000; // Quaternion x,y,z,w -} - -Bone59{‰E‚‚ܐæ‚h‚j - 0.069849,-0.366887,0.000000; // trans x,y,z - 0.000000,0.000000,0.000000,1.000000; // Quaternion x,y,z,w -} - diff --git a/frontend/apps/artcraft/app/public/resources/sound/smrpg_enemy_staticE.wav b/frontend/apps/artcraft/app/public/resources/sound/smrpg_enemy_staticE.wav deleted file mode 100644 index f6507d64..00000000 Binary files a/frontend/apps/artcraft/app/public/resources/sound/smrpg_enemy_staticE.wav and /dev/null differ diff --git a/frontend/apps/artcraft/app/src/Classes/ApiManager/BetaKeysApi.ts b/frontend/apps/artcraft/app/src/Classes/ApiManager/BetaKeysApi.ts index a089e67b..d01d39a6 100644 --- a/frontend/apps/artcraft/app/src/Classes/ApiManager/BetaKeysApi.ts +++ b/frontend/apps/artcraft/app/src/Classes/ApiManager/BetaKeysApi.ts @@ -1,6 +1,6 @@ import { ApiManager, ApiResponse } from "./ApiManager"; import { BetaKey } from "~/models"; -import type { Pagination } from "@storyteller/ui-pagescene"; +import type { Pagination } from "~/types/artcraft-web"; export class BetaKeysApi extends ApiManager { public CreateBetaKey({ diff --git a/frontend/apps/artcraft/app/src/Classes/ApiManager/MediaFilesApi.ts b/frontend/apps/artcraft/app/src/Classes/ApiManager/MediaFilesApi.ts index 9dde488f..921b734f 100644 --- a/frontend/apps/artcraft/app/src/Classes/ApiManager/MediaFilesApi.ts +++ b/frontend/apps/artcraft/app/src/Classes/ApiManager/MediaFilesApi.ts @@ -3,7 +3,7 @@ import type { MediaInfo, Pagination, PaginationInfinite, -} from "@storyteller/ui-pagescene"; +} from "~/types/artcraft-web"; import { ApiManager, ApiResponse } from "./ApiManager"; import { authentication } from "~/signals"; import type { Property } from "csstype"; diff --git a/frontend/apps/artcraft/app/src/Classes/ApiManager/PromptsApi.ts b/frontend/apps/artcraft/app/src/Classes/ApiManager/PromptsApi.ts index 7421c087..a4d73974 100644 --- a/frontend/apps/artcraft/app/src/Classes/ApiManager/PromptsApi.ts +++ b/frontend/apps/artcraft/app/src/Classes/ApiManager/PromptsApi.ts @@ -1,5 +1,5 @@ import { ApiManager, ApiResponse } from "./ApiManager"; -import type { Prompts } from "@storyteller/ui-pagescene"; +import type { Prompts } from "~/types/artcraft-web"; export class PromptsApi extends ApiManager { public GetPromptsByToken({ diff --git a/frontend/apps/artcraft/app/src/Classes/ApiManager/TtsApi.ts b/frontend/apps/artcraft/app/src/Classes/ApiManager/TtsApi.ts index 10a20b7d..d5e1ae43 100644 --- a/frontend/apps/artcraft/app/src/Classes/ApiManager/TtsApi.ts +++ b/frontend/apps/artcraft/app/src/Classes/ApiManager/TtsApi.ts @@ -1,5 +1,5 @@ import { ApiManager, ApiResponse } from "./ApiManager"; -import { GenerateTtsAudioErrorType } from "@storyteller/ui-pagescene"; +import { GenerateTtsAudioErrorType } from "~/types/artcraft-web"; import { Visibility } from "~/enums"; export interface GenerateTtsAudioRequest { diff --git a/frontend/apps/artcraft/app/src/Classes/ApiManager/UserBookmarksApi.ts b/frontend/apps/artcraft/app/src/Classes/ApiManager/UserBookmarksApi.ts index 369b2bd5..bdf1c8e4 100644 --- a/frontend/apps/artcraft/app/src/Classes/ApiManager/UserBookmarksApi.ts +++ b/frontend/apps/artcraft/app/src/Classes/ApiManager/UserBookmarksApi.ts @@ -1,5 +1,5 @@ import { ApiManager, ApiResponse } from "./ApiManager"; -import type { UserBookmarkBatch } from "@storyteller/ui-pagescene"; +import type { UserBookmarkBatch } from "~/types/artcraft-web"; export class UserBookmarksApi extends ApiManager { public CreateUserBookmark({ diff --git a/frontend/apps/artcraft/app/src/Classes/ApiManager/WeightsApi.ts b/frontend/apps/artcraft/app/src/Classes/ApiManager/WeightsApi.ts index 9cc192a2..7146602d 100644 --- a/frontend/apps/artcraft/app/src/Classes/ApiManager/WeightsApi.ts +++ b/frontend/apps/artcraft/app/src/Classes/ApiManager/WeightsApi.ts @@ -1,7 +1,7 @@ import { ApiManager, ApiResponse } from "./ApiManager"; import { authentication } from "~/signals"; import { Weight } from "~/models"; -import type { Pagination, PaginationInfinite } from "@storyteller/ui-pagescene"; +import type { Pagination, PaginationInfinite } from "~/types/artcraft-web"; import { FilterEngineCategories, FilterMediaClasses, diff --git a/frontend/apps/artcraft/app/src/components/signaled/ErrorDialog.tsx b/frontend/apps/artcraft/app/src/components/signaled/ErrorDialog.tsx deleted file mode 100644 index 2fd3308a..00000000 --- a/frontend/apps/artcraft/app/src/components/signaled/ErrorDialog.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { faTriangleExclamation } from "@fortawesome/pro-solid-svg-icons"; -import { useShallow } from "zustand/shallow"; - -import { Modal } from "@storyteller/ui-modal"; -import { Button } from "@storyteller/ui-button"; -import { usePageSceneStore } from "@storyteller/ui-pagescene"; - -export function ErrorDialog() { - const { showErrorDialog, errorDialogTitle, errorDialogMessage, setShowErrorDialog } = - usePageSceneStore( - useShallow((s) => ({ - showErrorDialog: s.showErrorDialog, - errorDialogTitle: s.errorDialogTitle, - errorDialogMessage: s.errorDialogMessage, - setShowErrorDialog: s.setShowErrorDialog, - })), - ); - - return ( - setShowErrorDialog(false)} - showClose={false} - > -
- {errorDialogMessage} -
- -
-
-
- ); -} diff --git a/frontend/apps/artcraft/app/src/components/signaled/index.ts b/frontend/apps/artcraft/app/src/components/signaled/index.ts index 5ff38720..18ca8cf1 100644 --- a/frontend/apps/artcraft/app/src/components/signaled/index.ts +++ b/frontend/apps/artcraft/app/src/components/signaled/index.ts @@ -1,3 +1,2 @@ -export * from "./ErrorDialog"; export * from "./TopBar"; diff --git a/frontend/apps/artcraft/app/src/config/appMenu.tsx b/frontend/apps/artcraft/app/src/config/appMenu.tsx index 99dc5045..c77b3f0a 100644 --- a/frontend/apps/artcraft/app/src/config/appMenu.tsx +++ b/frontend/apps/artcraft/app/src/config/appMenu.tsx @@ -6,24 +6,14 @@ import { faImage, faMusic, } from "@fortawesome/pro-solid-svg-icons"; -import { useExperimentalStore } from "@storyteller/ui-settings-modal"; import { useTabStore, TabId } from "~/pages/Stores/TabState"; export type AppId = | "IMAGE" | "VIDEO" | "AUDIO" - | "EDIT" - | "VIDEO_FRAME_EXTRACTOR" - | "VIDEO_WATERMARK_REMOVAL" - | "IMAGE_WATERMARK_REMOVAL" | "IMAGE_TO_3D_OBJECT" | "IMAGE_TO_3D_WORLD" - | "ANGLES" - | "STORYBOARD" - | "BACKGROUND_CHANGE" - | "VIDEO_EDITOR" - | "MOODBOARD" | "SETTINGS"; export interface AppDescriptor { @@ -35,8 +25,7 @@ export interface AppDescriptor { large?: boolean; } -// The five modalities. Everything else stays reachable in code (goToApp) -// but is deliberately absent from the nav. +// The five modalities. export const APP_DESCRIPTORS: AppDescriptor[] = [ { id: "IMAGE", @@ -72,24 +61,11 @@ export const goToApp = (action?: string) => { "IMAGE", "VIDEO", "AUDIO", - "VIDEO_FRAME_EXTRACTOR", - "VIDEO_WATERMARK_REMOVAL", - "IMAGE_WATERMARK_REMOVAL", "IMAGE_TO_3D_OBJECT", "IMAGE_TO_3D_WORLD", - "ANGLES", - "STORYBOARD", - "BACKGROUND_CHANGE", - "VIDEO_EDITOR", - "MOODBOARD", "SETTINGS", ].includes(action) ) { - if (action === "STORYBOARD") { - const { enabled, storyboardPageEnabled } = - useExperimentalStore.getState(); - if (!enabled || !storyboardPageEnabled) return; - } useTabStore.getState().setActiveTab(action as TabId); } }; diff --git a/frontend/apps/artcraft/app/src/enums/AssetFilterOption.ts b/frontend/apps/artcraft/app/src/enums/AssetFilterOption.ts index 86a6daeb..9e7dd8a2 100644 --- a/frontend/apps/artcraft/app/src/enums/AssetFilterOption.ts +++ b/frontend/apps/artcraft/app/src/enums/AssetFilterOption.ts @@ -1 +1 @@ -export { AssetFilterOption } from "@storyteller/ui-pagescene"; +export { AssetFilterOption } from "~/types/artcraft-web"; diff --git a/frontend/apps/artcraft/app/src/enums/AssetType.ts b/frontend/apps/artcraft/app/src/enums/AssetType.ts index 09662af2..360c1f9d 100644 --- a/frontend/apps/artcraft/app/src/enums/AssetType.ts +++ b/frontend/apps/artcraft/app/src/enums/AssetType.ts @@ -1,4 +1 @@ -// Single source of truth lives in @storyteller/ui-pagescene; this -// re-export keeps the artcraft `~/enums` surface stable while the -// 3D scene lib owns the canonical enum identity. -export { AssetType } from "@storyteller/ui-pagescene"; +export { AssetType } from "~/types/artcraft-web"; diff --git a/frontend/apps/artcraft/app/src/enums/ClipGroup.ts b/frontend/apps/artcraft/app/src/enums/ClipGroup.ts index 7a27917a..0a2f55d0 100644 --- a/frontend/apps/artcraft/app/src/enums/ClipGroup.ts +++ b/frontend/apps/artcraft/app/src/enums/ClipGroup.ts @@ -1 +1 @@ -export { ClipGroup } from "@storyteller/ui-pagescene"; +export { ClipGroup } from "~/types/artcraft-web"; diff --git a/frontend/apps/artcraft/app/src/enums/QueryFilters.ts b/frontend/apps/artcraft/app/src/enums/QueryFilters.ts index 8b74468c..e57a742e 100644 --- a/frontend/apps/artcraft/app/src/enums/QueryFilters.ts +++ b/frontend/apps/artcraft/app/src/enums/QueryFilters.ts @@ -1,7 +1,5 @@ -// Single source of truth lives in @storyteller/ui-pagescene; this -// re-export keeps the artcraft `~/enums` surface stable. export { FilterEngineCategories, FilterMediaClasses, FilterMediaType, -} from "@storyteller/ui-pagescene"; +} from "~/types/artcraft-web"; diff --git a/frontend/apps/artcraft/app/src/enums/toasts.ts b/frontend/apps/artcraft/app/src/enums/toasts.ts index e18c896e..2457ae7c 100644 --- a/frontend/apps/artcraft/app/src/enums/toasts.ts +++ b/frontend/apps/artcraft/app/src/enums/toasts.ts @@ -1,2 +1 @@ -// Single source of truth lives in @storyteller/ui-pagescene. -export { ToastTypes } from "@storyteller/ui-pagescene"; +export { ToastTypes } from "~/types/artcraft-web"; diff --git a/frontend/apps/artcraft/app/src/pages/MainApp.tsx b/frontend/apps/artcraft/app/src/pages/MainApp.tsx index f27f6544..c66b5555 100644 --- a/frontend/apps/artcraft/app/src/pages/MainApp.tsx +++ b/frontend/apps/artcraft/app/src/pages/MainApp.tsx @@ -8,7 +8,6 @@ import * as gpu from "detect-gpu"; import { useSignals } from "@preact/signals-react/runtime"; import { TopBar } from "~/components"; -import { ErrorDialog } from "~/components"; import { toast, Toaster } from "@storyteller/ui-toaster"; import { PricingModal, @@ -29,7 +28,6 @@ import { useGenerationFailedEvent, useTextToImageGenerationCompleteEvent, } from "@storyteller/tauri-events"; -import { useStoryboardPageEnabled } from "@storyteller/ui-settings-modal"; import { useActiveJobs } from "~/hooks/useActiveJobs"; import { useBackgroundLoadingMedia } from "~/hooks/useBackgroundLoadingMedia"; @@ -40,16 +38,8 @@ import { useTextToImageStore } from "./PageImage/TextToImageStore"; import TextToImage from "./PageImage/TextToImage"; import ImageToVideo from "./PageVideo/ImageToVideo"; import CreateAudio from "./PageAudio/CreateAudio"; -import { VideoFrameExtractor } from "./PageVideoFrameExtractor"; -import { VideoWatermarkRemover } from "./PageVideoWatermarkRemover"; -import { ImageWatermarkRemover } from "./PageImageWatermarkRemover"; import { ImageTo3DObject } from "./PageImageTo3DObject"; import { ImageTo3DWorld } from "./PageImageTo3DWorld"; -import { Angles } from "./PageAngles"; -import { Storyboard } from "./PageStoryboard"; -import { PageBackgroundChange } from "./PageBackgroundChange"; -import { PageVideoEditor } from "./PageVideoEditor"; -import { PageMoodboard } from "./PageMoodboard"; import { PageSettings } from "./PageSettings"; import { topNavMediaId, @@ -115,7 +105,6 @@ export const MainApp = () => { - {currentReminderModalProps && ( @@ -145,7 +134,6 @@ export const MainApp = () => { const TabBody = () => { const tabStore = useTabStore(); - const storyboardPageEnabled = useStoryboardPageEnabled(); // Every page is a fragment whose top-level children may use // position: fixed; the wrapping
scopes them so they don't @@ -172,24 +160,6 @@ const TabBody = () => {
); - case "VIDEO_FRAME_EXTRACTOR": - return ( -
- -
- ); - case "VIDEO_WATERMARK_REMOVAL": - return ( -
- -
- ); - case "IMAGE_WATERMARK_REMOVAL": - return ( -
- -
- ); case "IMAGE_TO_3D_OBJECT": return (
@@ -202,46 +172,12 @@ const TabBody = () => {
); - case "ANGLES": - return ( -
- -
- ); - case "STORYBOARD": - return storyboardPageEnabled ? ( -
- -
- ) : null; - case "BACKGROUND_CHANGE": - return ( -
- -
- ); - case "VIDEO_EDITOR": - return ( -
- -
- ); case "SETTINGS": return (
); - case "MOODBOARD": - // The TopBar is fixed and 40px tall, so the board sits below it and fills - // the rest of the viewport. overflow-hidden keeps sub-pixel 100vh - // rounding from spawning page scrollbars (which w-screen ancestors would - // turn into a horizontal scrollbar via the Windows scrollbar gutter). - return ( -
- -
- ); default: return null; } diff --git a/frontend/apps/artcraft/app/src/pages/PageAngles/Angles.tsx b/frontend/apps/artcraft/app/src/pages/PageAngles/Angles.tsx deleted file mode 100644 index 7de01a0f..00000000 --- a/frontend/apps/artcraft/app/src/pages/PageAngles/Angles.tsx +++ /dev/null @@ -1,775 +0,0 @@ -import { useState, useRef, useEffect, useCallback, useMemo } from "react"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { - faImages, - faDownload, - faCrosshairs, - faChevronUp, - faChevronDown, - faChevronLeft, - faChevronRight, -} from "@fortawesome/pro-solid-svg-icons"; -import { Button, GenerateButton } from "@storyteller/ui-button"; -import { GalleryItem, GalleryModal } from "@storyteller/ui-gallery-modal"; -import { - MediaUploadApi, - MediaFilesApi, - downloadFileFromUrl, -} from "@storyteller/api"; -import toast from "react-hot-toast"; -import { v4 as uuidv4 } from "uuid"; - -import { UploadEntryCard } from "../../components/media/UploadEntryCard"; -import { - useAnglesStore, - ROTATION_VALUES, - TILT_VALUES, - ZOOM_VALUES, - rotationToDisplay, - displayToRotation, - DISPLAY_ROTATION_VALUES, -} from "./AnglesStore"; -import { OrbitSphere, snapToNearest } from "./OrbitSphere"; -import { twMerge } from "tailwind-merge"; -import { LoadingSpinner } from "@storyteller/ui-loading-spinner"; -import { SliderV2 } from "@storyteller/ui-sliderv2"; -// import { Switch } from "@headlessui/react"; -import { GenerateImage } from "@storyteller/tauri-api"; -import { - ClassyModelSelector, - useAnglesPageModelList, - ModelPage, - useSelectedModel, - useSelectedProviderForModel, -} from "@storyteller/ui-model-selector"; -import { - CostCalculatorButton, - useCostBreakdownModalStore, -} from "@storyteller/ui-pricing-modal"; -import { HelpMenuButton } from "@storyteller/ui-help-menu"; -import { - HistoryStack, - type ImageBundle, - type BaseSelectorImage, -} from "@storyteller/ui-pagedraw"; - -const ANGLES_MODEL_PAGE = ModelPage.Angles; - -// ─── Main Angles Component ───────────────────────────────────────────────────── - -export const Angles = () => { - const anglesModelList = useAnglesPageModelList(); - const [isGalleryModalOpen, setIsGalleryModalOpen] = useState(false); - const [selectedGalleryImages, setSelectedGalleryImages] = useState( - [], - ); - const [windowSize, setWindowSize] = useState({ - width: window.innerWidth, - height: window.innerHeight, - }); - - const fileInputRef = useRef(null); - - // Model selector - const selectedModel = useSelectedModel(ANGLES_MODEL_PAGE); - const selectedProvider = useSelectedProviderForModel( - ANGLES_MODEL_PAGE, - selectedModel?.id, - ); - - // Cost estimation - const anglesCredits = useCostBreakdownModalStore( - (s) => s.estimatedCreditsByPage[ANGLES_MODEL_PAGE], - ); - - // State selectors (only re-render when specific values change) - const sourceImageUrl = useAnglesStore((s) => s.sourceImageUrl); - const sourceMediaToken = useAnglesStore((s) => s.sourceMediaToken); - const sourceThumbnailUrlTemplate = useAnglesStore( - (s) => s.sourceThumbnailUrlTemplate, - ); - const imageDimensions = useAnglesStore((s) => s.imageDimensions); - const angleConfig = useAnglesStore((s) => s.angleConfig); - // const generateFromBestAngles = useAnglesStore( - // (s) => s.generateFromBestAngles, - // ); - const generatedAngles = useAnglesStore((s) => s.generatedAngles); - const activeAngleId = useAnglesStore((s) => s.activeAngleId); - const isProcessing = useAnglesStore((s) => s.isProcessing); - const isLoadingImage = useAnglesStore((s) => s.isLoadingImage); - - // Actions (stable references — never cause re-renders) - const setSourceImage = useAnglesStore((s) => s.setSourceImage); - const setImageDimensions = useAnglesStore((s) => s.setImageDimensions); - const setRotation = useAnglesStore((s) => s.setRotation); - const setTilt = useAnglesStore((s) => s.setTilt); - const setZoom = useAnglesStore((s) => s.setZoom); - // const setGenerateFromBestAngles = useAnglesStore( - // (s) => s.setGenerateFromBestAngles, - // ); - const setActiveAngle = useAnglesStore((s) => s.setActiveAngle); - const setIsLoadingImage = useAnglesStore((s) => s.setIsLoadingImage); - const resetSource = useAnglesStore((s) => s.resetSource); - - // Currently displayed image (active generated angle, or source) - const activeAngle = useMemo( - () => generatedAngles.find((a) => a.id === activeAngleId) ?? null, - [generatedAngles, activeAngleId], - ); - - // The token of whatever is currently selected (for HistoryStack highlight) - const selectedImageToken = activeAngleId ?? sourceMediaToken ?? undefined; - - // Event listener is at module level in AnglesStore.ts — persists across navigations. - - // Derive history bundles from store (no local state needed) - const historyBundles = useMemo( - () => - generatedAngles.map((angle) => ({ - images: [ - { - url: angle.imageUrl, - mediaToken: angle.id, - thumbnailUrlTemplate: angle.thumbnailUrlTemplate, - fullImageUrl: angle.imageUrl, - }, - ], - })), - [generatedAngles], - ); - - // Derive pending placeholders from store - const pendingSubscriberIds = useAnglesStore((s) => s.pendingSubscriberIds); - const pendingGenerations = useMemo( - () => pendingSubscriberIds.map((id) => ({ id, count: 1 })), - [pendingSubscriberIds], - ); - - // Window resize handler (debounced to avoid excessive re-renders) - useEffect(() => { - let timeoutId: ReturnType; - const handleResize = () => { - clearTimeout(timeoutId); - timeoutId = setTimeout(() => { - setWindowSize({ width: window.innerWidth, height: window.innerHeight }); - }, 150); - }; - window.addEventListener("resize", handleResize); - return () => { - window.removeEventListener("resize", handleResize); - clearTimeout(timeoutId); - }; - }, []); - - // ─── Local file upload → MediaUploadApi → media token ─────────────────────── - const handleLocalImageSelect = useCallback( - async (files: FileList) => { - const file = files[0]; - if (!file || !file.type.startsWith("image/")) return; - - setIsLoadingImage(true); - - try { - const objectUrl = URL.createObjectURL(file); - - // Load dimensions - await new Promise((resolve, reject) => { - const img = new Image(); - img.onload = () => { - setImageDimensions({ - width: img.naturalWidth, - height: img.naturalHeight, - }); - resolve(); - }; - img.onerror = () => reject(new Error("Failed to load image")); - img.src = objectUrl; - }); - - // Upload to backend for media token - const mediaUploadApi = new MediaUploadApi(); - const uploadResult = await mediaUploadApi.UploadImage({ - blob: file, - fileName: file.name, - uuid: uuidv4(), - }); - - if (!uploadResult?.success || !uploadResult.data) { - throw new Error("Upload failed — no media token returned"); - } - - const mediaToken = uploadResult.data; - - // Fetch CDN URL + thumbnail template from backend - let cdnUrl = objectUrl; - let thumbnailUrlTemplate: string | undefined; - try { - const api = new MediaFilesApi(); - const mediaFile = await api.GetMediaFileByToken({ - mediaFileToken: mediaToken, - }); - if (mediaFile.success && mediaFile.data) { - const links = mediaFile.data.media_links as - | { - cdn_url?: string; - thumbnail_template?: string; - maybe_thumbnail_template?: string; - } - | undefined; - cdnUrl = - links?.cdn_url || - (mediaFile.data as any).public_bucket_url || - objectUrl; - thumbnailUrlTemplate = - links?.thumbnail_template || links?.maybe_thumbnail_template; - } - } catch { - // Fall back to blob URL if fetching CDN info fails - } - - setSourceImage(cdnUrl, mediaToken, thumbnailUrlTemplate); - setIsLoadingImage(false); - } catch (error) { - console.error("Error processing image:", error); - toast.error("Failed to process image"); - setIsLoadingImage(false); - } - }, - [setSourceImage, setImageDimensions, setIsLoadingImage], - ); - - const handleImageSelect = useCallback((id: string) => { - setSelectedGalleryImages((prev) => { - if (prev.includes(id)) return prev.filter((x) => x !== id); - return [id]; - }); - }, []); - - // ─── Gallery select → media token from item.id ────────────────────────────── - const handleGallerySelect = useCallback( - async (selectedItems: GalleryItem[]) => { - const item = selectedItems[0]; - if (!item || !item.fullImage) { - toast.error("No image selected"); - return; - } - - const mediaToken = item.id; // item.id is a media token - const imageUrl = item.fullImage; - - setIsGalleryModalOpen(false); - setSelectedGalleryImages([]); - setIsLoadingImage(true); - - try { - await new Promise((resolve, reject) => { - const img = new Image(); - img.onload = () => { - setImageDimensions({ - width: img.naturalWidth, - height: img.naturalHeight, - }); - resolve(); - }; - img.onerror = () => reject(new Error("Failed to load image")); - img.src = imageUrl; - }); - - setSourceImage(imageUrl, mediaToken); - setIsLoadingImage(false); - } catch (error) { - console.error("Error processing gallery image:", error); - toast.error("Failed to process image"); - setIsLoadingImage(false); - } - }, - [setSourceImage, setImageDimensions, setIsLoadingImage], - ); - - // ─── Generate with subscriber pattern ─────────────────────────────────────── - const handleGenerate = useCallback(async () => { - const state = useAnglesStore.getState(); - if (!state.sourceMediaToken) return; - - const subscriberId = uuidv4(); - state.startGeneration(subscriberId); - - window.__storeTaskEnqueueMeta?.({ - prompt: "", - modelType: (selectedModel as any)?.tauriId || "flux_2_lora_angles", - timestamp: Date.now(), - }); - - try { - await GenerateImage({ - model: selectedModel ?? "flux_2_lora_angles", - provider: selectedProvider ?? undefined, - image_media_tokens: [state.sourceMediaToken], - prompt: "", - adjust_horizontal_angle: state.angleConfig.rotation, - adjust_vertical_angle: state.angleConfig.tilt, - adjust_zoom: state.angleConfig.zoom, - frontend_subscriber_id: subscriberId, - }); - } catch (error) { - console.error("Error generating angle:", error); - toast.error("Failed to generate angle"); - useAnglesStore.getState().removePendingSubscriber(subscriberId); - } - }, [selectedModel, selectedProvider]); - - const handleDownload = useCallback(async () => { - if (!activeAngle) { - toast.error("No image to download"); - return; - } - try { - await downloadFileFromUrl(activeAngle.imageUrl); - toast.success("Image saved to Downloads folder"); - } catch (error) { - console.error("Download failed:", error); - toast.error("Failed to download image"); - } - }, [activeAngle]); - - // Called when user releases the sphere drag — values are already snapped - const handleSphereDragEnd = useCallback( - (snappedRotation: number, snappedTilt: number) => { - setRotation(snappedRotation); - setTilt(snappedTilt); - }, - [setRotation, setTilt], - ); - - const showUploadScreen = !sourceImageUrl && !isLoadingImage; - - const imageContainerStyle = useMemo(() => { - if (!imageDimensions) { - return { width: "600px", height: "450px" }; - } - - const horizontalPadding = 128 + 80; - const verticalPadding = 56 + 64 + 160; - - const availableWidth = windowSize.width - horizontalPadding; - const availableHeight = windowSize.height - verticalPadding; - - const maxWidth = Math.min(availableWidth, 1200); - const maxHeight = Math.max(availableHeight, 200); - const imageAspect = imageDimensions.width / imageDimensions.height; - - let width = maxWidth; - let height = width / imageAspect; - - if (height > maxHeight) { - height = maxHeight; - width = height * imageAspect; - } - - width = Math.max(width, 200); - height = Math.max(height, 150); - - return { width: `${width}px`, height: `${height}px` }; - }, [imageDimensions, windowSize.width, windowSize.height]); - - const handleFileInputChange = useCallback( - (e: React.ChangeEvent) => { - if (e.target.files) { - handleLocalImageSelect(e.target.files); - e.target.value = ""; - } - }, - [handleLocalImageSelect], - ); - - const handleOpenGallery = useCallback(() => { - setIsGalleryModalOpen(true); - }, []); - - const handleCloseGallery = useCallback(() => { - setIsGalleryModalOpen(false); - setSelectedGalleryImages([]); - }, []); - - const handleChangeImage = useCallback(() => { - resetSource(); - useAnglesStore.getState().clearGeneratedAngles(); - }, [resetSource]); - - // Slider handlers that snap to allowed values - const handleRotationSlider = useCallback( - (value: number) => { - const snapped = snapToNearest(value, DISPLAY_ROTATION_VALUES); - setRotation(displayToRotation(snapped)); - }, - [setRotation], - ); - - const handleTiltSlider = useCallback( - (value: number) => { - setTilt(snapToNearest(value, TILT_VALUES)); - }, - [setTilt], - ); - - const handleZoomSlider = useCallback( - (value: number) => { - setZoom(snapToNearest(value, ZOOM_VALUES)); - }, - [setZoom], - ); - - // Arrow step handlers for orbit sphere - const handleRotationStep = useCallback( - (direction: 1 | -1) => { - const idx = ROTATION_VALUES.indexOf(angleConfig.rotation); - const next = - (idx + direction + ROTATION_VALUES.length) % ROTATION_VALUES.length; - setRotation(ROTATION_VALUES[next]); - }, - [angleConfig.rotation, setRotation], - ); - - const handleTiltStep = useCallback( - (direction: 1 | -1) => { - const idx = TILT_VALUES.indexOf(angleConfig.tilt); - const next = Math.max( - 0, - Math.min(TILT_VALUES.length - 1, idx + direction), - ); - setTilt(TILT_VALUES[next]); - }, - [angleConfig.tilt, setTilt], - ); - - // ─── HistoryStack data ────────────────────────────────────────────────────── - // Source image as the first entry so user can return to original - const allBundles = useMemo(() => { - if (!sourceImageUrl || !sourceMediaToken) return historyBundles; - const sourceBundle: ImageBundle = { - images: [ - { - url: sourceImageUrl, - mediaToken: sourceMediaToken, - thumbnailUrlTemplate: sourceThumbnailUrlTemplate ?? undefined, - fullImageUrl: sourceImageUrl, - }, - ], - }; - return [sourceBundle, ...historyBundles]; - }, [ - sourceImageUrl, - sourceMediaToken, - sourceThumbnailUrlTemplate, - historyBundles, - ]); - - const handleHistoryImageSelect = useCallback( - (image: BaseSelectorImage) => { - // If selecting the source image, clear active angle to show source - if (image.mediaToken === useAnglesStore.getState().sourceMediaToken) { - setActiveAngle(null); - } else { - setActiveAngle(image.mediaToken); - } - }, - [setActiveAngle], - ); - - const handleHistoryImageRemove = useCallback((image: BaseSelectorImage) => { - useAnglesStore.getState().removeGeneratedAngle(image.mediaToken); - }, []); - - const handleHistoryClear = useCallback(() => { - useAnglesStore.getState().clearGeneratedAngles(); - }, []); - - return ( - <> -
- {showUploadScreen ? ( - /* ──── Upload screen ──── */ -
-
-
- - {isLoadingImage && ( -
- -
- )} -
-
-
- ) : ( - /* ──── Editor layout — floating panels over full-bleed canvas ──── */ -
- - - {/* Full-bleed image display */} -
-
- {activeAngle ? ( - Generated Angle - ) : sourceImageUrl ? ( - Source - ) : ( -
- -
- )} -
-
- - {/* ── Floating top toolbar ── */} -
-
- - {activeAngle && ( - - )} -
-
- - {/* ── History stack (right side, like PageEdit) ── */} -
- -
- - {/* ── Floating bottom angle controls ── */} -
-
- {/* Orbit sphere */} -
- - - - - -
- - {/* Divider */} -
- - {/* Sliders group */} -
-
- - Rotation - -
- -
- - {rotationToDisplay(angleConfig.rotation)}° - -
- -
- - Tilt - -
- -
- - {angleConfig.tilt}° - -
- -
- - Zoom - -
- -
- - {angleConfig.zoom} - -
-
- - {/* Divider */} -
- - {/* Toggle + Generate */} -
- {/* -
- - Best 12 angles - - - - -
-
*/} - - - - Generate - -
-
-
- - {/* ── Cost calculator + Help (bottom-right) ── */} -
- - -
-
- )} -
- - - - ); -}; diff --git a/frontend/apps/artcraft/app/src/pages/PageAngles/AnglesStore.ts b/frontend/apps/artcraft/app/src/pages/PageAngles/AnglesStore.ts deleted file mode 100644 index 32031c9c..00000000 --- a/frontend/apps/artcraft/app/src/pages/PageAngles/AnglesStore.ts +++ /dev/null @@ -1,288 +0,0 @@ -import { create } from "zustand"; -import { listen } from "@tauri-apps/api/event"; - -export interface GeneratedAngle { - id: string; - imageUrl: string; - thumbnailUrlTemplate?: string; - rotation: number; - tilt: number; - zoom: number; - timestamp: number; -} - -export interface ImageDimensions { - width: number; - height: number; -} - -export interface AngleConfig { - rotation: number; // 0, 45, 90, 135, 180, 225, 270, 315 - tilt: number; // -30, 0, 30, 60 - zoom: number; // 0, 5, 10 -} - -// Snapped value options (backend values) -export const ROTATION_VALUES = [0, 45, 90, 135, 180, 225, 270, 315]; -export const TILT_VALUES = [-30, 0, 30, 60]; -export const ZOOM_VALUES = [0, 5, 10]; - -// ─── Display conversion helpers ────────────────────────────────────────────── -// Backend uses 0–315 for rotation; UI displays −180 to +180 centered at 0. - -/** Convert backend rotation (0–315) to display value (−180 to +180). */ -export const rotationToDisplay = (backend: number): number => - backend > 180 ? backend - 360 : backend; - -/** Convert display rotation (−180 to +180) to backend value (0–315). */ -export const displayToRotation = (display: number): number => - display < 0 ? display + 360 : display; - -/** Snap targets for the display-range rotation slider. */ -export const DISPLAY_ROTATION_VALUES = [-180, -135, -90, -45, 0, 45, 90, 135, 180]; - -interface AnglesState { - // Source image - sourceImageUrl: string | null; - sourceMediaToken: string | null; - sourceThumbnailUrlTemplate: string | null; - imageDimensions: ImageDimensions | null; - - // Angle config - angleConfig: AngleConfig; - generateFromBestAngles: boolean; - - // Generated results - generatedAngles: GeneratedAngle[]; - activeAngleId: string | null; - - // Processing - isProcessing: boolean; - pendingSubscriberIds: string[]; - isLoadingImage: boolean; - - // Actions - setSourceImage: (url: string, mediaToken: string | null, thumbnailUrlTemplate?: string | null) => void; - setImageDimensions: (dims: ImageDimensions | null) => void; - setRotation: (value: number) => void; - setTilt: (value: number) => void; - setZoom: (value: number) => void; - setGenerateFromBestAngles: (value: boolean) => void; - addGeneratedAngle: (angle: GeneratedAngle) => void; - setActiveAngle: (id: string | null) => void; - getActiveAngle: () => GeneratedAngle | null; - removeGeneratedAngle: (id: string) => void; - clearGeneratedAngles: () => void; - removePendingSubscriber: (subscriberId: string) => void; - setIsLoadingImage: (value: boolean) => void; - startGeneration: (subscriberId: string) => void; - completeGeneration: ( - subscriberId: string, - images: Array<{ - cdn_url: string; - media_token: string; - maybe_thumbnail_template?: string; - }>, - ) => void; - resetSource: () => void; - clearAll: () => void; -} - -const DEFAULT_CONFIG: AngleConfig = { - rotation: 0, - tilt: 0, - zoom: 0, -}; - -export const useAnglesStore = create((set, get) => ({ - sourceImageUrl: null, - sourceMediaToken: null, - sourceThumbnailUrlTemplate: null, - imageDimensions: null, - angleConfig: { ...DEFAULT_CONFIG }, - generateFromBestAngles: false, - generatedAngles: [], - activeAngleId: null, - isProcessing: false, - pendingSubscriberIds: [], - isLoadingImage: false, - - setSourceImage: (url, mediaToken, thumbnailUrlTemplate) => { - set({ sourceImageUrl: url, sourceMediaToken: mediaToken, sourceThumbnailUrlTemplate: thumbnailUrlTemplate ?? null }); - }, - - setImageDimensions: (dims) => { - set({ imageDimensions: dims }); - }, - - setRotation: (value) => { - set((state) => ({ - angleConfig: { ...state.angleConfig, rotation: value }, - })); - }, - - setTilt: (value) => { - set((state) => ({ - angleConfig: { ...state.angleConfig, tilt: value }, - })); - }, - - setZoom: (value) => { - set((state) => ({ - angleConfig: { ...state.angleConfig, zoom: value }, - })); - }, - - setGenerateFromBestAngles: (value) => { - set({ generateFromBestAngles: value }); - }, - - addGeneratedAngle: (angle) => { - set((state) => ({ - generatedAngles: [...state.generatedAngles, angle], - activeAngleId: angle.id, - })); - }, - - setActiveAngle: (id) => { - set({ activeAngleId: id }); - }, - - getActiveAngle: () => { - const state = get(); - return ( - state.generatedAngles.find((a) => a.id === state.activeAngleId) ?? null - ); - }, - - removeGeneratedAngle: (id) => { - set((s) => { - const filtered = s.generatedAngles.filter((a) => a.id !== id); - return { - generatedAngles: filtered, - activeAngleId: s.activeAngleId === id ? null : s.activeAngleId, - }; - }); - }, - - clearGeneratedAngles: () => { - set({ - generatedAngles: [], - activeAngleId: null, - isProcessing: false, - pendingSubscriberIds: [], - }); - }, - - removePendingSubscriber: (subscriberId) => { - set((s) => { - const remaining = s.pendingSubscriberIds.filter( - (id) => id !== subscriberId, - ); - return { - pendingSubscriberIds: remaining, - isProcessing: remaining.length > 0, - }; - }); - }, - - setIsLoadingImage: (value) => { - set({ isLoadingImage: value }); - }, - - startGeneration: (subscriberId) => { - set((s) => ({ - isProcessing: true, - pendingSubscriberIds: [...s.pendingSubscriberIds, subscriberId], - })); - }, - - completeGeneration: (subscriberId, images) => { - const state = get(); - if (!state.pendingSubscriberIds.includes(subscriberId)) return; - - const newAngles: GeneratedAngle[] = images.map((img) => ({ - id: img.media_token, - imageUrl: img.cdn_url, - thumbnailUrlTemplate: img.maybe_thumbnail_template, - rotation: state.angleConfig.rotation, - tilt: state.angleConfig.tilt, - zoom: state.angleConfig.zoom, - timestamp: Date.now(), - })); - - const remaining = state.pendingSubscriberIds.filter( - (id) => id !== subscriberId, - ); - set((s) => ({ - generatedAngles: [...s.generatedAngles, ...newAngles], - activeAngleId: newAngles[0]?.id ?? s.activeAngleId, - isProcessing: remaining.length > 0, - pendingSubscriberIds: remaining, - })); - }, - - resetSource: () => { - set({ - sourceImageUrl: null, - sourceMediaToken: null, - sourceThumbnailUrlTemplate: null, - imageDimensions: null, - angleConfig: { ...DEFAULT_CONFIG }, - generateFromBestAngles: false, - isProcessing: false, - pendingSubscriberIds: [], - isLoadingImage: false, - }); - }, - - clearAll: () => { - set({ - sourceImageUrl: null, - sourceMediaToken: null, - sourceThumbnailUrlTemplate: null, - imageDimensions: null, - angleConfig: { ...DEFAULT_CONFIG }, - generateFromBestAngles: false, - generatedAngles: [], - activeAngleId: null, - isProcessing: false, - pendingSubscriberIds: [], - isLoadingImage: false, - }); - }, -})); - -// ─── Module-level event listener ──────────────────────────────────────────────── -// Persists across page navigations so generation results are never lost. -// Angle models route through ImageGeneration task type, which fires -// text_to_image_generation_complete_event (not image_edit_complete_event). -listen<{ - status: string; - data: { - generated_images: Array<{ - media_token: string; - cdn_url: string; - maybe_thumbnail_template?: string; - }>; - maybe_frontend_subscriber_id?: string; - }; -}>("text_to_image_generation_complete_event", (wrappedEvent) => { - const event = wrappedEvent.payload.data; - const state = useAnglesStore.getState(); - if (state.pendingSubscriberIds.length === 0) return; - - const subscriberId = event.maybe_frontend_subscriber_id; - if (subscriberId && !state.pendingSubscriberIds.includes(subscriberId)) - return; - - const resolvedId = subscriberId ?? state.pendingSubscriberIds[0]; - - const images = event.generated_images.map((img) => ({ - cdn_url: img.cdn_url, - media_token: img.media_token, - maybe_thumbnail_template: img.maybe_thumbnail_template, - })); - - state.completeGeneration(resolvedId, images); -}); diff --git a/frontend/apps/artcraft/app/src/pages/PageAngles/OrbitSphere.tsx b/frontend/apps/artcraft/app/src/pages/PageAngles/OrbitSphere.tsx deleted file mode 100644 index f5723481..00000000 --- a/frontend/apps/artcraft/app/src/pages/PageAngles/OrbitSphere.tsx +++ /dev/null @@ -1,729 +0,0 @@ -import { useRef, useEffect, useCallback, memo } from "react"; -import { ROTATION_VALUES, TILT_VALUES } from "./AnglesStore"; - -// ─── Types ──────────────────────────────────────────────────────────────────── - -export interface OrbitSphereProps { - rotation: number; - tilt: number; - zoom: number; - onDragEnd: (rotation: number, tilt: number) => void; -} - -// ─── Helpers ───────────────────────────────────────────────────────────────── - -/** Snap a raw value to the nearest element in an array of allowed values. */ -export const snapToNearest = ( - value: number, - allowedValues: number[], -): number => { - let closest = allowedValues[0]; - let minDist = Math.abs(value - closest); - for (const v of allowedValues) { - const dist = Math.abs(value - v); - if (dist < minDist) { - minDist = dist; - closest = v; - } - } - return closest; -}; - -// ─── Face descriptor used by the painter's-algorithm renderer ──────────────── - -interface Face { - pts: { x: number; y: number; depth: number }[]; - fillColor: string; - edgeColor: string; - edgeWidth: number; - /** Lens front cap — draws concentric glass rings. */ - isFront: boolean; - /** Camera body back face — draws the LCD screen inset. */ - isBack: boolean; - /** Camera body front face — draws the red recording dot. */ - isBodyFront: boolean; - avgDepth: number; - /** Targeting-laser line from lens tip to sphere centre. */ - isLine?: boolean; -} - -// ─── OrbitSphere Component ─────────────────────────────────────────────────── - -export const OrbitSphere = memo( - ({ rotation, tilt, onDragEnd }: OrbitSphereProps) => { - const canvasRef = useRef(null); - const isDragging = useRef(false); - const lastPos = useRef({ x: 0, y: 0 }); - const liveRotation = useRef(rotation); - const liveTilt = useRef(tilt); - const rafId = useRef(null); - - // Keep live refs in sync when props change (from sliders / snap) - useEffect(() => { - if (!isDragging.current) { - liveRotation.current = rotation; - liveTilt.current = tilt; - } - }, [rotation, tilt]); - - // ─── Core draw function ────────────────────────────────────────────────── - - const drawSphere = useCallback( - (renderRotation: number, renderTilt: number, dragging = false) => { - const canvas = canvasRef.current; - if (!canvas) return; - const ctx = canvas.getContext("2d"); - if (!ctx) return; - - // HiDPI setup - const displayW = canvas.clientWidth; - const displayH = canvas.clientHeight; - const dpr = window.devicePixelRatio || 1; - canvas.width = displayW * dpr; - canvas.height = displayH * dpr; - ctx.setTransform(dpr, 0, 0, dpr, 0, 0); - - const w = displayW; - const h = displayH; - const cx = w / 2; - const cy = h / 2; - const radius = Math.min(w, h) * 0.4; - - ctx.clearRect(0, 0, w, h); - - // Subtle radial outer glow - const outerGlow = ctx.createRadialGradient( - cx, - cy, - radius * 0.8, - cx, - cy, - radius * 1.3, - ); - outerGlow.addColorStop(0, "rgba(255, 255, 255, 0.02)"); - outerGlow.addColorStop(1, "rgba(255, 255, 255, 0)"); - ctx.fillStyle = outerGlow; - ctx.fillRect(0, 0, w, h); - - const rotRad = (renderRotation * Math.PI) / 180; - const tiltRad = (renderTilt * Math.PI) / 180; - - // ── Perspective projection ────────────────────────────────────────── - const project = ( - x3d: number, - y3d: number, - z3d: number, - ): { x: number; y: number; depth: number } => { - const perspective = 6; - const scale = perspective / (perspective - z3d); - return { - x: cx + x3d * radius * scale, - y: cy + y3d * radius * scale, - depth: z3d, - }; - }; - - // ── Wireframe sphere ──────────────────────────────────────────────── - const wireAlpha = dragging ? 0.12 : 0.06; - ctx.strokeStyle = `rgba(255, 255, 255, ${wireAlpha})`; - ctx.lineWidth = 0.7; - - // Meridians - for (let i = 0; i < 12; i++) { - const angle = (i * Math.PI) / 6 + rotRad; - ctx.beginPath(); - for (let j = 0; j <= 40; j++) { - const phi = (j / 40) * Math.PI * 2; - const p = project( - Math.cos(angle) * Math.sin(phi), - Math.cos(phi), - Math.sin(angle) * Math.sin(phi), - ); - if (j === 0) ctx.moveTo(p.x, p.y); - else ctx.lineTo(p.x, p.y); - } - ctx.stroke(); - } - - // Parallels - for (let i = 1; i < 6; i++) { - const phi = (i * Math.PI) / 6; - ctx.beginPath(); - for (let j = 0; j <= 40; j++) { - const angle = (j / 40) * Math.PI * 2 + rotRad; - const p = project( - Math.cos(angle) * Math.sin(phi), - Math.cos(phi), - Math.sin(angle) * Math.sin(phi), - ); - if (j === 0) ctx.moveTo(p.x, p.y); - else ctx.lineTo(p.x, p.y); - } - ctx.stroke(); - } - - // ── 3D camera position & orientation ─────────────────────────────── - const camPosX = Math.sin(rotRad) * Math.cos(tiltRad); - const camPosY = -Math.sin(tiltRad); - const camPosZ = Math.cos(rotRad) * Math.cos(tiltRad); - - const centerScreen = project(0, 0, 0); - - // Direction vector pointing from camera towards origin - const dirX = -camPosX; - const dirY = -camPosY; - const dirZ = -camPosZ; - - // Camera-local right axis (world-up crossed with dir) - let rightX = dirZ; - const rightY = 0; - let rightZ = -dirX; - const rightLen = Math.sqrt(rightX * rightX + rightZ * rightZ) || 1; - rightX /= rightLen; - rightZ /= rightLen; - - // Camera-local up axis - let upX = rightY * dirZ - rightZ * dirY; - let upY = rightZ * dirX - rightX * dirZ; - let upZ = rightX * dirY - rightY * dirX; - const upLen = Math.sqrt(upX * upX + upY * upY + upZ * upZ) || 1; - upX /= upLen; - upY /= upLen; - upZ /= upLen; - - // ── Camera body dimensions ───────────────────────────────────────── - const bodyW = 0.28; - const bodyH = 0.19; - const bodyD = 0.22; - - // ── Geometry helpers ─────────────────────────────────────────────── - - /** Build 8 corners of an axis-aligned box in camera-local space. */ - const makeBox = ( - bx: number, - by: number, - bz: number, - hw: number, - hh: number, - hd: number, - ) => [ - { - x: bx + rightX * hw + upX * hh, - y: by + rightY * hw + upY * hh, - z: bz + rightZ * hw + upZ * hh, - }, - { - x: bx - rightX * hw + upX * hh, - y: by - rightY * hw + upY * hh, - z: bz - rightZ * hw + upZ * hh, - }, - { - x: bx - rightX * hw - upX * hh, - y: by - rightY * hw - upY * hh, - z: bz - rightZ * hw - upZ * hh, - }, - { - x: bx + rightX * hw - upX * hh, - y: by + rightY * hw - upY * hh, - z: bz + rightZ * hw - upZ * hh, - }, - { - x: bx + dirX * hd + rightX * hw + upX * hh, - y: by + dirY * hd + rightY * hw + upY * hh, - z: bz + dirZ * hd + rightZ * hw + upZ * hh, - }, - { - x: bx + dirX * hd - rightX * hw + upX * hh, - y: by + dirY * hd - rightY * hw + upY * hh, - z: bz + dirZ * hd - rightZ * hw + upZ * hh, - }, - { - x: bx + dirX * hd - rightX * hw - upX * hh, - y: by + dirY * hd - rightY * hw - upY * hh, - z: bz + dirZ * hd - rightZ * hw - upZ * hh, - }, - { - x: bx + dirX * hd + rightX * hw - upX * hh, - y: by + dirY * hd + rightY * hw - upY * hh, - z: bz + dirZ * hd + rightZ * hw - upZ * hh, - }, - ]; - - // ── Build camera geometry ────────────────────────────────────────── - const bodyCorners3D = makeBox( - camPosX, - camPosY, - camPosZ, - bodyW, - bodyH, - bodyD, - ); - - const lensLen = 0.14; - const lensR = 0.1; - const lensCx = camPosX + dirX * bodyD; - const lensCy = camPosY + dirY * bodyD; - const lensCz = camPosZ + dirZ * bodyD; - - const vfCorners3D = makeBox( - camPosX + dirX * -0.03 + upX * (bodyH + 0.05), - camPosY + dirY * -0.03 + upY * (bodyH + 0.05), - camPosZ + dirZ * -0.03 + upZ * (bodyH + 0.05), - 0.09, - 0.05, - 0.1, - ); - const gripCorners3D = makeBox( - camPosX + rightX * (bodyW + 0.03), - camPosY + rightY * (bodyW + 0.03), - camPosZ + rightZ * (bodyW + 0.03), - 0.04, - bodyH * 0.85, - bodyD * 0.7, - ); - - // Project box corners to screen - const bodyCorners = bodyCorners3D.map((c) => project(c.x, c.y, c.z)); - const vfCorners = vfCorners3D.map((c) => project(c.x, c.y, c.z)); - const gripCorners = gripCorners3D.map((c) => project(c.x, c.y, c.z)); - - // Box face winding: [back, front, top, bottom, left, right] - const BOX_FACES: [number, number, number, number][] = [ - [0, 1, 2, 3], - [4, 5, 6, 7], - [0, 1, 5, 4], - [3, 2, 6, 7], - [1, 2, 6, 5], - [0, 3, 7, 4], - ]; - - // Edge colours - const edgeGlow = "rgba(180, 200, 255, 0.35)"; - const edgeGlowBright = "rgba(180, 200, 255, 0.55)"; - - // ── Face collector ───────────────────────────────────────────────── - const allFaces: Face[] = []; - - const addBoxFaces = ( - corners: { x: number; y: number; depth: number }[], - fillBase: string, - edge: string, - edgeW: number, - markBack = false, - markBodyFront = false, - ) => { - for (let i = 0; i < 6; i++) { - const pts = BOX_FACES[i].map((idx) => corners[idx]); - allFaces.push({ - pts, - fillColor: fillBase, - edgeColor: edge, - edgeWidth: edgeW, - isFront: false, - isBack: markBack && i === 0, - isBodyFront: markBodyFront && i === 1, - avgDepth: pts.reduce((s, p) => s + p.depth, 0) / pts.length, - }); - } - }; - - const addCylinder = ( - bx: number, - by: number, - bz: number, - r: number, - len: number, - fillBase: string, - edge: string, - edgeW: number, - markFrontCap: boolean, - ) => { - const NUM_SEGS = 20; - const basePts: { x: number; y: number; z: number }[] = []; - const frontPts: { x: number; y: number; z: number }[] = []; - - for (let i = 0; i < NUM_SEGS; i++) { - const a = (i / NUM_SEGS) * Math.PI * 2; - const lx = Math.cos(a) * r; - const ly = Math.sin(a) * r; - basePts.push({ - x: bx + rightX * lx + upX * ly, - y: by + rightY * lx + upY * ly, - z: bz + rightZ * lx + upZ * ly, - }); - frontPts.push({ - x: bx + dirX * len + rightX * lx + upX * ly, - y: by + dirY * len + rightY * lx + upY * ly, - z: bz + dirZ * len + rightZ * lx + upZ * ly, - }); - } - - // Side quads - for (let i = 0; i < NUM_SEGS; i++) { - const ni = (i + 1) % NUM_SEGS; - const pts = [ - basePts[i], - basePts[ni], - frontPts[ni], - frontPts[i], - ].map((pt) => project(pt.x, pt.y, pt.z)); - allFaces.push({ - pts, - fillColor: fillBase, - edgeColor: edge, - edgeWidth: edgeW, - isFront: false, - isBack: false, - isBodyFront: false, - avgDepth: pts.reduce((s, p) => s + p.depth, 0) / pts.length, - }); - } - - // Front cap - const capPts = frontPts.map((p) => project(p.x, p.y, p.z)); - allFaces.push({ - pts: capPts, - fillColor: fillBase, - edgeColor: edge, - edgeWidth: edgeW, - isFront: markFrontCap, - isBack: false, - isBodyFront: false, - avgDepth: capPts.reduce((s, p) => s + p.depth, 0) / capPts.length, - }); - }; - - // ── Add all camera parts ─────────────────────────────────────────── - addBoxFaces( - bodyCorners, - "rgba(35, 38, 45, 0.92)", - edgeGlow, - 1.0, - true, - true, - ); - addBoxFaces( - gripCorners, - "rgba(30, 33, 40, 0.9)", - edgeGlow, - 0.7, - false, - false, - ); - addCylinder( - lensCx, - lensCy, - lensCz, - lensR, - lensLen, - "rgba(28, 30, 38, 0.95)", - edgeGlowBright, - 0.8, - true, - ); - addBoxFaces( - vfCorners, - "rgba(40, 43, 50, 0.9)", - edgeGlow, - 0.8, - false, - false, - ); - - // Targeting laser from lens tip to sphere centre - const lensTip = project( - lensCx + dirX * lensLen, - lensCy + dirY * lensLen, - lensCz + dirZ * lensLen, - ); - allFaces.push({ - pts: [lensTip, centerScreen], - fillColor: "", - edgeColor: "", - edgeWidth: 0, - isFront: false, - isBack: false, - isBodyFront: false, - avgDepth: (lensTip.depth + centerScreen.depth) / 2, - isLine: true, - }); - - // ── Painter's sort & draw ───────────────────────────────────────── - allFaces.sort((a, b) => a.avgDepth - b.avgDepth); - ctx.lineJoin = "round"; - - for (const face of allFaces) { - // ── Targeting laser ─────────────────────────────────────────── - if (face.isLine) { - ctx.save(); - ctx.strokeStyle = "rgba(180, 210, 255, 0.7)"; - ctx.lineWidth = 1.5; - ctx.shadowColor = "rgba(160, 200, 255, 0.9)"; - ctx.shadowBlur = 4; - ctx.beginPath(); - ctx.moveTo(face.pts[0].x, face.pts[0].y); - ctx.lineTo(face.pts[1].x, face.pts[1].y); - ctx.stroke(); - ctx.restore(); - continue; - } - - // ── Fill + glow edges ───────────────────────────────────────── - ctx.beginPath(); - ctx.moveTo(face.pts[0].x, face.pts[0].y); - for (let i = 1; i < face.pts.length; i++) - ctx.lineTo(face.pts[i].x, face.pts[i].y); - ctx.closePath(); - ctx.fillStyle = face.fillColor; - ctx.fill(); - - ctx.save(); - ctx.shadowColor = "rgba(160, 190, 255, 0.4)"; - ctx.shadowBlur = 3; - ctx.strokeStyle = face.edgeColor; - ctx.lineWidth = face.edgeWidth; - ctx.stroke(); - ctx.restore(); - - ctx.strokeStyle = face.edgeColor; - ctx.lineWidth = face.edgeWidth * 0.5; - ctx.stroke(); - - // ── Back LCD screen inset ───────────────────────────────────── - if (face.isBack) { - const scx = - (face.pts[0].x + face.pts[1].x + face.pts[2].x + face.pts[3].x) / - 4; - const scy = - (face.pts[0].y + face.pts[1].y + face.pts[2].y + face.pts[3].y) / - 4; - const screenPts = face.pts.map((p) => ({ - x: scx + (p.x - scx) * 0.82, - y: scy + (p.y - scy) * 0.75, - })); - - ctx.beginPath(); - ctx.moveTo(screenPts[0].x, screenPts[0].y); - for (let i = 1; i < screenPts.length; i++) - ctx.lineTo(screenPts[i].x, screenPts[i].y); - ctx.closePath(); - ctx.fillStyle = "rgba(18, 20, 26, 0.98)"; - ctx.fill(); - ctx.strokeStyle = "rgba(255, 255, 255, 0.06)"; - ctx.lineWidth = 1; - ctx.stroke(); - - // Subtle specular highlight on top-left corner - ctx.beginPath(); - ctx.moveTo(screenPts[0].x, screenPts[0].y); - ctx.lineTo(screenPts[1].x, screenPts[1].y); - ctx.lineTo(screenPts[2].x, screenPts[2].y); - ctx.closePath(); - ctx.fillStyle = "rgba(255, 255, 255, 0.02)"; - ctx.fill(); - } - - // ── Lens glass rings & gradient ─────────────────────────────── - if (face.isFront) { - // Projects a concentric ring of lens-local radius rScale onto the screen. - const getCirclePts = (rScale: number) => { - const pts = []; - for (let i = 0; i < 20; i++) { - const a = (i / 20) * Math.PI * 2; - const lx = Math.cos(a) * lensR * rScale; - const ly = Math.sin(a) * lensR * rScale; - pts.push( - project( - lensCx + dirX * lensLen + rightX * lx + upX * ly, - lensCy + dirY * lensLen + rightY * lx + upY * ly, - lensCz + dirZ * lensLen + rightZ * lx + upZ * ly, - ), - ); - } - return pts; - }; - - const drawRing = (pts: { x: number; y: number }[]) => { - ctx.beginPath(); - ctx.moveTo(pts[0].x, pts[0].y); - for (let i = 1; i < pts.length; i++) - ctx.lineTo(pts[i].x, pts[i].y); - ctx.closePath(); - }; - - // Mid ring - ctx.strokeStyle = "rgba(180, 200, 255, 0.45)"; - ctx.lineWidth = 1.0; - drawRing(getCirclePts(0.7)); - ctx.stroke(); - - // Inner ring - ctx.strokeStyle = "rgba(160, 190, 255, 0.3)"; - ctx.lineWidth = 0.6; - drawRing(getCirclePts(0.4)); - ctx.stroke(); - - // Glass gradient fill - const lfc = project( - lensCx + dirX * lensLen, - lensCy + dirY * lensLen, - lensCz + dirZ * lensLen, - ); - const lensGrad = ctx.createRadialGradient( - lfc.x, - lfc.y, - 0, - lfc.x, - lfc.y, - 8, - ); - lensGrad.addColorStop(0, "rgba(100, 160, 255, 0.4)"); - lensGrad.addColorStop(0.6, "rgba(60, 100, 200, 0.2)"); - lensGrad.addColorStop(1, "rgba(40, 70, 150, 0.05)"); - ctx.fillStyle = lensGrad; - drawRing(getCirclePts(0.85)); - ctx.fill(); - - // Outer glow ring - ctx.save(); - ctx.shadowColor = "rgba(160, 190, 255, 0.6)"; - ctx.shadowBlur = 6; - ctx.strokeStyle = "rgba(180, 200, 255, 0.4)"; - ctx.lineWidth = 1.2; - drawRing(getCirclePts(0.95)); - ctx.stroke(); - ctx.restore(); - } - - // ── Red recording dot on body front ─────────────────────────── - if (face.isBodyFront) { - const dot3D = { - x: - camPosX + - dirX * bodyD + - rightX * bodyW * 0.6 + - upX * bodyH * 0.6, - y: - camPosY + - dirY * bodyD + - rightY * bodyW * 0.6 + - upY * bodyH * 0.6, - z: - camPosZ + - dirZ * bodyD + - rightZ * bodyW * 0.6 + - upZ * bodyH * 0.6, - }; - const dot = project(dot3D.x, dot3D.y, dot3D.z); - - const glow = ctx.createRadialGradient( - dot.x, - dot.y, - 0, - dot.x, - dot.y, - 5, - ); - glow.addColorStop(0, "rgba(255, 40, 40, 0.8)"); - glow.addColorStop(0.5, "rgba(255, 40, 40, 0.2)"); - glow.addColorStop(1, "rgba(255, 40, 40, 0)"); - ctx.fillStyle = glow; - ctx.beginPath(); - ctx.arc(dot.x, dot.y, 5, 0, Math.PI * 2); - ctx.fill(); - - ctx.fillStyle = "rgba(255, 60, 60, 1)"; - ctx.beginPath(); - ctx.arc(dot.x, dot.y, 1.8, 0, Math.PI * 2); - ctx.fill(); - } - } - - // ── Centre crosshair ───────────────────────────────────────────── - ctx.lineJoin = "miter"; - ctx.strokeStyle = "rgba(255, 255, 255, 0.12)"; - ctx.lineWidth = 0.8; - const crossSize = 5; - ctx.beginPath(); - ctx.moveTo(centerScreen.x - crossSize, centerScreen.y); - ctx.lineTo(centerScreen.x + crossSize, centerScreen.y); - ctx.moveTo(centerScreen.x, centerScreen.y - crossSize); - ctx.lineTo(centerScreen.x, centerScreen.y + crossSize); - ctx.stroke(); - }, - [], - ); - - // Redraw on prop changes (not during drag — drag has its own rAF loop) - useEffect(() => { - if (!isDragging.current) drawSphere(rotation, tilt); - }, [rotation, tilt, drawSphere]); - - // Keep stable refs so mouse handlers are never stale - const drawSphereRef = useRef(drawSphere); - drawSphereRef.current = drawSphere; - const onDragEndRef = useRef(onDragEnd); - onDragEndRef.current = onDragEnd; - - // Window-level mouse listeners so dragging continues outside the canvas - useEffect(() => { - const onMove = (e: MouseEvent) => { - if (!isDragging.current) return; - const dx = e.clientX - lastPos.current.x; - const dy = e.clientY - lastPos.current.y; - lastPos.current = { x: e.clientX, y: e.clientY }; - - liveRotation.current += dx * 0.8; - liveTilt.current = Math.max( - -30, - Math.min(60, liveTilt.current - dy * 0.8), - ); - - if (rafId.current !== null) cancelAnimationFrame(rafId.current); - rafId.current = requestAnimationFrame(() => { - drawSphereRef.current(liveRotation.current, liveTilt.current, true); - rafId.current = null; - }); - }; - - const onUp = () => { - if (!isDragging.current) return; - isDragging.current = false; - - let rawRot = liveRotation.current % 360; - if (rawRot < 0) rawRot += 360; - const snappedRot = snapToNearest(rawRot, ROTATION_VALUES); - const snappedTilt = snapToNearest(liveTilt.current, TILT_VALUES); - - liveRotation.current = snappedRot; - liveTilt.current = snappedTilt; - - drawSphereRef.current(snappedRot, snappedTilt); - onDragEndRef.current(snappedRot, snappedTilt); - }; - - window.addEventListener("mousemove", onMove); - window.addEventListener("mouseup", onUp); - return () => { - window.removeEventListener("mousemove", onMove); - window.removeEventListener("mouseup", onUp); - }; - }, []); - - const handleMouseDown = (e: React.MouseEvent) => { - isDragging.current = true; - lastPos.current = { x: e.clientX, y: e.clientY }; - }; - - return ( - - ); - }, -); - -OrbitSphere.displayName = "OrbitSphere"; diff --git a/frontend/apps/artcraft/app/src/pages/PageAngles/index.ts b/frontend/apps/artcraft/app/src/pages/PageAngles/index.ts deleted file mode 100644 index 13a7558c..00000000 --- a/frontend/apps/artcraft/app/src/pages/PageAngles/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { Angles } from "./Angles"; -export { useAnglesStore } from "./AnglesStore"; diff --git a/frontend/apps/artcraft/app/src/pages/PageBackgroundChange/PageBackgroundChange.tsx b/frontend/apps/artcraft/app/src/pages/PageBackgroundChange/PageBackgroundChange.tsx deleted file mode 100644 index 5e08160f..00000000 --- a/frontend/apps/artcraft/app/src/pages/PageBackgroundChange/PageBackgroundChange.tsx +++ /dev/null @@ -1,203 +0,0 @@ -import { useCallback, useEffect, useRef, useState } from "react"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faSparkles } from "@fortawesome/pro-solid-svg-icons"; -import { toast } from "@storyteller/ui-toaster"; -import { UploadImageMedia, UploadVideoMedia } from "@storyteller/api"; -import { - PromptBoxVFX, - SubTabStrip, - TruchetPattern, - VFXResultCard, - VFXShowcaseView, - newIdempotencyToken, - submitVFXJob, - useVFXStore, - VFX_NOT_AVAILABLE_ERROR, - VFX_SHOWCASE, -} from "@storyteller/ui-vfx"; - -export const PageBackgroundChange = () => { - const subTab = useVFXStore((s) => s.subTab); - const setSubTab = useVFXStore((s) => s.setSubTab); - const history = useVFXStore((s) => s.history); - const startResult = useVFXStore((s) => s.startResult); - const failResult = useVFXStore((s) => s.failResult); - const dismissResult = useVFXStore((s) => s.dismissResult); - const loadFromShowcase = useVFXStore((s) => s.loadFromShowcase); - const selectedShowcaseId = useVFXStore((s) => s.selectedShowcaseId); - const source = useVFXStore((s) => s.source); - const mask = useVFXStore((s) => s.mask); - const reference = useVFXStore((s) => s.reference); - const prompt = useVFXStore((s) => s.prompt); - const resolution = useVFXStore((s) => s.resolution); - - const promptBoxRef = useRef(null); - const [promptBoxHeight, setPromptBoxHeight] = useState(96); - const [isSubmitting, setIsSubmitting] = useState(false); - - useEffect(() => { - const el = promptBoxRef.current; - if (!el || typeof ResizeObserver === "undefined") return; - const update = () => setPromptBoxHeight(el.offsetHeight); - update(); - const ro = new ResizeObserver(update); - ro.observe(el); - return () => ro.disconnect(); - }, []); - - const handleSubmit = useCallback(async () => { - if (!source || !reference || isSubmitting) return; - setIsSubmitting(true); - const id = startResult(); - const trimmed = prompt.trim(); - const result = await submitVFXJob({ - source_video_media_token: source.mediaToken, - reference_image_media_token: reference.mediaToken, - prompt: trimmed.length > 0 ? trimmed : null, - uuid_idempotency_token: newIdempotencyToken(), - }); - setIsSubmitting(false); - - if (!result.success) { - const isExpected = result.error_code_str === VFX_NOT_AVAILABLE_ERROR; - const message = isExpected - ? "Background change backend coming soon. Your inputs are saved." - : result.error_message || "Failed to submit background change job."; - failResult(id, message); - if (isExpected) toast(message); - else toast.error(message); - } - }, [source, reference, prompt, isSubmitting, startResult, failResult]); - - const handleTryShowcase = useCallback(() => { - const entry = VFX_SHOWCASE.find((e) => e.id === selectedShowcaseId); - if (!entry) return; - loadFromShowcase({ - prompt: entry.prompt, - resolution: entry.resolution, - source: { - id: entry.source.mediaToken, - url: entry.source.url, - mediaToken: entry.source.mediaToken, - }, - mask: entry.mask - ? { - id: entry.mask.mediaToken, - url: entry.mask.url, - mediaToken: entry.mask.mediaToken, - } - : undefined, - reference: entry.reference - ? { - id: entry.reference.mediaToken, - url: entry.reference.url, - mediaToken: entry.reference.mediaToken, - } - : undefined, - }); - toast("Loaded showcase. Edit and Generate."); - }, [selectedShowcaseId, loadFromShowcase]); - - return ( -
- - - {subTab === "showcase" ? ( -
- -
- ) : history.length === 0 ? ( -
-
- -
-
- -
-
- ) : ( -
-
- {history.map((r) => ( - dismissResult(r.id)} - /> - ))} -
-
- )} - -
- -
-
- toast.error(msg)} - /> -
-
-
- ); -}; - -interface EmptyStateProps { - title: string; - subtitle: string; -} - -const EmptyState = ({ title, subtitle }: EmptyStateProps) => ( -
-
- -
-

{title}

-

{subtitle}

-
-); - -export default PageBackgroundChange; diff --git a/frontend/apps/artcraft/app/src/pages/PageBackgroundChange/index.ts b/frontend/apps/artcraft/app/src/pages/PageBackgroundChange/index.ts deleted file mode 100644 index 6530419c..00000000 --- a/frontend/apps/artcraft/app/src/pages/PageBackgroundChange/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { PageBackgroundChange } from "./PageBackgroundChange"; diff --git a/frontend/apps/artcraft/app/src/pages/PageImageWatermarkRemover/ImageWatermarkRemover.tsx b/frontend/apps/artcraft/app/src/pages/PageImageWatermarkRemover/ImageWatermarkRemover.tsx deleted file mode 100644 index 2b34bdfe..00000000 --- a/frontend/apps/artcraft/app/src/pages/PageImageWatermarkRemover/ImageWatermarkRemover.tsx +++ /dev/null @@ -1,245 +0,0 @@ -import { useState, useRef, useEffect } from "react"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { - faImages, - faDroplet, - faArrowRotateRight, -} from "@fortawesome/pro-solid-svg-icons"; -import { Button } from "@storyteller/ui-button"; -import { GalleryItem, GalleryModal } from "@storyteller/ui-gallery-modal"; -import { downloadFileFromUrl } from "@storyteller/api"; -import { UploadEntryCard } from "../../components/media/UploadEntryCard"; -import toast from "react-hot-toast"; - -export const ImageWatermarkRemover = () => { - const [imageUrl, setImageUrl] = useState(""); - const [isGalleryModalOpen, setIsGalleryModalOpen] = useState(false); - const [selectedGalleryImages, setSelectedGalleryImages] = useState( - [], - ); - const [isProcessing, setIsProcessing] = useState(false); - const [isLoadingFromGallery, setIsLoadingFromGallery] = useState(false); - const [imageDimensions, setImageDimensions] = useState<{ - width: number; - height: number; - } | null>(null); - - const imageRef = useRef(null); - - useEffect(() => { - return () => { - if (imageUrl && imageUrl.startsWith("blob:")) { - URL.revokeObjectURL(imageUrl); - } - }; - }, [imageUrl]); - - const handleLocalImageSelect = (files: FileList) => { - const file = files[0]; - if (file && file.type.startsWith("image/")) { - if (imageUrl && imageUrl.startsWith("blob:")) { - URL.revokeObjectURL(imageUrl); - } - - const url = URL.createObjectURL(file); - setImageUrl(url); - setImageDimensions(null); - } - }; - - const handleImageSelect = (id: string) => { - setSelectedGalleryImages((prev) => { - if (prev.includes(id)) return prev.filter((x) => x !== id); - const maxSelections = 1; - if (prev.length >= maxSelections) { - return maxSelections === 1 ? [id] : prev; - } - return [...prev, id]; - }); - }; - - const handleGallerySelect = async (selectedItems: GalleryItem[]) => { - const item = selectedItems[0]; - if (!item || !item.fullImage) { - toast.error("No image selected"); - return; - } - - if (isLoadingFromGallery) { - return; - } - - setIsLoadingFromGallery(true); - - if (imageUrl && imageUrl.startsWith("blob:")) { - URL.revokeObjectURL(imageUrl); - } - - setImageUrl(item.fullImage); - setImageDimensions(null); - - setIsGalleryModalOpen(false); - setSelectedGalleryImages([]); - setIsLoadingFromGallery(false); - }; - - const handleRemoveWatermark = async () => { - if (!imageUrl) { - toast.error("Please select an image first"); - return; - } - - setIsProcessing(true); - toast.loading("Processing watermark removal...", { - id: "watermark-removal", - }); - - try { - await new Promise((resolve) => setTimeout(resolve, 2000)); - toast.success("Watermark removal completed!", { - id: "watermark-removal", - }); - } catch (error) { - toast.error("Failed to remove watermark", { id: "watermark-removal" }); - console.error("Error removing watermark:", error); - } finally { - setIsProcessing(false); - } - }; - - return ( - <> -
-
-
- {!imageUrl ? ( -
-
- setIsGalleryModalOpen(true)} - /> -
-
- ) : ( -
-
-
- -
- Selected pic { - const img = e.currentTarget; - setImageDimensions({ - width: img.naturalWidth, - height: img.naturalHeight, - }); - }} - /> -
-
-
- -
- -
- -
-
-
- - Image Information -
- {imageDimensions ? ( -
-
- - Resolution - - - {imageDimensions.width} × {imageDimensions.height} - -
-
- - Aspect Ratio - - - {( - imageDimensions.width / imageDimensions.height - ).toFixed(2)} - :1 - -
-
- ) : ( -
Loading...
- )} -
-
-
- )} -
-
-
- - { - if (!isLoadingFromGallery) { - setIsGalleryModalOpen(false); - setSelectedGalleryImages([]); - } - }} - mode="select" - selectedItemIds={selectedGalleryImages} - onSelectItem={handleImageSelect} - maxSelections={1} - onUseSelected={handleGallerySelect} - onDownloadClicked={downloadFileFromUrl} - forceFilter="image" - /> - - ); -}; diff --git a/frontend/apps/artcraft/app/src/pages/PageImageWatermarkRemover/index.ts b/frontend/apps/artcraft/app/src/pages/PageImageWatermarkRemover/index.ts deleted file mode 100644 index d1a42702..00000000 --- a/frontend/apps/artcraft/app/src/pages/PageImageWatermarkRemover/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { ImageWatermarkRemover } from "./ImageWatermarkRemover"; - diff --git a/frontend/apps/artcraft/app/src/pages/PageMoodboard/PageMoodboard.tsx b/frontend/apps/artcraft/app/src/pages/PageMoodboard/PageMoodboard.tsx deleted file mode 100644 index 60376a18..00000000 --- a/frontend/apps/artcraft/app/src/pages/PageMoodboard/PageMoodboard.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { useEffect } from "react"; -import { - MoodboardWorkspace, - dispatchGalleryMoodboardDrop, -} from "@storyteller/ui-moodboard"; -import { - GalleryItem, - onImageDrop, - removeImageDropListener, -} from "@storyteller/ui-gallery-modal"; -import { desktopMoodboardAdapter } from "./desktopMoodboardAdapter"; - -// Desktop moodboard tab. The shared workspace fills its parent; MainApp's -// TabBody wraps this in a sized container. -export const PageMoodboard = () => { - useGalleryDropBridge(); - return ; -}; - -// Bridges gallery-modal's onImageDrop into the moodboard's board-drop event, so -// dragging a library image onto the grid/canvas adds it. Mounted only while the -// moodboard tab is active, so it doesn't race the 2D/3D tab drop handlers. -const useGalleryDropBridge = () => { - useEffect(() => { - const handler = onImageDrop( - (item: GalleryItem, position: { x: number; y: number }) => { - dispatchGalleryMoodboardDrop(item, position); - }, - ); - return () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - if (handler) removeImageDropListener(handler as any); - }; - }, []); -}; diff --git a/frontend/apps/artcraft/app/src/pages/PageMoodboard/desktopMoodboardAdapter.tsx b/frontend/apps/artcraft/app/src/pages/PageMoodboard/desktopMoodboardAdapter.tsx deleted file mode 100644 index 17338105..00000000 --- a/frontend/apps/artcraft/app/src/pages/PageMoodboard/desktopMoodboardAdapter.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import { useState } from "react"; -import toast from "react-hot-toast"; -import { UploaderStates } from "@storyteller/common"; -import { UploadImageMedia } from "@storyteller/api"; -import { GalleryModal, GalleryItem } from "@storyteller/ui-gallery-modal"; -import { usePromptImageStore, RefImage } from "@storyteller/ui-promptbox"; -import type { - MoodboardAdapter, - MoodboardReference, - MoodboardLibraryPickerProps, -} from "@storyteller/ui-moodboard"; -import { useTabStore } from "../Stores/TabState"; - -// Desktop (Tauri) implementation of the moodboard's platform seams. - -const uploadImage = async (file: File): Promise => { - let token: string | null = null; - try { - await UploadImageMedia({ - title: file.name || "Moodboard image", - assetFile: file, - progressCallback: (state) => { - if (state.status === UploaderStates.success && state.data) { - token = String(state.data); - } - }, - }); - } catch (err) { - console.error("[Moodboard] upload failed", err); - } - return token; -}; - -const sendToGeneration = (refs: MoodboardReference[]): void => { - const newRefs: RefImage[] = refs.map((r) => ({ - id: r.id, - url: r.url, - file: new File([], r.id), - mediaToken: r.mediaToken, - })); - const store = usePromptImageStore.getState(); - store.setReferenceImages(dedupeByToken([...store.referenceImages, ...newRefs])); - void useTabStore.getState().setActiveTab("IMAGE"); - toast.success( - `${refs.length} reference${refs.length > 1 ? "s" : ""} added to the prompt`, - ); -}; - -const DesktopLibraryPicker = ({ - open, - onClose, - onPick, -}: MoodboardLibraryPickerProps) => { - const [selectedIds, setSelectedIds] = useState([]); - const close = () => { - setSelectedIds([]); - onClose(); - }; - return ( - - setSelectedIds((prev) => - prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id], - ) - } - onUseSelected={(items: GalleryItem[]) => { - onPick( - items.map((it) => ({ - url: it.fullImage || it.thumbnail || "", - mediaToken: it.id ?? null, - kind: it.mediaClass === "video" ? "video" : "image", - })), - ); - setSelectedIds([]); - }} - forceFilter="image" - /> - ); -}; - -export const desktopMoodboardAdapter: MoodboardAdapter = { - uploadImage, - sendToGeneration, - renderLibraryPicker: (props) => , -}; - -const dedupeByToken = (refs: RefImage[]): RefImage[] => { - const seen = new Set(); - const out: RefImage[] = []; - for (const r of refs) { - if (r.mediaToken) { - if (seen.has(r.mediaToken)) continue; - seen.add(r.mediaToken); - } - out.push(r); - } - return out; -}; diff --git a/frontend/apps/artcraft/app/src/pages/PageMoodboard/index.ts b/frontend/apps/artcraft/app/src/pages/PageMoodboard/index.ts deleted file mode 100644 index 4cb4346f..00000000 --- a/frontend/apps/artcraft/app/src/pages/PageMoodboard/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { PageMoodboard } from "./PageMoodboard"; -export { desktopMoodboardAdapter } from "./desktopMoodboardAdapter"; diff --git a/frontend/apps/artcraft/app/src/pages/PageSettings/PageSettings.tsx b/frontend/apps/artcraft/app/src/pages/PageSettings/PageSettings.tsx index 88bab73b..c4c1c1c9 100644 --- a/frontend/apps/artcraft/app/src/pages/PageSettings/PageSettings.tsx +++ b/frontend/apps/artcraft/app/src/pages/PageSettings/PageSettings.tsx @@ -1,6 +1,5 @@ import { SettingsContent } from "@storyteller/ui-settings-modal"; import { goToApp } from "~/config/appMenu"; -import { useStoryboardStore } from "~/pages/PageStoryboard"; import { setLogoutStates } from "~/signals/authentication/utilities"; // Settings as a full page (opened from the titlebar gear) instead of a modal. @@ -14,10 +13,6 @@ export const PageSettings = () => { setLogoutStates(); goToApp("IMAGE"); }} - onStoryboardPageDisable={() => { - useStoryboardStore.getState().reset(); - goToApp("IMAGE"); - }} />
); diff --git a/frontend/apps/artcraft/app/src/pages/PageStoryboard/Storyboard.tsx b/frontend/apps/artcraft/app/src/pages/PageStoryboard/Storyboard.tsx deleted file mode 100644 index 7577987d..00000000 --- a/frontend/apps/artcraft/app/src/pages/PageStoryboard/Storyboard.tsx +++ /dev/null @@ -1,1276 +0,0 @@ -import { useState, useRef, useCallback, useMemo, useEffect } from "react"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { - faPlus, - faTrash, - faUpload, - faPhotoFilm, - faPlay, - faPause, - faBackwardStep, - faForwardStep, - faBackwardFast, - faForwardFast, - faPencil, - faEraser, - faRotateLeft, - faXmark, - faCheck, -} from "@fortawesome/pro-solid-svg-icons"; -import { Button } from "@storyteller/ui-button"; -import { twMerge } from "tailwind-merge"; -import { useStoryboardStore, type Board } from "./StoryboardStore"; - -const PIXELS_PER_SECOND = 80; -const MIN_BLOCK_WIDTH = 40; -const MIN_DURATION = 0.5; -const CANVAS_W = 1280; -const CANVAS_H = 720; -const MAX_UNDO = 20; - -// ─── Helpers ────────────────────────────────────────────────────────────────── - -interface BoardTiming { - board: Board; - startTime: number; -} - -const buildBoardTimings = (boards: Board[]): BoardTiming[] => { - let t = 0; - return boards.map((board) => { - const startTime = t; - t += board.duration; - return { board, startTime }; - }); -}; - -const formatTime = (s: number): string => { - const clamped = Math.max(0, s); - const mins = Math.floor(clamped / 60); - const secs = (clamped % 60).toFixed(1); - return mins > 0 ? `${mins}:${secs.padStart(4, "0")}` : `${secs}s`; -}; - -// ─── EmptyState ─────────────────────────────────────────────────────────────── - -const EmptyState = ({ onAdd }: { onAdd: () => void }) => ( -
-
-
- -
-
-

No shots yet

-

Add your first shot to get started

-
- -
-
-); - -// ─── MetadataEditor ─────────────────────────────────────────────────────────── - -interface MetadataEditorProps { - board: Board; - onUpdate: (patch: Partial>) => void; - onDelete: () => void; - onUpload: () => void; - onSketch: () => void; -} - -const MetadataEditor = ({ board, onUpdate, onDelete, onUpload, onSketch }: MetadataEditorProps) => ( -