mirror of
https://github.com/stan-smith/FossFLOW.git
synced 2025-12-25 15:39:35 -05:00
fix: fixes issue where exported image isn't positioned correctly
This commit is contained in:
@@ -13,7 +13,7 @@ import {
|
||||
Colors,
|
||||
Icons
|
||||
} from 'src/types';
|
||||
import { setWindowCursor } from 'src/utils';
|
||||
import { setWindowCursor, modelFromModelStore } from 'src/utils';
|
||||
import { modelSchema } from 'src/schemas/model';
|
||||
import { useModelStore, ModelProvider } from 'src/stores/modelStore';
|
||||
import { SceneProvider } from 'src/stores/sceneStore';
|
||||
@@ -21,9 +21,8 @@ import { GlobalStyles } from 'src/styles/GlobalStyles';
|
||||
import { Renderer } from 'src/components/Renderer/Renderer';
|
||||
import { UiOverlay } from 'src/components/UiOverlay/UiOverlay';
|
||||
import { UiStateProvider, useUiStateStore } from 'src/stores/uiStateStore';
|
||||
import { INITIAL_DATA, MAIN_MENU_OPTIONS, INITIAL_UI_STATE } from 'src/config';
|
||||
import { useModel } from 'src/hooks/useModel';
|
||||
import { useIconCategories } from './hooks/useIconCategories';
|
||||
import { INITIAL_DATA, MAIN_MENU_OPTIONS } from 'src/config';
|
||||
import { useInitialDataManager } from 'src/hooks/useInitialDataManager';
|
||||
|
||||
const App = ({
|
||||
initialData,
|
||||
@@ -34,37 +33,24 @@ const App = ({
|
||||
enableDebugTools = false,
|
||||
editorMode = 'EDITABLE'
|
||||
}: IsoflowProps) => {
|
||||
const modelActions = useModelStore((state) => {
|
||||
const uiStateActions = useUiStateStore((state) => {
|
||||
return state.actions;
|
||||
});
|
||||
const uiActions = useUiStateStore((state) => {
|
||||
return state.actions;
|
||||
const initialDataManager = useInitialDataManager();
|
||||
const model = useModelStore((state) => {
|
||||
return modelFromModelStore(state);
|
||||
});
|
||||
const { setIconCategoriesState } = useIconCategories();
|
||||
const model = useModel();
|
||||
|
||||
const { load } = initialDataManager;
|
||||
|
||||
useEffect(() => {
|
||||
if (initialData?.zoom) {
|
||||
uiActions.setZoom(initialData.zoom);
|
||||
}
|
||||
load({ ...INITIAL_DATA, ...initialData });
|
||||
}, [initialData, load]);
|
||||
|
||||
if (initialData?.scroll) {
|
||||
uiActions.setScroll({
|
||||
position: initialData.scroll,
|
||||
offset: INITIAL_UI_STATE.scroll.offset
|
||||
});
|
||||
}
|
||||
|
||||
uiActions.setEditorMode(editorMode);
|
||||
uiActions.setMainMenuOptions(mainMenuOptions);
|
||||
}, [
|
||||
initialData?.zoom,
|
||||
initialData?.scroll,
|
||||
editorMode,
|
||||
modelActions,
|
||||
uiActions,
|
||||
mainMenuOptions
|
||||
]);
|
||||
useEffect(() => {
|
||||
uiStateActions.setEditorMode(editorMode);
|
||||
uiStateActions.setMainMenuOptions(mainMenuOptions);
|
||||
}, [editorMode, uiStateActions, mainMenuOptions]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
@@ -72,27 +58,17 @@ const App = ({
|
||||
};
|
||||
}, []);
|
||||
|
||||
const { load } = model;
|
||||
|
||||
useEffect(() => {
|
||||
load({ ...INITIAL_DATA, ...initialData });
|
||||
}, [initialData, load]);
|
||||
|
||||
useEffect(() => {
|
||||
setIconCategoriesState();
|
||||
}, [model.icons, setIconCategoriesState]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!model.isReady || !onModelUpdated) return;
|
||||
if (!initialDataManager.isReady || !onModelUpdated) return;
|
||||
|
||||
onModelUpdated(model);
|
||||
}, [model, onModelUpdated, model.isReady]);
|
||||
}, [model, initialDataManager.isReady, onModelUpdated]);
|
||||
|
||||
useEffect(() => {
|
||||
uiActions.setenableDebugTools(enableDebugTools);
|
||||
}, [enableDebugTools, uiActions]);
|
||||
uiStateActions.setEnableDebugTools(enableDebugTools);
|
||||
}, [enableDebugTools, uiStateActions]);
|
||||
|
||||
if (!model.isReady) return null;
|
||||
if (!initialDataManager.isReady) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -12,7 +12,6 @@ import { useModelStore } from 'src/stores/modelStore';
|
||||
import {
|
||||
exportAsImage,
|
||||
downloadFile as downloadFileUtil,
|
||||
getTileScrollPosition,
|
||||
base64ToBlob,
|
||||
generateGenericFilename,
|
||||
modelFromModelStore
|
||||
@@ -32,7 +31,7 @@ export const ExportImageDialog = ({ onClose, quality = 1.5 }: Props) => {
|
||||
const containerRef = useRef<HTMLDivElement>();
|
||||
const debounceRef = useRef<NodeJS.Timeout>();
|
||||
const [imageData, setImageData] = React.useState<string>();
|
||||
const { getUnprojectedBounds, getFitToViewParams } = useDiagramUtils();
|
||||
const { getUnprojectedBounds } = useDiagramUtils();
|
||||
const uiStateActions = useUiStateStore((state) => {
|
||||
return state.actions;
|
||||
});
|
||||
@@ -43,9 +42,6 @@ export const ExportImageDialog = ({ onClose, quality = 1.5 }: Props) => {
|
||||
const unprojectedBounds = useMemo(() => {
|
||||
return getUnprojectedBounds();
|
||||
}, [getUnprojectedBounds]);
|
||||
const previewParams = useMemo(() => {
|
||||
return getFitToViewParams(unprojectedBounds);
|
||||
}, [unprojectedBounds, getFitToViewParams]);
|
||||
|
||||
useEffect(() => {
|
||||
uiStateActions.setMode({
|
||||
@@ -113,11 +109,7 @@ export const ExportImageDialog = ({ onClose, quality = 1.5 }: Props) => {
|
||||
<Isoflow
|
||||
editorMode="NON_INTERACTIVE"
|
||||
onModelUpdated={exportImage}
|
||||
initialData={{
|
||||
...model,
|
||||
zoom: previewParams.zoom * quality,
|
||||
scroll: getTileScrollPosition(previewParams.scrollTarget)
|
||||
}}
|
||||
initialData={{ ...model, fitToView: true }}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -13,11 +13,15 @@ import { UiElement } from 'src/components/UiElement/UiElement';
|
||||
import { IconButton } from 'src/components/IconButton/IconButton';
|
||||
import { useUiStateStore } from 'src/stores/uiStateStore';
|
||||
import { exportAsJSON, modelFromModelStore } from 'src/utils';
|
||||
import { useModel } from 'src/hooks/useModel';
|
||||
import { useInitialDataManager } from 'src/hooks/useInitialDataManager';
|
||||
import { useModelStore } from 'src/stores/modelStore';
|
||||
import { MenuItem } from './MenuItem';
|
||||
|
||||
export const MainMenu = () => {
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const model = useModelStore((state) => {
|
||||
return modelFromModelStore(state);
|
||||
});
|
||||
const isMainMenuOpen = useUiStateStore((state) => {
|
||||
return state.isMainMenuOpen;
|
||||
});
|
||||
@@ -27,7 +31,7 @@ export const MainMenu = () => {
|
||||
const uiStateActions = useUiStateStore((state) => {
|
||||
return state.actions;
|
||||
});
|
||||
const model = useModel();
|
||||
const initialDataManager = useInitialDataManager();
|
||||
|
||||
const onToggleMenu = useCallback(
|
||||
(event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
@@ -41,7 +45,7 @@ export const MainMenu = () => {
|
||||
window.open(url, '_blank');
|
||||
}, []);
|
||||
|
||||
const { load } = model;
|
||||
const { load } = initialDataManager;
|
||||
|
||||
const onOpenModel = useCallback(async () => {
|
||||
const fileInput = document.createElement('input');
|
||||
@@ -71,7 +75,7 @@ export const MainMenu = () => {
|
||||
}, [uiStateActions, load]);
|
||||
|
||||
const onExportAsJSON = useCallback(async () => {
|
||||
exportAsJSON(modelFromModelStore(model));
|
||||
exportAsJSON(model);
|
||||
uiStateActions.setIsMainMenuOpen(false);
|
||||
}, [model, uiStateActions]);
|
||||
|
||||
@@ -80,7 +84,7 @@ export const MainMenu = () => {
|
||||
uiStateActions.setDialog('EXPORT_IMAGE');
|
||||
}, [uiStateActions]);
|
||||
|
||||
const { clear } = model;
|
||||
const { clear } = initialDataManager;
|
||||
|
||||
const onClearCanvas = useCallback(() => {
|
||||
clear();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
Size,
|
||||
Model,
|
||||
InitialData,
|
||||
MainMenuOptions,
|
||||
Icon,
|
||||
Connector,
|
||||
@@ -10,8 +10,8 @@ import {
|
||||
Rectangle,
|
||||
Colors
|
||||
} from 'src/types';
|
||||
import { CoordsUtils } from 'src/utils';
|
||||
import { customVars } from './styles/theme';
|
||||
import { CoordsUtils } from './utils';
|
||||
|
||||
// TODO: This file could do with better organisation and convention for easier reading.
|
||||
export const UNPROJECTED_TILE_SIZE = 100;
|
||||
@@ -72,13 +72,14 @@ export const MIN_ZOOM = 0.2;
|
||||
export const MAX_ZOOM = 1;
|
||||
export const TRANSFORM_ANCHOR_SIZE = 30;
|
||||
export const TRANSFORM_CONTROLS_COLOR = '#0392ff';
|
||||
export const INITIAL_DATA: Model = {
|
||||
export const INITIAL_DATA: InitialData = {
|
||||
title: 'Untitled',
|
||||
version: '',
|
||||
icons: [],
|
||||
colors: [DEFAULT_COLOR],
|
||||
items: [],
|
||||
views: []
|
||||
views: [],
|
||||
fitToView: false
|
||||
};
|
||||
export const INITIAL_UI_STATE = {
|
||||
zoom: 1,
|
||||
@@ -105,5 +106,4 @@ export const DEFAULT_ICON: Icon = {
|
||||
};
|
||||
|
||||
export const DEFAULT_LABEL_HEIGHT = 20;
|
||||
|
||||
export const EDITOR_CONFIG = {};
|
||||
export const PROJECT_BOUNDING_BOX_PADDING = 3;
|
||||
|
||||
@@ -3,5 +3,5 @@ import Isoflow from 'src/Isoflow';
|
||||
import { initialData } from '../initialData';
|
||||
|
||||
export const BasicEditor = () => {
|
||||
return <Isoflow initialData={initialData} />;
|
||||
return <Isoflow initialData={{ ...initialData, fitToView: true }} />;
|
||||
};
|
||||
|
||||
@@ -2,19 +2,13 @@ import { useCallback } from 'react';
|
||||
import { useUiStateStore } from 'src/stores/uiStateStore';
|
||||
import { Size, Coords } from 'src/types';
|
||||
import {
|
||||
getBoundingBox,
|
||||
getBoundingBoxSize,
|
||||
sortByPosition,
|
||||
clamp,
|
||||
getTilePosition,
|
||||
getUnprojectedBounds as getUnprojectedBoundsUtil,
|
||||
getFitToViewParams as getFitToViewParamsUtil,
|
||||
CoordsUtils
|
||||
} from 'src/utils';
|
||||
import { MAX_ZOOM } from 'src/config';
|
||||
import { useScene } from 'src/hooks/useScene';
|
||||
import { useResizeObserver } from './useResizeObserver';
|
||||
|
||||
const BOUNDING_BOX_PADDING = 3;
|
||||
|
||||
export const useDiagramUtils = () => {
|
||||
const scene = useScene();
|
||||
const rendererEl = useUiStateStore((state) => {
|
||||
@@ -25,111 +19,24 @@ export const useDiagramUtils = () => {
|
||||
return state.actions;
|
||||
});
|
||||
|
||||
const getProjectBounds = useCallback((): Coords[] => {
|
||||
const itemTiles = scene.items.map((item) => {
|
||||
return item.tile;
|
||||
});
|
||||
|
||||
const connectorTiles = scene.connectors.reduce<Coords[]>(
|
||||
(acc, connector) => {
|
||||
return [
|
||||
...acc,
|
||||
connector.path.rectangle.from,
|
||||
connector.path.rectangle.to
|
||||
];
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const rectangleTiles = scene.rectangles.reduce<Coords[]>(
|
||||
(acc, rectangle) => {
|
||||
return [...acc, rectangle.from, rectangle.to];
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const textBoxTiles = scene.textBoxes.reduce<Coords[]>((acc, textBox) => {
|
||||
return [
|
||||
...acc,
|
||||
textBox.tile,
|
||||
CoordsUtils.add(textBox.tile, {
|
||||
x: textBox.size.width,
|
||||
y: textBox.size.height
|
||||
})
|
||||
];
|
||||
}, []);
|
||||
|
||||
let allTiles = [
|
||||
...itemTiles,
|
||||
...connectorTiles,
|
||||
...rectangleTiles,
|
||||
...textBoxTiles
|
||||
];
|
||||
|
||||
if (allTiles.length === 0) {
|
||||
const centerTile = CoordsUtils.zero();
|
||||
allTiles = [centerTile, centerTile, centerTile, centerTile];
|
||||
}
|
||||
|
||||
const corners = getBoundingBox(allTiles, {
|
||||
x: BOUNDING_BOX_PADDING,
|
||||
y: BOUNDING_BOX_PADDING
|
||||
});
|
||||
|
||||
return corners;
|
||||
}, [scene]);
|
||||
|
||||
const getUnprojectedBounds = useCallback((): Size & Coords => {
|
||||
const projectBounds = getProjectBounds();
|
||||
|
||||
const cornerPositions = projectBounds.map((corner) => {
|
||||
return getTilePosition({
|
||||
tile: corner
|
||||
});
|
||||
});
|
||||
const sortedCorners = sortByPosition(cornerPositions);
|
||||
const topLeft = { x: sortedCorners.lowX, y: sortedCorners.lowY };
|
||||
const size = getBoundingBoxSize(cornerPositions);
|
||||
|
||||
return {
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
x: topLeft.x,
|
||||
y: topLeft.y
|
||||
};
|
||||
}, [getProjectBounds]);
|
||||
return getUnprojectedBoundsUtil(scene.currentView);
|
||||
}, [scene.currentView]);
|
||||
|
||||
const getFitToViewParams = useCallback(
|
||||
(viewportSize: Size) => {
|
||||
const projectBounds = getProjectBounds();
|
||||
const sortedCornerPositions = sortByPosition(projectBounds);
|
||||
const boundingBoxSize = getBoundingBoxSize(projectBounds);
|
||||
const unprojectedBounds = getUnprojectedBounds();
|
||||
const newZoom = clamp(
|
||||
Math.min(
|
||||
viewportSize.width / unprojectedBounds.width,
|
||||
viewportSize.height / unprojectedBounds.height
|
||||
),
|
||||
0,
|
||||
MAX_ZOOM
|
||||
);
|
||||
const scrollTarget: Coords = {
|
||||
x: (sortedCornerPositions.lowX + boundingBoxSize.width / 2) * newZoom,
|
||||
y: (sortedCornerPositions.lowY + boundingBoxSize.height / 2) * newZoom
|
||||
};
|
||||
|
||||
return {
|
||||
zoom: newZoom,
|
||||
scrollTarget
|
||||
};
|
||||
return getFitToViewParamsUtil(scene.currentView, viewportSize);
|
||||
},
|
||||
[getProjectBounds, getUnprojectedBounds]
|
||||
[scene.currentView]
|
||||
);
|
||||
|
||||
const fitToView = useCallback(async () => {
|
||||
const { zoom, scrollTarget } = getFitToViewParams(rendererSize);
|
||||
const { zoom, scroll } = getFitToViewParams(rendererSize);
|
||||
|
||||
uiStateActions.scrollToTile(scrollTarget);
|
||||
uiStateActions.setScroll({
|
||||
position: scroll,
|
||||
offset: CoordsUtils.zero()
|
||||
});
|
||||
uiStateActions.setZoom(zoom);
|
||||
}, [uiStateActions, getFitToViewParams, rendererSize]);
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useMemo, useCallback } from 'react';
|
||||
import { IconCollectionStateWithIcons, IconCollectionState } from 'src/types';
|
||||
import { useMemo } from 'react';
|
||||
import { IconCollectionStateWithIcons } from 'src/types';
|
||||
import { useUiStateStore } from 'src/stores/uiStateStore';
|
||||
import { useModelStore } from 'src/stores/modelStore';
|
||||
import { categoriseIcons } from 'src/utils';
|
||||
|
||||
export const useIconCategories = () => {
|
||||
const icons = useModelStore((state) => {
|
||||
@@ -11,22 +10,6 @@ export const useIconCategories = () => {
|
||||
const iconCategoriesState = useUiStateStore((state) => {
|
||||
return state.iconCategoriesState;
|
||||
});
|
||||
const uiActions = useUiStateStore((state) => {
|
||||
return state.actions;
|
||||
});
|
||||
|
||||
const setIconCategoriesState = useCallback(() => {
|
||||
const categoriesState: IconCollectionState[] = categoriseIcons(icons).map(
|
||||
(collection) => {
|
||||
return {
|
||||
id: collection.name,
|
||||
isExpanded: false
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
uiActions.setIconCategoriesState(categoriesState);
|
||||
}, [icons, uiActions]);
|
||||
|
||||
const iconCategories = useMemo<IconCollectionStateWithIcons[]>(() => {
|
||||
return iconCategoriesState.map((collection) => {
|
||||
@@ -40,7 +23,6 @@ export const useIconCategories = () => {
|
||||
}, [icons, iconCategoriesState]);
|
||||
|
||||
return {
|
||||
iconCategories,
|
||||
setIconCategoriesState
|
||||
iconCategories
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import { useCallback, useState, useRef } from 'react';
|
||||
import { InitialData } from 'src/types';
|
||||
import { InitialData, IconCollectionState } from 'src/types';
|
||||
import { INITIAL_DATA, VIEW_DEFAULTS } from 'src/config';
|
||||
import { generateId } from 'src/utils';
|
||||
import {
|
||||
generateId,
|
||||
getFitToViewParams,
|
||||
CoordsUtils,
|
||||
categoriseIcons
|
||||
} from 'src/utils';
|
||||
import { createView } from 'src/stores/reducers';
|
||||
import { useModelStore } from 'src/stores/modelStore';
|
||||
import { useView } from 'src/hooks/useView';
|
||||
import { useUiStateStore } from 'src/stores/uiStateStore';
|
||||
import { modelSchema } from 'src/schemas/model';
|
||||
import { useResizeObserver } from './useResizeObserver';
|
||||
|
||||
export const useModel = () => {
|
||||
export const useInitialDataManager = () => {
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
const prevInitialData = useRef<InitialData>();
|
||||
const model = useModelStore((state) => {
|
||||
@@ -17,7 +23,11 @@ export const useModel = () => {
|
||||
const uiStateActions = useUiStateStore((state) => {
|
||||
return state.actions;
|
||||
});
|
||||
const rendererEl = useUiStateStore((state) => {
|
||||
return state.rendererEl;
|
||||
});
|
||||
const { changeView } = useView();
|
||||
const { size } = useResizeObserver(rendererEl);
|
||||
|
||||
const load = useCallback(
|
||||
(_initialData: InitialData) => {
|
||||
@@ -45,13 +55,35 @@ export const useModel = () => {
|
||||
|
||||
Object.assign(initialData, updates);
|
||||
}
|
||||
|
||||
prevInitialData.current = initialData;
|
||||
model.actions.set(initialData);
|
||||
|
||||
changeView(initialData.views[0].id, initialData);
|
||||
|
||||
if (initialData.fitToView) {
|
||||
const { zoom, scroll } = getFitToViewParams(initialData.views[0], size);
|
||||
|
||||
uiStateActions.setScroll({
|
||||
position: scroll,
|
||||
offset: CoordsUtils.zero()
|
||||
});
|
||||
uiStateActions.setZoom(zoom);
|
||||
}
|
||||
|
||||
const categoriesState: IconCollectionState[] = categoriseIcons(
|
||||
initialData.icons
|
||||
).map((collection) => {
|
||||
return {
|
||||
id: collection.name,
|
||||
isExpanded: false
|
||||
};
|
||||
});
|
||||
|
||||
uiStateActions.setIconCategoriesState(categoriesState);
|
||||
|
||||
setIsReady(true);
|
||||
},
|
||||
[changeView, model.actions]
|
||||
[changeView, model.actions, size, uiStateActions]
|
||||
);
|
||||
|
||||
const clear = useCallback(() => {
|
||||
@@ -62,7 +94,6 @@ export const useModel = () => {
|
||||
return {
|
||||
load,
|
||||
clear,
|
||||
isReady,
|
||||
...model
|
||||
isReady
|
||||
};
|
||||
};
|
||||
@@ -1,11 +1,7 @@
|
||||
import { produce } from 'immer';
|
||||
import { TextBox } from 'src/types';
|
||||
import { getItemByIdOrThrow, getTextWidth } from 'src/utils';
|
||||
import {
|
||||
DEFAULT_FONT_FAMILY,
|
||||
TEXTBOX_DEFAULTS,
|
||||
TEXTBOX_FONT_WEIGHT
|
||||
} from 'src/config';
|
||||
import { getItemByIdOrThrow, getTextBoxDimensions } from 'src/utils';
|
||||
|
||||
import { State } from './types';
|
||||
|
||||
export const syncTextBox = (
|
||||
@@ -17,15 +13,9 @@ export const syncTextBox = (
|
||||
const view = getItemByIdOrThrow(draft.model.views, viewId);
|
||||
const textBox = getItemByIdOrThrow(view.value.textBoxes ?? [], id);
|
||||
|
||||
const width = getTextWidth(textBox.value.content, {
|
||||
fontSize: textBox.value.fontSize ?? TEXTBOX_DEFAULTS.fontSize,
|
||||
fontFamily: DEFAULT_FONT_FAMILY,
|
||||
fontWeight: TEXTBOX_FONT_WEIGHT
|
||||
});
|
||||
const height = 1;
|
||||
const size = { width, height };
|
||||
const textBoxSize = getTextBoxDimensions(textBox.value);
|
||||
|
||||
draft.scene.textBoxes[textBox.value.id] = { size };
|
||||
draft.scene.textBoxes[textBox.value.id] = { size: textBoxSize };
|
||||
});
|
||||
|
||||
return newState;
|
||||
|
||||
@@ -4,8 +4,7 @@ import {
|
||||
CoordsUtils,
|
||||
incrementZoom,
|
||||
decrementZoom,
|
||||
getStartingMode,
|
||||
getTileScrollPosition
|
||||
getStartingMode
|
||||
} from 'src/utils';
|
||||
import { UiStateStore } from 'src/types';
|
||||
import { INITIAL_UI_STATE } from 'src/config';
|
||||
@@ -78,14 +77,6 @@ const initialState = () => {
|
||||
setScroll: ({ position, offset }) => {
|
||||
set({ scroll: { position, offset: offset ?? get().scroll.offset } });
|
||||
},
|
||||
scrollToTile: (tile, origin) => {
|
||||
const scrollTo = getTileScrollPosition(tile, origin);
|
||||
|
||||
get().actions.setScroll({
|
||||
offset: CoordsUtils.zero(),
|
||||
position: scrollTo
|
||||
});
|
||||
},
|
||||
setItemControls: (itemControls) => {
|
||||
set({ itemControls });
|
||||
},
|
||||
@@ -95,7 +86,7 @@ const initialState = () => {
|
||||
setMouse: (mouse) => {
|
||||
set({ mouse });
|
||||
},
|
||||
setenableDebugTools: (enableDebugTools) => {
|
||||
setEnableDebugTools: (enableDebugTools) => {
|
||||
set({ enableDebugTools });
|
||||
},
|
||||
setRendererEl: (el) => {
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { Coords } from 'src/types';
|
||||
import type { EditorModeEnum, MainMenuOptions } from './common';
|
||||
import type { Model } from './model';
|
||||
|
||||
export type InitialData = Model & {
|
||||
zoom?: number;
|
||||
scroll?: Coords;
|
||||
fitToView?: boolean;
|
||||
};
|
||||
|
||||
export interface IsoflowProps {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Coords, EditorModeEnum, MainMenuOptions } from './common';
|
||||
import { Icon } from './model';
|
||||
import { TileOrigin, ItemReference } from './scene';
|
||||
import { ItemReference } from './scene';
|
||||
|
||||
interface AddItemControls {
|
||||
type: 'ADD_ITEM';
|
||||
@@ -161,12 +161,11 @@ export interface UiStateActions {
|
||||
setDialog: (dialog: keyof typeof DialogTypeEnum | null) => void;
|
||||
setZoom: (zoom: number) => void;
|
||||
setScroll: (scroll: Scroll) => void;
|
||||
scrollToTile: (tile: Coords, origin?: TileOrigin) => void;
|
||||
setItemControls: (itemControls: ItemControls | null) => void;
|
||||
setContextMenu: (contextMenu: ContextMenu | null) => void;
|
||||
setMouse: (mouse: Mouse) => void;
|
||||
setRendererEl: (el: HTMLDivElement) => void;
|
||||
setenableDebugTools: (enabled: boolean) => void;
|
||||
setEnableDebugTools: (enabled: boolean) => void;
|
||||
}
|
||||
|
||||
export type UiStateStore = UiState & {
|
||||
|
||||
@@ -6,7 +6,11 @@ import {
|
||||
MAX_ZOOM,
|
||||
MIN_ZOOM,
|
||||
TEXTBOX_PADDING,
|
||||
CONNECTOR_SEARCH_OFFSET
|
||||
CONNECTOR_SEARCH_OFFSET,
|
||||
DEFAULT_FONT_FAMILY,
|
||||
TEXTBOX_DEFAULTS,
|
||||
TEXTBOX_FONT_WEIGHT,
|
||||
PROJECT_BOUNDING_BOX_PADDING
|
||||
} from 'src/config';
|
||||
import {
|
||||
Coords,
|
||||
@@ -538,6 +542,17 @@ export const getTextWidth = (text: string, fontProps: FontProps) => {
|
||||
return (metrics.width + paddingX * 2) / UNPROJECTED_TILE_SIZE - 0.8;
|
||||
};
|
||||
|
||||
export const getTextBoxDimensions = (textBox: TextBox): Size => {
|
||||
const width = getTextWidth(textBox.content, {
|
||||
fontSize: textBox.fontSize ?? TEXTBOX_DEFAULTS.fontSize,
|
||||
fontFamily: DEFAULT_FONT_FAMILY,
|
||||
fontWeight: TEXTBOX_FONT_WEIGHT
|
||||
});
|
||||
const height = 1;
|
||||
|
||||
return { width, height };
|
||||
};
|
||||
|
||||
export const outermostCornerPositions: TileOrigin[] = [
|
||||
'BOTTOM',
|
||||
'RIGHT',
|
||||
@@ -647,3 +662,102 @@ export const getConnectorDirectionIcon = (connectorTiles: Coords[]) => {
|
||||
rotation
|
||||
};
|
||||
};
|
||||
|
||||
export const getProjectBounds = (
|
||||
view: View,
|
||||
padding = PROJECT_BOUNDING_BOX_PADDING
|
||||
): Coords[] => {
|
||||
const itemTiles = view.items.map((item) => {
|
||||
return item.tile;
|
||||
});
|
||||
|
||||
const connectors = view.connectors ?? [];
|
||||
const connectorTiles = connectors.reduce<Coords[]>((acc, connector) => {
|
||||
const path = getConnectorPath({ anchors: connector.anchors, view });
|
||||
|
||||
return [...acc, path.rectangle.from, path.rectangle.to];
|
||||
}, []);
|
||||
|
||||
const rectangles = view.rectangles ?? [];
|
||||
const rectangleTiles = rectangles.reduce<Coords[]>((acc, rectangle) => {
|
||||
return [...acc, rectangle.from, rectangle.to];
|
||||
}, []);
|
||||
|
||||
const textBoxes = view.textBoxes ?? [];
|
||||
const textBoxTiles = textBoxes.reduce<Coords[]>((acc, textBox) => {
|
||||
const size = getTextBoxDimensions(textBox);
|
||||
|
||||
return [
|
||||
...acc,
|
||||
textBox.tile,
|
||||
CoordsUtils.add(textBox.tile, {
|
||||
x: size.width,
|
||||
y: size.height
|
||||
})
|
||||
];
|
||||
}, []);
|
||||
|
||||
let allTiles = [
|
||||
...itemTiles,
|
||||
...connectorTiles,
|
||||
...rectangleTiles,
|
||||
...textBoxTiles
|
||||
];
|
||||
|
||||
if (allTiles.length === 0) {
|
||||
const centerTile = CoordsUtils.zero();
|
||||
allTiles = [centerTile, centerTile, centerTile, centerTile];
|
||||
}
|
||||
|
||||
const corners = getBoundingBox(allTiles, {
|
||||
x: padding,
|
||||
y: padding
|
||||
});
|
||||
|
||||
return corners;
|
||||
};
|
||||
|
||||
export const getUnprojectedBounds = (view: View) => {
|
||||
const projectBounds = getProjectBounds(view);
|
||||
|
||||
const cornerPositions = projectBounds.map((corner) => {
|
||||
return getTilePosition({
|
||||
tile: corner
|
||||
});
|
||||
});
|
||||
const sortedCorners = sortByPosition(cornerPositions);
|
||||
const topLeft = { x: sortedCorners.lowX, y: sortedCorners.lowY };
|
||||
const size = getBoundingBoxSize(cornerPositions);
|
||||
|
||||
return {
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
x: topLeft.x,
|
||||
y: topLeft.y
|
||||
};
|
||||
};
|
||||
|
||||
export const getFitToViewParams = (view: View, viewportSize: Size) => {
|
||||
const projectBounds = getProjectBounds(view);
|
||||
const sortedCornerPositions = sortByPosition(projectBounds);
|
||||
const boundingBoxSize = getBoundingBoxSize(projectBounds);
|
||||
const unprojectedBounds = getUnprojectedBounds(view);
|
||||
const zoom = clamp(
|
||||
Math.min(
|
||||
viewportSize.width / unprojectedBounds.width,
|
||||
viewportSize.height / unprojectedBounds.height
|
||||
),
|
||||
0,
|
||||
MAX_ZOOM
|
||||
);
|
||||
const scrollTarget: Coords = {
|
||||
x: (sortedCornerPositions.lowX + boundingBoxSize.width / 2) * zoom,
|
||||
y: (sortedCornerPositions.lowY + boundingBoxSize.height / 2) * zoom
|
||||
};
|
||||
const scroll = getTileScrollPosition(scrollTarget);
|
||||
|
||||
return {
|
||||
zoom,
|
||||
scroll
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user