const centerStage = (state: ApplicationState, workspaceId: string, innerBounds: Bounds, smooth?: boolean, zoomOrZoomToFit?: boolean|number) => { const workspace = getWorkspaceById(state, workspaceId); const { stage: { container, translate }} = workspace; if (!container) return state; const { width, height } = container.getBoundingClientRect(); const innerSize = getBoundsSize(innerBounds); const centered = { left: -innerBounds.left + width / 2 - (innerSize.width) / 2, top: -innerBounds.top + height / 2 - (innerSize.height) / 2, }; const scale = typeof zoomOrZoomToFit === "boolean" ? Math.min( (width - INITIAL_ZOOM_PADDING) / innerSize.width, (height - INITIAL_ZOOM_PADDING) / innerSize.height ) : typeof zoomOrZoomToFit === "number" ? zoomOrZoomToFit : translate.zoom; return updateWorkspaceStage(state, workspaceId, { smooth, translate: centerTransformZoom({ ...centered, zoom: 1 }, { left: 0, top: 0, right: width, bottom: height }, scale) }); };
const setStageZoom = (state: ApplicationState, workspaceId: string, zoom: number, smooth: boolean = true) => { const workspace = getWorkspaceById(state, workspaceId); return updateWorkspaceStage(state, workspace.$id, { smooth, translate: centerTransformZoom( workspace.stage.translate, workspace.stage.container.getBoundingClientRect(), clamp(zoom, MIN_ZOOM, MAX_ZOOM), workspace.stage.mousePosition ) }); };
const centerSelectedWorkspace = (state: ApplicationState, smooth: boolean = false) => { const workspace = getWorkspaceById(state, state.selectedWorkspaceId); const innerBounds = getArtboardBounds(workspace); // no windows loaded if (innerBounds.left + innerBounds.right + innerBounds.top + innerBounds.bottom === 0) { console.warn(`Stage mounted before windows have been loaded`); return state; } return centerStage(state, workspace.$id, innerBounds, smooth, true); }
const unfullscreen = (state: ApplicationState, workspaceId: string = state.selectedWorkspaceId) => { const workspace = getWorkspaceById(state, workspaceId); const { originalArtboardBounds, artboardId } = workspace.stage.fullScreen; state = updateWorkspaceStage(state, workspace.$id, { smooth: true, fullScreen: undefined }); state = updateWorkspaceStage(state, workspace.$id, { translate: workspace.stage.fullScreen.originalTranslate, smooth: true }); state = updateArtboard(state, artboardId, { bounds: originalArtboardBounds }); return state; }
const stageReducer = (state: ApplicationState, event: BaseEvent) => { switch(event.type) { case VISUAL_EDITOR_WHEEL: { const { workspaceId, metaKey, ctrlKey, deltaX, deltaY, canvasHeight, canvasWidth } = event as StageWheel; const workspace = getWorkspaceById(state, workspaceId); if (workspace.stage.fullScreen) { return state; } let translate = getStageTranslate(workspace.stage); if (metaKey || ctrlKey) { translate = centerTransformZoom(translate, boundsFromRect({ width: canvasWidth, height: canvasHeight }), clamp(translate.zoom + translate.zoom * deltaY / ZOOM_SENSITIVITY, MIN_ZOOM, MAX_ZOOM), workspace.stage.mousePosition); } else { translate = { ...translate, left: translate.left - deltaX, top: translate.top - deltaY }; } return updateWorkspaceStage(state, workspace.$id, { smooth: false, translate }); } case TOGGLE_TOOLS_SHORTCUT_PRESSED: { const workspace = getSelectedWorkspace(state); return updateWorkspaceStage(state, workspace.$id, { showTools: workspace.stage.showTools == null ? false : !workspace.stage.showTools }) } case STAGE_TOOL_EDIT_TEXT_KEY_DOWN: { const { sourceEvent, nodeId } = event as StageToolEditTextKeyDown; if (sourceEvent.key === "Escape") { // const workspace = getSyntheticNodeWorkspace(state, nodeId); // state = setWorkspaceSelection(state, workspace.$id, getStructReference(getNestedObjectById(nodeId, getNodeArtboard(nodeId, state).document))); // state = updateWorkspaceStage(state, workspace.$id, { // secondarySelection: false // }); } return state; } case RESIZER_MOVED: { const { point, workspaceId, point: newPoint } = event as ResizerMoved; const workspace = getSelectedWorkspace(state); state = updateWorkspaceStage(state, workspace.$id, { movingOrResizing: true }); const translate = getStageTranslate(workspace.stage); const selectionBounds = getWorkspaceSelectionBounds(workspace); for (const item of getBoundedWorkspaceSelection(workspace)) { const itemBounds = getWorkspaceItemBounds(item, workspace); // skip moving window if in full screen mode if (workspace.stage.fullScreen && workspace.stage.fullScreen.artboardId === item.$id) { break; } const newBounds = roundBounds(scaleInnerBounds(itemBounds, selectionBounds, moveBounds(selectionBounds, newPoint))); if (item.$type === ARTBOARD) { state = updateArtboard(state, item.$id, { bounds: newBounds }); } } return state; } case RESIZER_PATH_MOUSE_MOVED: { let { workspaceId, anchor, originalBounds, newBounds, sourceEvent } = event as ResizerPathMoved; const workspace = getSelectedWorkspace(state); state = updateWorkspaceStage(state, workspace.$id, { movingOrResizing: true }); // TODO - possibly use BoundsStruct instead of Bounds since there are cases where bounds prop doesn't exist const currentBounds = getWorkspaceSelectionBounds(workspace); const keepAspectRatio = sourceEvent.shiftKey; const keepCenter = sourceEvent.altKey; if (keepCenter) { // newBounds = keepBoundsCenter(newBounds, bounds, anchor); } if (keepAspectRatio) { newBounds = keepBoundsAspectRatio(newBounds, originalBounds, anchor, keepCenter ? { left: 0.5, top: 0.5 } : anchor); } for (const item of getBoundedWorkspaceSelection(workspace)) { const innerBounds = getWorkspaceItemBounds(item, workspace); const scaledBounds = scaleInnerBounds(currentBounds, currentBounds, newBounds); if (item.$type === ARTBOARD) { state = updateArtboard(state, item.$id, { bounds: scaleInnerBounds(innerBounds, currentBounds, newBounds) }) } } return state; } case RESIZER_PATH_MOUSE_STOPPED_MOVING: case RESIZER_STOPPED_MOVING: { const workspace = getSelectedWorkspace(state); state = updateWorkspaceStage(state, workspace.$id, { movingOrResizing: false }); return state; } case ARTBOARD_FOCUSED: { const { artboardId } = event as ArtboardFocused; return selectAndCenterArtboard(state, getArtboardById(artboardId, state)); } case STAGE_TOOL_OVERLAY_MOUSE_LEAVE: { const { sourceEvent } = event as StageToolOverlayMouseMoved; return updateWorkspace(state, state.selectedWorkspaceId, { hoveringRefs: [] }); } case CSS_DECLARATION_TITLE_MOUSE_ENTER: { const { artboardId, ruleId } = event as CSSDeclarationTitleMouseLeaveEnter; const artboard = getArtboardById(artboardId, state); // TODO return state; // const { selectorText }: SEnvCSSStyleRuleInterface = getNestedObjectById(ruleId, artboard.document); // return updateWorkspace(state, state.selectedWorkspaceId, { // hoveringRefs: getMatchingElements(artboard, selectorText).map((element) => [ // element.$type, // element.$id // ]) as [[string, string]] // }); } case CSS_DECLARATION_TITLE_MOUSE_LEAVE: { const { artboardId, ruleId } = event as CSSDeclarationTitleMouseLeaveEnter; return updateWorkspace(state, state.selectedWorkspaceId, { hoveringRefs: [] }); } case BREADCRUMB_ITEM_CLICKED: { const { artboardId, nodeId } = event as BreadcrumbItemClicked; const artboard = getArtboardById(artboardId, state); const node = getNestedObjectById(nodeId, artboard.document) as SlimBaseNode; const workspace = getArtboardWorkspace(artboard.$id, state); return setWorkspaceSelection(state, workspace.$id, [node.type, node.id]); } case BREADCRUMB_ITEM_MOUSE_ENTER: { const { artboardId, nodeId } = event as BreadcrumbItemMouseEnterLeave; return updateWorkspace(state, state.selectedWorkspaceId, { hoveringRefs: [[SlimVMObjectType.ELEMENT, nodeId]] }); } case BREADCRUMB_ITEM_MOUSE_LEAVE: { const { artboardId, nodeId } = event as BreadcrumbItemMouseEnterLeave; return updateWorkspace(state, state.selectedWorkspaceId, { hoveringRefs: [] }); } case STAGE_MOUNTED: { const { element } = event as StageMounted; const { width = 400, height = 300 } = element.getBoundingClientRect() || {}; const workspaceId = state.selectedWorkspaceId; const workspace = getSelectedWorkspace(state); state = updateWorkspaceStage(state, workspaceId, { container: element }); // do not center if in full screen mode if (workspace.stage.fullScreen) { return updateArtboardSize(state, workspace.stage.fullScreen.artboardId, width, height); } return centerSelectedWorkspace(state); }; case STAGE_TOOL_OVERLAY_MOUSE_PAN_START: { const { artboardId } = event as StageToolOverlayMousePanStart; const workspace = getArtboardWorkspace(artboardId, state); return updateWorkspaceStage(state, workspace.$id, { panning: true }); } case STAGE_TOOL_OVERLAY_MOUSE_PAN_END: { const { artboardId } = event as StageToolOverlayMousePanEnd; const workspace = getArtboardWorkspace(artboardId, state) return updateWorkspaceStage(state, workspace.$id, { panning: false }); } case STAGE_MOUSE_MOVED: { const { sourceEvent: { pageX, pageY }} = event as WrappedEvent<React.MouseEvent<any>>; state = updateWorkspaceStage(state, state.selectedWorkspaceId, { mousePosition: { left: pageX, top: pageY } }); const workspace = getSelectedWorkspace(state); // TODO - in the future, we'll probably want to be able to highlight hovered nodes as the user is moving an element around to indicate where // they can drop the element. const targetRef = workspace.stage.movingOrResizing ? null : getStageToolMouseNodeTargetReference(state, event as StageToolOverlayMouseMoved); state = updateWorkspace(state, state.selectedWorkspaceId, { hoveringRefs: targetRef ? [targetRef] : [] }); return state; }; case STAGE_MOUSE_CLICKED: { const { sourceEvent } = event as StageToolNodeOverlayClicked; if (/textarea|input/i.test((sourceEvent.target as Element).nodeName)) { return state; } // alt key opens up a new link const altKey = sourceEvent.altKey; const workspace = getSelectedWorkspace(state); state = updateWorkspaceStageSmoothing(state, workspace); // do not allow selection while window is panning (scrolling) if (workspace.stage.panning || workspace.stage.movingOrResizing) return state; const targetRef = getStageToolMouseNodeTargetReference(state, event as StageToolNodeOverlayClicked); if (!targetRef) { return state; } if (!altKey) { state = handleArtboardSelectionFromAction(state, targetRef, event as StageToolNodeOverlayClicked); state = updateWorkspaceStage(state, workspace.$id, { secondarySelection: false }); return state; } return state; } case STAGE_TOOL_OVERLAY_MOUSE_DOUBLE_CLICKED: { const { sourceEvent, artboardId } = event as StageToolNodeOverlayClicked; const workspace = getArtboardWorkspace(artboardId, state); const targetRef = getStageToolMouseNodeTargetReference(state, event as StageToolNodeOverlayClicked); if (!targetRef) return state; state = updateWorkspaceStage(state, workspace.$id, { secondarySelection: true }); state = setWorkspaceSelection(state, workspace.$id, targetRef); return state; } case ARTBOARD_SELECTION_SHIFTED: { const { artboardId } = event as ArtboardSelectionShifted; return selectAndCenterArtboard(state, getArtboardById(artboardId, state)); } case SELECTOR_DOUBLE_CLICKED: { const { sourceEvent, item } = event as SelectorDoubleClicked; const workspace = getSyntheticNodeWorkspace(state, item.$id); state = updateWorkspaceStage(state, workspace.$id, { secondarySelection: true }); state = setWorkspaceSelection(state, workspace.$id, getStructReference(item)); return state; } case ARTBOARD_SCROLL: { const { artboardId, scrollPosition } = event as ArtboardScroll; return updateArtboard(state, artboardId, { scrollPosition }); } case WORKSPACE_DELETION_SELECTED: { const { workspaceId } = event as WorkspaceSelectionDeleted; state = clearWorkspaceSelection(state, workspaceId); return state; } case STAGE_TOOL_ARTBOARD_TITLE_CLICKED: { state = updateWorkspaceStageSmoothing(state); return handleArtboardSelectionFromAction(state, getStructReference(getArtboardById((event as ArtboardPaneRowClicked).artboardId, state)), event as ArtboardPaneRowClicked); } case STAGE_TOOL_WINDOW_BACKGROUND_CLICKED: { const workspace = getSelectedWorkspace(state); return clearWorkspaceSelection(state, workspace.$id); } } return state; }