diff --git a/src/components/grapher/Dot.vue b/src/components/grapher/Dot.vue index 83d0a67..9688162 100644 --- a/src/components/grapher/Dot.vue +++ b/src/components/grapher/Dot.vue @@ -48,22 +48,41 @@ import Vue from "vue"; import StuntSheet from "@/models/StuntSheet"; import DotAppearance from "@/models/DotAppearance"; +const nextSSDotAppearance = new DotAppearance({ + filled: true, + fill: "purple", + color: "purple", +}); + +const nextSSUnconnectedDotAppearance = new DotAppearance({ + ...nextSSDotAppearance, + fill: "deeppink", + color: "deeppink", +}); + /** * Renders a single dot */ export default Vue.extend({ name: "Dot", props: { + isNextSS: Boolean, dotTypeIndex: Number, label: String, labeled: Boolean, selected: Boolean, + isConnected: Boolean, }, computed: { radius(): number { - return 0.7; + return this.$props.isNextSS ? 0.5 : 0.7; }, dotAppearance(): DotAppearance { + if (this.$props.isNextSS) { + return this.$props.isConnected + ? nextSSDotAppearance + : nextSSUnconnectedDotAppearance; + } const currentSS: StuntSheet = this.$store.getters.getSelectedStuntSheet; return currentSS.dotAppearances[this.dotTypeIndex]; }, diff --git a/src/components/grapher/Grapher.vue b/src/components/grapher/Grapher.vue index a12ba81..66b9ac4 100644 --- a/src/components/grapher/Grapher.vue +++ b/src/components/grapher/Grapher.vue @@ -10,6 +10,7 @@ + @@ -20,6 +21,7 @@ diff --git a/src/components/grapher/GrapherFlow.vue b/src/components/grapher/GrapherFlow.vue new file mode 100644 index 0000000..45a831b --- /dev/null +++ b/src/components/grapher/GrapherFlow.vue @@ -0,0 +1,47 @@ + + + + + diff --git a/src/components/menu-bottom/MenuBottom.vue b/src/components/menu-bottom/MenuBottom.vue index 84c874a..d2abe89 100644 --- a/src/components/menu-bottom/MenuBottom.vue +++ b/src/components/menu-bottom/MenuBottom.vue @@ -27,12 +27,15 @@ import BaseTool, { ToolConstructor } from "@/tools/BaseTool"; import ToolBoxSelect from "@/tools/ToolBoxSelect"; import ToolLassoSelect from "@/tools/ToolLassoSelect"; import ToolSingleDot from "@/tools/ToolSingleDot"; +import ToolConnectDots from "@/tools/ToolConnectDots"; import { Mutations } from "@/store/mutations"; +import { VIEW_MODES } from "@/store/constants"; interface ToolData { label: string; - icon: string; + icon: string; // See https://materialdesignicons.com/ tool: ToolConstructor; + forceViewMode?: VIEW_MODES; "data-test": string; } @@ -59,8 +62,16 @@ export default Vue.extend({ label: "Add and Remove Single Dot", icon: "plus-minus", tool: ToolSingleDot, + forceViewMode: VIEW_MODES.STUNTSHEET, "data-test": "add-rm", }, + { + label: "Connect Dots between Stuntsheets", + icon: "transit-connection-horizontal", + tool: ToolConnectDots, + forceViewMode: VIEW_MODES.FLOW, + "data-test": "connect-dots", + }, ], toolSelectedIndex: 0, // Assume that 0 is the pan/zoom tool }), @@ -70,14 +81,16 @@ export default Vue.extend({ methods: { setTool(toolIndex: number): void { this.$data.toolSelectedIndex = toolIndex; - const ToolConstructor: ToolConstructor = this.$data.toolDataList[ - toolIndex - ].tool; + const toolItem = this.$data.toolDataList[toolIndex]; + const ToolConstructor: ToolConstructor = toolItem.tool; const tool: BaseTool = new ToolConstructor(); this.$store.commit(Mutations.SET_TOOL_SELECTED, tool); if (!tool.supportsSelection) { this.$store.commit(Mutations.CLEAR_SELECTED_DOTS); } + if (toolItem.forceViewMode) { + this.$store.commit(Mutations.SET_VIEW_MODE, toolItem.forceViewMode); + } }, }, }); diff --git a/src/components/menu-right/MenuRight.vue b/src/components/menu-right/MenuRight.vue index 3c7822b..d84c202 100644 --- a/src/components/menu-right/MenuRight.vue +++ b/src/components/menu-right/MenuRight.vue @@ -1,5 +1,7 @@ diff --git a/src/store/constants.ts b/src/store/constants.ts new file mode 100644 index 0000000..1f321b1 --- /dev/null +++ b/src/store/constants.ts @@ -0,0 +1,11 @@ +/** + * Defines the different ways that the data can be viewed and interacted with in the UI + */ +export enum VIEW_MODES { + // "Stuntsheet" mode is for viewing and updating a specific stuntsheet. + // Used for drawing and editing formations. + STUNTSHEET = "Stuntsheet", + // "Flow" mode is for viewing and updating the transition between two stuntsheets. + // Used for creating and editing continuities. + FLOW = "Flow", +} diff --git a/src/store/index.ts b/src/store/index.ts index 4235f22..c2479ae 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -1,5 +1,6 @@ import Vue from "vue"; import Vuex, { Store } from "vuex"; +import { VIEW_MODES } from "./constants"; import { mutations } from "./mutations"; import getters from "./getters"; import Show from "@/models/Show"; @@ -17,6 +18,7 @@ Vue.use(Vuex); * @property initialShowState - Beginning spot for show * @property selectedSS - Index of stuntsheet currently in view * @property beat - The point in time the show is in + * @property viewMode - Defines the possible UI interactions * @property fourStepGrid - View setting to toggle the grapher grid * @property grapherSvgPanZoom - Initialized upon mounting Grapher * @property invertedCTMMatrix - Used to calculate clientX/Y to SVG X/Y @@ -32,6 +34,8 @@ export class CalChartState extends Serializable { beat = 0; + viewMode: VIEW_MODES = VIEW_MODES.STUNTSHEET; + fourStepGrid = true; yardlines = true; diff --git a/src/store/mutations.ts b/src/store/mutations.ts index 97e61dc..48ab428 100644 --- a/src/store/mutations.ts +++ b/src/store/mutations.ts @@ -14,6 +14,7 @@ import DotAppearance from "@/models/DotAppearance"; import { MARCH_TYPES } from "@/models/util/constants"; import ContETFStatic from "@/models/continuity/ContETFStatic"; import ContGateTurn from "@/models/continuity/ContGateTurn"; +import { VIEW_MODES } from "./constants"; export enum Mutations { // Show mutations: @@ -34,6 +35,8 @@ export enum Mutations { SET_STUNT_SHEET_BEATS = "Set Stund Sheet beats", ADD_DOT_TYPE = "Add Marcher type", ADD_CONTINUITY = "Add Continuity", + // StuntSheetDot mutations: + SET_DOT_NEXT_DOT_ID = "setDotNextDotId", // Continuity mutations: UPDATE_DOT_TYPE_MARCH_STYLE = "Update Marcher Step Style", UPDATE_DOT_TYPE_DURATION = "Update Marcher Duration", @@ -44,6 +47,7 @@ export enum Mutations { UPDATE_DOT_TYPE_ANGLE = "Update Marcher Angle", // View mutations: + SET_VIEW_MODE = "setViewMode", SET_SELECTED_SS = "setSelectedSS", SET_BEAT = "setBeat", INCREMENT_BEAT = "incrementBeat", @@ -219,6 +223,22 @@ export const mutations: MutationTree = { currentSS.calculateIssuesDeep(state.selectedSS); }, + // Show -> StuntSheet -> StuntSheetDot + [Mutations.SET_DOT_NEXT_DOT_ID]( + state, + { dotId, nextDotId }: { dotId: number; nextDotId: number | null } + ) { + const getSelectedStuntSheet = getters.getSelectedStuntSheet as ( + state: CalChartState + ) => StuntSheet; + const currentSS = getSelectedStuntSheet(state); + const currentDot = currentSS.stuntSheetDots.find((dot) => dot.id === dotId); + if (currentDot) { + currentDot.nextDotId = nextDotId; + state.show.generateFlows(state.selectedSS); + } + }, + // Show -> StuntSheet -> BaseCont [Mutations.UPDATE_DOT_TYPE_MARCH_STYLE]( state, @@ -395,6 +415,9 @@ export const mutations: MutationTree = { }, // View Settings + [Mutations.SET_VIEW_MODE](state, viewMode: VIEW_MODES): void { + state.viewMode = viewMode; + }, [Mutations.SET_FOUR_STEP_GRID](state, enabled: boolean): void { state.fourStepGrid = enabled; }, diff --git a/src/tools/BaseTool.ts b/src/tools/BaseTool.ts index 0f38340..1323444 100644 --- a/src/tools/BaseTool.ts +++ b/src/tools/BaseTool.ts @@ -64,6 +64,23 @@ export default abstract class BaseTool { }); } + /** + * returns dot from the next stunt sheet at mouse event, or undefined if nothing found + **/ + static findNextSSDotAtEvent(event: MouseEvent): StuntSheetDot | undefined { + const [x, y] = BaseTool.convertClientCoordinatesRounded(event); + const { show, selectedSS } = GlobalStore.state; + const nextSelectedSS = selectedSS + 1; + if (nextSelectedSS >= show.stuntSheets.length) { + return; + } + const stuntSheetDots: StuntSheetDot[] = + show.stuntSheets[nextSelectedSS].stuntSheetDots; + return stuntSheetDots.find((dot: StuntSheetDot): boolean => { + return x === dot.xAtBeat(0) && y === dot.yAtBeat(0); + }); + } + /** * Convert clientX/Y to the X/Y coordinates on the SVG rectangle. **/ diff --git a/src/tools/ToolConnectDots.ts b/src/tools/ToolConnectDots.ts new file mode 100644 index 0000000..6cb4836 --- /dev/null +++ b/src/tools/ToolConnectDots.ts @@ -0,0 +1,68 @@ +import { ToastProgrammatic as Toast } from "buefy"; +import { BNoticeComponent } from "buefy/types/components"; +import BaseMoveTool from "./BaseMoveTool"; +import BaseTool, { ToolConstructor } from "./BaseTool"; +import { GlobalStore } from "@/store"; +import { Mutations } from "@/store/mutations"; + +/** + * Connect dots between stuntsheets. This tool is performed in 2 steps: + * 1. Select a dot in the current stuntsheet + * 2. Select a dot in the next stuntsheet to connect to + */ +const ToolConnectDots: ToolConstructor = class ToolConnectDots extends BaseMoveTool { + private toast: BNoticeComponent | undefined; + + constructor() { + super(); + GlobalStore.commit(Mutations.CLEAR_SELECTED_DOTS); + this.openToast( + "Connect Dots tool activated. Select a dot from the current stuntsheet." + ); + } + + onMouseUpInternal(event: MouseEvent): void { + const { selectedDotIds, show, selectedSS } = GlobalStore.state; + if (selectedDotIds.length === 1) { + const nextSSDot = BaseTool.findNextSSDotAtEvent(event); + if (nextSSDot) { + const stuntSheetDots = show.stuntSheets[selectedSS].stuntSheetDots; + const prevConnectedDot = stuntSheetDots.find( + (dot) => dot.nextDotId === nextSSDot.id + ); + if (prevConnectedDot) { + GlobalStore.commit(Mutations.SET_DOT_NEXT_DOT_ID, { + dotId: prevConnectedDot.id, + nextDotId: null, + }); + } + GlobalStore.commit(Mutations.SET_DOT_NEXT_DOT_ID, { + dotId: selectedDotIds[0], + nextDotId: nextSSDot.id, + }); + GlobalStore.commit(Mutations.CLEAR_SELECTED_DOTS); + this.openToast("Dots successfully connectted!", 2000); + } + } else { + const selectedDot = BaseTool.findDotAtEvent(event); + if (selectedDot) { + GlobalStore.commit(Mutations.ADD_SELECTED_DOTS, [selectedDot.id]); + this.openToast( + "Dot selected. Now, select a dot from the next stuntsheet (pink indicates dots that have not been connected yet)." + ); + } + } + } + + private openToast(message: string, duration = 7000) { + this.toast && this.toast.close(); + this.toast = Toast.open({ + type: "is-info", + queue: false, + duration, + message, + }); + } +}; + +export default ToolConnectDots; diff --git a/tests/e2e/specs/menu-bottom/MenuBottom.spec.js b/tests/e2e/specs/menu-bottom/MenuBottom.spec.js index 77400c9..e2b3303 100644 --- a/tests/e2e/specs/menu-bottom/MenuBottom.spec.js +++ b/tests/e2e/specs/menu-bottom/MenuBottom.spec.js @@ -4,7 +4,7 @@ describe("components/menu-bottom/MenuBottom", () => { }); it("all buttons are rendered and box select is selected", () => { - cy.get('[data-test="menu-bottom--tooltip"]').should("have.length", 3); + cy.get('[data-test="menu-bottom--tooltip"]').should("have.length", 4); cy.get('[data-test="menu-bottom-tool--select-box-move"]').should( "have.class", @@ -13,7 +13,7 @@ describe("components/menu-bottom/MenuBottom", () => { cy.get('[data-test="menu-bottom--tooltip"] .is-light').should( "have.length", - 2 + 3 ); }); @@ -25,7 +25,7 @@ describe("components/menu-bottom/MenuBottom", () => { cy.get('[data-test="menu-bottom--tooltip"] .is-light').should( "have.length", - 2 + 3 ); // eslint-disable-next-line cypress/require-data-selectors diff --git a/tests/unit/components/menu-bottom/MenuBottom.spec.ts b/tests/unit/components/menu-bottom/MenuBottom.spec.ts index 1e2588f..e8218a8 100644 --- a/tests/unit/components/menu-bottom/MenuBottom.spec.ts +++ b/tests/unit/components/menu-bottom/MenuBottom.spec.ts @@ -39,7 +39,7 @@ describe("components/menu-bottom/MenuBottom", () => { it("renders the correct amount of tools", () => { expect(menu.findAll('[data-test="menu-bottom--tooltip"]')).toHaveLength( - 3 + 4 ); });