diff --git a/monosketch-svelte/src/app/app-context.ts b/monosketch-svelte/src/app/app-context.ts index ac11a75f9..99d2ea967 100644 --- a/monosketch-svelte/src/app/app-context.ts +++ b/monosketch-svelte/src/app/app-context.ts @@ -16,23 +16,24 @@ export class AppContext { shapeManager = new ShapeManager(); + actionManager = new ActionManager(this.appLifecycleOwner); + onStart = (): void => { this.appLifecycleOwner.onStart(); this.init(); this.appUiStateManager.observeTheme(); + this.actionManager.observeKeyCommand( + this.appUiStateManager.keyCommandFlow.map((keyCommand) => keyCommand.command), + ); }; private init() { - const actionManager = new ActionManager( - this.appLifecycleOwner, - this.appUiStateManager.keyCommandFlow.map((keyCommand) => keyCommand.command), - ); - actionManager.installDebugCommand(); + this.actionManager.installDebugCommand(); const browserManager = new BrowserManager((projectId: string) => { - actionManager.setOneTimeAction(OneTimeAction.ProjectAction.SwitchProject(projectId)); + this.actionManager.setOneTimeAction(OneTimeAction.ProjectAction.SwitchProject(projectId)); }); browserManager.startObserveStateChange( this.shapeManager.rootIdFlow, diff --git a/monosketch-svelte/src/lib/libs/sequence.ts b/monosketch-svelte/src/lib/libs/sequence.ts index 058079b6c..97686ba54 100644 --- a/monosketch-svelte/src/lib/libs/sequence.ts +++ b/monosketch-svelte/src/lib/libs/sequence.ts @@ -145,6 +145,17 @@ export function getOrDefault(array: T[], index: number, defaultValue: T): T { return index >= 0 && index < array.length ? array[index] : defaultValue; } +export function singleOrNull(array: T[] | Set): T | null { + if (array instanceof Set) { + if (array.size === 1) { + return array.values().next().value ?? null; + } else { + return null; + } + } + return array.length === 1 ? array[0] : null; +} + export namespace ListExt { /** * Create a list of the specified size with the specified value. diff --git a/monosketch-svelte/src/lib/mono/action-manager/action-manager.ts b/monosketch-svelte/src/lib/mono/action-manager/action-manager.ts index ba169d7ac..b9178d130 100644 --- a/monosketch-svelte/src/lib/mono/action-manager/action-manager.ts +++ b/monosketch-svelte/src/lib/mono/action-manager/action-manager.ts @@ -12,11 +12,17 @@ import { type OneTimeActionType, OneTimeAction } from './one-time-actions'; * A class which gathers UI events and converts them into equivalent command. */ export class ActionManager { - private retainableActionFlow: Flow = new Flow(RetainableActionType.IDLE); - private oneTimeActionFlow: Flow = new Flow(OneTimeAction.Idle); + private retainableActionMutableFlow: Flow = new Flow(RetainableActionType.IDLE); + retainableActionFlow = this.retainableActionMutableFlow.distinctUntilChanged(); - constructor(lifecycleOwner: LifecycleOwner, keyCommandFlow: Flow) { - keyCommandFlow.distinctUntilChanged().observe(lifecycleOwner, this.onKeyEvent.bind(this)); + private oneTimeActionMutableFlow: Flow = new Flow(OneTimeAction.Idle); + oneTimeActionFlow = this.oneTimeActionMutableFlow.immutable(); + + constructor(private lifecycleOwner: LifecycleOwner) { + } + + observeKeyCommand(keyCommandFlow: Flow) { + keyCommandFlow.distinctUntilChanged().observe(this.lifecycleOwner, this.onKeyEvent.bind(this)); } private onKeyEvent(keyCommand: KeyCommandType) { @@ -97,12 +103,12 @@ export class ActionManager { } setRetainableAction(actionType: RetainableActionType) { - this.retainableActionFlow.value = actionType; + this.retainableActionMutableFlow.value = actionType; } setOneTimeAction(actionType: OneTimeActionType) { - this.oneTimeActionFlow.value = actionType; - this.oneTimeActionFlow.value = OneTimeAction.Idle; + this.oneTimeActionMutableFlow.value = actionType; + this.oneTimeActionMutableFlow.value = OneTimeAction.Idle; } installDebugCommand() { diff --git a/monosketch-svelte/src/lib/mono/keycommand/keycommands.ts b/monosketch-svelte/src/lib/mono/keycommand/keycommands.ts index 27a978f05..81c16ef87 100644 --- a/monosketch-svelte/src/lib/mono/keycommand/keycommands.ts +++ b/monosketch-svelte/src/lib/mono/keycommand/keycommands.ts @@ -9,7 +9,7 @@ const KeyCommandOptionsDefaults: KeyCommand = { isRepeatable: false, }; -export const KeyCommands: KeyCommand[] = [ +const KeyCommands: KeyCommand[] = [ { ...KeyCommandOptionsDefaults, command: KeyCommandType.IDLE, diff --git a/monosketch-svelte/src/lib/ui/nav/mouseaction/MouseActionButton.svelte b/monosketch-svelte/src/lib/ui/nav/mouseaction/MouseActionButton.svelte index e40e09eaa..427c08019 100644 --- a/monosketch-svelte/src/lib/ui/nav/mouseaction/MouseActionButton.svelte +++ b/monosketch-svelte/src/lib/ui/nav/mouseaction/MouseActionButton.svelte @@ -1,5 +1,5 @@ - + diff --git a/monosketch-svelte/src/lib/ui/nav/mouseaction/MouseActionGroup.svelte b/monosketch-svelte/src/lib/ui/nav/mouseaction/MouseActionGroup.svelte index a3c02f55e..c11bf2c7a 100644 --- a/monosketch-svelte/src/lib/ui/nav/mouseaction/MouseActionGroup.svelte +++ b/monosketch-svelte/src/lib/ui/nav/mouseaction/MouseActionGroup.svelte @@ -1,12 +1,37 @@
diff --git a/monosketch-svelte/src/lib/ui/nav/mouseaction/model.ts b/monosketch-svelte/src/lib/ui/nav/mouseaction/model.ts index fcb61eff5..eb67774b5 100644 --- a/monosketch-svelte/src/lib/ui/nav/mouseaction/model.ts +++ b/monosketch-svelte/src/lib/ui/nav/mouseaction/model.ts @@ -1,3 +1,5 @@ +import { RetainableActionType } from "$mono/action-manager/retainable-actions"; + export enum MouseActionType { SELECTION, ADD_RECTANGLE, @@ -12,22 +14,39 @@ export const mouseActionTypes = [ MouseActionType.ADD_LINE, ]; -export const mouseActionToContentMap = { +interface MouseAction { + retainableActionType: RetainableActionType; + iconPath: string; + title: string; +} + +export const MouseActionToContentMap: Record = { [MouseActionType.SELECTION]: { + retainableActionType: RetainableActionType.IDLE, iconPath: 'M7.436 20.61L7.275 3.914l12.296 11.29-7.165.235-4.97 5.168z', title: 'Select (V)', }, [MouseActionType.ADD_RECTANGLE]: { + retainableActionType: RetainableActionType.ADD_RECTANGLE, iconPath: 'M22 19H2V5h20v14zM4 7v10h16V7z', title: 'Rectangle (R)', }, [MouseActionType.ADD_TEXT]: { + retainableActionType: RetainableActionType.ADD_TEXT, iconPath: 'M5.635 21v-2h12.731v2zm3.27-4v-1.12h2.005V4.12H7.425l-.39.44v2.58h-1.4V3h12.731v4.14h-1.4V4.56l-.39-.44h-3.485v11.76h2.005V17z', title: 'Text (T)', }, [MouseActionType.ADD_LINE]: { + retainableActionType: RetainableActionType.ADD_LINE, iconPath: 'M18 15v-2H6v2H0V9h6v2h12V9h6v6z', title: 'Line (L)', }, }; + +export const RetainableActionTypeToMouseActionTypeMap: Record = { + [RetainableActionType.IDLE]: MouseActionType.SELECTION, + [RetainableActionType.ADD_RECTANGLE]: MouseActionType.ADD_RECTANGLE, + [RetainableActionType.ADD_TEXT]: MouseActionType.ADD_TEXT, + [RetainableActionType.ADD_LINE]: MouseActionType.ADD_LINE, +}; diff --git a/monosketch-svelte/src/lib/ui/pannel/common/NumberTextField.svelte b/monosketch-svelte/src/lib/ui/pannel/common/NumberTextField.svelte index 3fd0ab248..c0088da9a 100644 --- a/monosketch-svelte/src/lib/ui/pannel/common/NumberTextField.svelte +++ b/monosketch-svelte/src/lib/ui/pannel/common/NumberTextField.svelte @@ -2,7 +2,7 @@ export let label: string; export let value: number; export let minValue: number | null = null; -export let isEnabled = true; +export let isEnabled: boolean = true; export let boundIncludesLabel = false; diff --git a/monosketch-svelte/src/lib/ui/pannel/shapetool/Footer.svelte b/monosketch-svelte/src/lib/ui/pannel/shapetool/Footer.svelte new file mode 100644 index 000000000..1f4e93a7f --- /dev/null +++ b/monosketch-svelte/src/lib/ui/pannel/shapetool/Footer.svelte @@ -0,0 +1,44 @@ + + + + + + diff --git a/monosketch-svelte/src/lib/ui/pannel/shapetool/ShapeTool.svelte b/monosketch-svelte/src/lib/ui/pannel/shapetool/ShapeTool.svelte index 6337b8de8..49895a0d5 100644 --- a/monosketch-svelte/src/lib/ui/pannel/shapetool/ShapeTool.svelte +++ b/monosketch-svelte/src/lib/ui/pannel/shapetool/ShapeTool.svelte @@ -1,65 +1,65 @@ {#if isVisible}
- - - - +
- +
{/if} diff --git a/monosketch-svelte/src/lib/ui/pannel/shapetool/ShapeToolBody.svelte b/monosketch-svelte/src/lib/ui/pannel/shapetool/ShapeToolBody.svelte new file mode 100644 index 000000000..747b9f89a --- /dev/null +++ b/monosketch-svelte/src/lib/ui/pannel/shapetool/ShapeToolBody.svelte @@ -0,0 +1,94 @@ + + + + +{#if isReorderToolVisible } + +{/if} + +{#if isTransformToolVisible } + +{/if} + +{#if isAppearanceToolVisible} + +{/if} + +{#if isTextToolVisible } + +{/if} + +{#if !hasAnyVisibleTool} +
+ Select a shape for updating its properties here +
+{/if} + + diff --git a/monosketch-svelte/src/lib/ui/pannel/shapetool/appearance/AppearanceTool.svelte b/monosketch-svelte/src/lib/ui/pannel/shapetool/appearance/AppearanceTool.svelte index 7eddb9a1e..527805fc1 100644 --- a/monosketch-svelte/src/lib/ui/pannel/shapetool/appearance/AppearanceTool.svelte +++ b/monosketch-svelte/src/lib/ui/pannel/shapetool/appearance/AppearanceTool.svelte @@ -1,16 +1,16 @@
- - - - - + + + + +
diff --git a/monosketch-svelte/src/lib/ui/pannel/shapetool/appearance/FillTool.svelte b/monosketch-svelte/src/lib/ui/pannel/shapetool/appearance/FillTool.svelte index 4e8282f43..dbe9c23ac 100644 --- a/monosketch-svelte/src/lib/ui/pannel/shapetool/appearance/FillTool.svelte +++ b/monosketch-svelte/src/lib/ui/pannel/shapetool/appearance/FillTool.svelte @@ -1,7 +1,11 @@
- {#each strokeOptions as option} + {#each viewModel.strokeOptions as option} - {option.title} + {option.name} {/each}
@@ -41,33 +45,33 @@ function onRoundedCornerButtonClick() { {#if isCornerRoundable}
{/if}
- +
diff --git a/monosketch-svelte/src/lib/ui/pannel/shapetool/appearance/common/CommonLineAnchorTool.svelte b/monosketch-svelte/src/lib/ui/pannel/shapetool/appearance/common/CommonLineAnchorTool.svelte index 1beff9649..3eea44e2e 100644 --- a/monosketch-svelte/src/lib/ui/pannel/shapetool/appearance/common/CommonLineAnchorTool.svelte +++ b/monosketch-svelte/src/lib/ui/pannel/shapetool/appearance/common/CommonLineAnchorTool.svelte @@ -1,7 +1,11 @@
- +
- +
diff --git a/monosketch-svelte/src/lib/ui/pannel/shapetool/viewmodel/line-appearance-data-controller.ts b/monosketch-svelte/src/lib/ui/pannel/shapetool/viewmodel/line-appearance-data-controller.ts new file mode 100644 index 000000000..5abf9a1bf --- /dev/null +++ b/monosketch-svelte/src/lib/ui/pannel/shapetool/viewmodel/line-appearance-data-controller.ts @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2024, tuanchauict + */ + +import { Flow } from "$libs/flow"; +import { singleOrNull } from "$libs/sequence"; +import { RetainableActionType } from "$mono/action-manager/retainable-actions"; +import { type ILineExtra, ShapeExtraManager } from "$mono/shape/extra/extra-manager"; +import type { LineExtra } from "$mono/shape/extra/shape-extra"; +import { type StraightStrokeDashPattern } from "$mono/shape/extra/style"; +import type { AbstractShape } from "$mono/shape/shape/abstract-shape"; +import { Line } from "$mono/shape/shape/line"; +import { selectedOrDefault, type CloudItemSelectionState } from "./models"; + +/** + * A class which manages the appearance data of a line related shape. + */ +export class LineAppearanceDataController { + private singleLineExtraFlow: Flow; + private defaultLineExtraFlow: Flow; + + public strokeToolStateFlow: Flow; + public strokeDashPatternFlow: Flow; + public strokeRoundedCornerFlow: Flow; + public startHeadToolStateFlow: Flow; + public endHeadToolStateFlow: Flow; + public hasAnyVisibleToolFlow: Flow; + + constructor( + shapesFlow: Flow>, + retainableActionFlow: Flow, + ) { + this.singleLineExtraFlow = shapesFlow.map((shapes) => { + const shape = singleOrNull(shapes); + if (shape instanceof Line) { + return shape.extra; + } + return null; + }); + + this.defaultLineExtraFlow = retainableActionFlow.map((actionType) => { + if (actionType === RetainableActionType.ADD_LINE) { + return ShapeExtraManager.defaultLineExtra; + } + return null; + }); + + this.strokeToolStateFlow = this.createLineStrokeAppearanceVisibilityFlow(); + this.strokeDashPatternFlow = this.createLineStrokeDashPatternFlow(); + this.strokeRoundedCornerFlow = this.createLineStrokeRoundedCornerFlow(); + this.startHeadToolStateFlow = this.createStartHeadAppearanceVisibilityFlow(); + this.endHeadToolStateFlow = this.createEndHeadAppearanceVisibilityFlow(); + + this.hasAnyVisibleToolFlow = Flow.combineList( + [this.strokeToolStateFlow, + this.strokeDashPatternFlow, + this.strokeRoundedCornerFlow, + this.startHeadToolStateFlow, + this.endHeadToolStateFlow], + (list) => list.some((item) => item !== null), + ); + } + + private createLineStrokeAppearanceVisibilityFlow(): Flow { + return selectedOrDefault({ + selectedFlow: this.singleLineExtraFlow.map(createStrokeState), + defaultFlow: this.defaultLineExtraFlow.map(createStrokeState), + }); + } + + private createLineStrokeDashPatternFlow(): Flow { + return selectedOrDefault({ + selectedFlow: this.singleLineExtraFlow.map((extra) => extra?.dashPattern ?? null), + defaultFlow: this.defaultLineExtraFlow.map((extra) => extra?.dashPattern ?? null), + }); + } + + private createLineStrokeRoundedCornerFlow(): Flow { + return selectedOrDefault({ + selectedFlow: this.singleLineExtraFlow.map(createStrokeRoundedCornerState), + defaultFlow: this.defaultLineExtraFlow.map(createStrokeRoundedCornerState), + }); + } + + private createStartHeadAppearanceVisibilityFlow(): Flow { + return selectedOrDefault({ + selectedFlow: this.singleLineExtraFlow.map(createStartHeadState), + defaultFlow: this.defaultLineExtraFlow.map(createStartHeadState), + }); + } + + private createEndHeadAppearanceVisibilityFlow(): Flow { + return selectedOrDefault({ + selectedFlow: this.singleLineExtraFlow.map(createEndHeadState), + defaultFlow: this.defaultLineExtraFlow.map(createEndHeadState), + }); + } +} + +function createStrokeState(extra: ILineExtra | null): CloudItemSelectionState | null { + return extra == null ? null : { isChecked: extra.isStrokeEnabled, selectedId: extra.userSelectedStrokeStyle.id }; +} + +function createStrokeRoundedCornerState(extra: ILineExtra | null): boolean | null { + if (extra == null) { + return null; + } + return extra.isStrokeEnabled ? extra.isRoundedCorner : null; +} + +function createStartHeadState(extra: ILineExtra | null): CloudItemSelectionState | null { + return extra == null ? null : { + isChecked: extra.isStartAnchorEnabled, + selectedId: extra.userSelectedStartAnchor.id, + }; +} + +function createEndHeadState(extra: ILineExtra | null): CloudItemSelectionState | null { + return extra == null ? null : { isChecked: extra.isEndAnchorEnabled, selectedId: extra.userSelectedEndAnchor.id }; +} diff --git a/monosketch-svelte/src/lib/ui/pannel/shapetool/viewmodel/models.ts b/monosketch-svelte/src/lib/ui/pannel/shapetool/viewmodel/models.ts new file mode 100644 index 000000000..a59025d9c --- /dev/null +++ b/monosketch-svelte/src/lib/ui/pannel/shapetool/viewmodel/models.ts @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2024, tuanchauict + */ + +import { Flow } from "$libs/flow"; + +export interface CloudItemSelectionState { + isChecked: boolean; + selectedId: string; +} + +export interface AppearanceOptionItem { + id: string; + name: string; + useDashBorder: boolean; +} + +export function selectedOrDefault({ selectedFlow, defaultFlow }: { + selectedFlow: Flow, + defaultFlow: Flow +}): Flow { + return Flow.combine2(selectedFlow, defaultFlow, (a, b) => a ?? b); +} diff --git a/monosketch-svelte/src/lib/ui/pannel/shapetool/viewmodel/rectangle-appearance-data-controller.ts b/monosketch-svelte/src/lib/ui/pannel/shapetool/viewmodel/rectangle-appearance-data-controller.ts new file mode 100644 index 000000000..dbc051236 --- /dev/null +++ b/monosketch-svelte/src/lib/ui/pannel/shapetool/viewmodel/rectangle-appearance-data-controller.ts @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2024, tuanchauict + */ + +import { Flow } from "$libs/flow"; +import { singleOrNull } from "$libs/sequence"; +import { RetainableActionType } from "$mono/action-manager/retainable-actions"; +import { type IRectangleExtra, ShapeExtraManager } from "$mono/shape/extra/extra-manager"; +import type { RectangleExtra } from "$mono/shape/extra/shape-extra"; +import { RectangleBorderCornerPattern, type StraightStrokeDashPattern } from "$mono/shape/extra/style"; +import type { AbstractShape } from "$mono/shape/shape/abstract-shape"; +import { Rectangle } from "$mono/shape/shape/rectangle"; +import { Text } from "$mono/shape/shape/text"; +import { type CloudItemSelectionState, selectedOrDefault } from "$ui/pannel/shapetool/viewmodel/models"; + +/** + * A class which manages the appearance data of a rectangle related shape. + */ +export class RectangleAppearanceDataController { + private singleRectExtraFlow: Flow; + private defaultRectangleExtraFlow: Flow; + + public fillToolStateFlow: Flow; + public borderToolStateFlow: Flow; + public borderDashPatternFlow: Flow; + public borderRoundedCornerFlow: Flow; + public hasAnyVisibleToolFlow: Flow; + + constructor( + shapesFlow: Flow>, + retainableActionFlow: Flow, + ) { + this.singleRectExtraFlow = shapesFlow.map((shapes) => { + const shape = singleOrNull(shapes); + if (shape instanceof Rectangle) { + return shape.extra; + } else if (shape instanceof Text) { + return shape.extra.boundExtra; + } + return null; + }); + + this.defaultRectangleExtraFlow = retainableActionFlow.map((actionType) => { + if (actionType === RetainableActionType.ADD_RECTANGLE || actionType === RetainableActionType.ADD_TEXT) { + return ShapeExtraManager.defaultRectangleExtra; + } + return null; + }); + + this.fillToolStateFlow = this.createFillAppearanceVisibilityFlow(); + this.borderToolStateFlow = this.createBorderAppearanceVisibilityFlow(); + this.borderDashPatternFlow = this.createBorderDashPatternFlow(); + this.borderRoundedCornerFlow = this.createBorderRoundedCornerFlow(); + + this.hasAnyVisibleToolFlow = Flow.combineList( + [ + this.fillToolStateFlow, + this.borderToolStateFlow, + this.borderDashPatternFlow, + this.borderRoundedCornerFlow, + ], + (array) => array.some((item) => item != null), + ); + } + + private createFillAppearanceVisibilityFlow(): Flow { + return selectedOrDefault( + { + selectedFlow: this.singleRectExtraFlow.map(createFillAppearanceVisibilityState), + defaultFlow: this.defaultRectangleExtraFlow.map(createFillAppearanceVisibilityState), + }, + ); + } + + private createBorderAppearanceVisibilityFlow(): Flow { + return selectedOrDefault({ + selectedFlow: this.singleRectExtraFlow.map(createBorderState), + defaultFlow: this.defaultRectangleExtraFlow.map(createBorderState), + }); + } + + private createBorderDashPatternFlow(): Flow { + return selectedOrDefault({ + selectedFlow: this.singleRectExtraFlow.map((extra) => extra?.dashPattern ?? null), + defaultFlow: this.defaultRectangleExtraFlow.map((extra) => extra?.dashPattern ?? null), + }); + } + + private createBorderRoundedCornerFlow(): Flow { + return selectedOrDefault({ + selectedFlow: this.singleRectExtraFlow.map(createBorderRoundedCornerState), + defaultFlow: this.defaultRectangleExtraFlow.map(createBorderRoundedCornerState), + }); + } +} + +function createFillAppearanceVisibilityState(extra: IRectangleExtra | null): CloudItemSelectionState | null { + return extra == null ? null : { isChecked: extra.isFillEnabled, selectedId: extra.userSelectedFillStyle.id }; +} + +function createBorderState(extra: IRectangleExtra | null): CloudItemSelectionState | null { + return extra == null ? null : { isChecked: extra.isBorderEnabled, selectedId: extra.userSelectedBorderStyle.id }; +} + +function createBorderRoundedCornerState(extra: IRectangleExtra | null): boolean | null { + if (extra == null) { + return null; + } + return extra.isBorderEnabled ? extra.corner == RectangleBorderCornerPattern.ENABLED : null; +} diff --git a/monosketch-svelte/src/lib/ui/pannel/shapetool/viewmodel/shape-tool-viewmodel.ts b/monosketch-svelte/src/lib/ui/pannel/shapetool/viewmodel/shape-tool-viewmodel.ts new file mode 100644 index 000000000..0f5065269 --- /dev/null +++ b/monosketch-svelte/src/lib/ui/pannel/shapetool/viewmodel/shape-tool-viewmodel.ts @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2024, tuanchauict + */ + +import { Flow } from "$libs/flow"; +import type { Rect } from "$libs/graphics-geo/rect"; +import { singleOrNull } from "$libs/sequence"; +import type { ActionManager } from "$mono/action-manager/action-manager"; +import { RetainableActionType } from "$mono/action-manager/retainable-actions"; +import { ShapeExtraManager } from "$mono/shape/extra/extra-manager"; +import type { StraightStrokeDashPattern, TextAlign } from "$mono/shape/extra/style"; +import type { AbstractShape } from "$mono/shape/shape/abstract-shape"; +import { Rectangle } from "$mono/shape/shape/rectangle"; +import { Text } from "$mono/shape/shape/text"; +import { LineAppearanceDataController } from "./line-appearance-data-controller"; +import { type AppearanceOptionItem, type CloudItemSelectionState, selectedOrDefault } from "./models"; +import { RectangleAppearanceDataController } from "./rectangle-appearance-data-controller"; + +/** + * A view model for the shape tool panel. + */ +export class ShapeToolViewModel { + private readonly shapesFlow: Flow>; + private readonly retainableActionTypeFlow: Flow; + + private readonly rectangleAppearanceDataController: RectangleAppearanceDataController; + private readonly lineAppearanceDataController: LineAppearanceDataController; + + public readonly reorderToolVisibilityFlow: Flow; + public readonly singleShapeBoundFlow: Flow; + public readonly singleShapeResizeableFlow: Flow; + + public readonly shapeFillTypeFlow: Flow; + public readonly shapeBorderTypeFlow: Flow; + public readonly shapeBorderDashTypeFlow: Flow; + public readonly shapeBorderRoundedCornerFlow: Flow; + + public readonly lineStrokeTypeFlow: Flow; + public readonly lineStrokeDashTypeFlow: Flow; + public readonly lineStrokeRoundedCornerFlow: Flow; + + public readonly lineStartHeadFlow: Flow; + public readonly lineEndHeadFlow: Flow; + + public readonly appearanceVisibilityFlow: Flow; + + public readonly textAlignFlow: Flow; + + public readonly fillOptions: AppearanceOptionItem[] = + ShapeExtraManager.getAllPredefinedRectangleFillStyles().map(({ id, displayName }) => ({ + id, + name: displayName, + useDashBorder: id === 'F1', + })); + + public readonly strokeOptions: AppearanceOptionItem[] = + ShapeExtraManager.getAllPredefinedStrokeStyles().map(({ id, displayName }) => ({ + id, + name: displayName, + useDashBorder: false, + })); + public readonly headOptions: AppearanceOptionItem[] = + ShapeExtraManager.getAllPredefinedAnchorChars().map(({ id, displayName }) => ({ + id, + name: displayName, + useDashBorder: false, + })); + + constructor( + selectedShapesFlow: Flow>, + shapeManagerVersionFlow: Flow, + actionManager: ActionManager, + ) { + this.shapesFlow = Flow.combine2( + selectedShapesFlow, + shapeManagerVersionFlow, + (selected, _) => selected, + ); + + this.retainableActionTypeFlow = Flow.combine2( + actionManager.retainableActionFlow, + ShapeExtraManager.defaultExtraStateUpdateFlow, + (action, _) => action, + ); + + this.rectangleAppearanceDataController = new RectangleAppearanceDataController( + this.shapesFlow, + this.retainableActionTypeFlow, + ); + + this.lineAppearanceDataController = new LineAppearanceDataController( + this.shapesFlow, + this.retainableActionTypeFlow, + ); + + const singleShapeFlow = Flow.combine2( + selectedShapesFlow, + shapeManagerVersionFlow, + (selected) => singleOrNull(selected), + ); + + const retainableActionFlow = Flow.combine2( + actionManager.retainableActionFlow, + ShapeExtraManager.defaultExtraStateUpdateFlow, + (action, _) => action, + ); + + this.reorderToolVisibilityFlow = singleShapeFlow.map((shape) => shape !== null); + this.singleShapeBoundFlow = singleShapeFlow.map((shape) => shape?.bound ?? null); + this.singleShapeResizeableFlow = singleShapeFlow.map((shape) => shape instanceof Rectangle || shape instanceof Text); + + this.shapeFillTypeFlow = this.rectangleAppearanceDataController.fillToolStateFlow; + this.shapeBorderTypeFlow = this.rectangleAppearanceDataController.borderToolStateFlow; + this.shapeBorderDashTypeFlow = this.rectangleAppearanceDataController.borderDashPatternFlow; + this.shapeBorderRoundedCornerFlow = this.rectangleAppearanceDataController.borderRoundedCornerFlow; + + this.lineStrokeTypeFlow = this.lineAppearanceDataController.strokeToolStateFlow; + this.lineStrokeDashTypeFlow = this.lineAppearanceDataController.strokeDashPatternFlow; + this.lineStrokeRoundedCornerFlow = this.lineAppearanceDataController.strokeRoundedCornerFlow; + this.lineStartHeadFlow = this.lineAppearanceDataController.startHeadToolStateFlow; + this.lineEndHeadFlow = this.lineAppearanceDataController.endHeadToolStateFlow; + + this.appearanceVisibilityFlow = Flow.combine2( + this.rectangleAppearanceDataController.hasAnyVisibleToolFlow, + this.lineAppearanceDataController.hasAnyVisibleToolFlow, + (isRectAvailable, isLineAvailable) => isRectAvailable || isLineAvailable, + ); + + this.textAlignFlow = this.createTextAlignFlow(singleShapeFlow, retainableActionFlow); + } + + private createTextAlignFlow( + selectedShapeFlow: Flow, + retainableActionTypeFlow: Flow, + ): Flow { + const selectedTextAlignFlow = selectedShapeFlow.map((shape) => { + const text = shape as Text | null; + return text?.isTextEditable ? text.extra.textAlign : null; + }); + + const defaultTextAlignFlow = retainableActionTypeFlow.map((actionType) => { + return actionType === RetainableActionType.ADD_TEXT ? ShapeExtraManager.defaultTextAlign : null; + }); + + return selectedOrDefault({ + selectedFlow: selectedTextAlignFlow, + defaultFlow: defaultTextAlignFlow, + }); + } +}