feat: stores colors as part of model

This commit is contained in:
Mark Mankarious
2023-10-29 14:08:32 +00:00
parent 35b73defe7
commit 4797b22304
36 changed files with 202 additions and 101 deletions

View File

@@ -10,15 +10,17 @@ import {
Connector,
Rectangle,
IsoflowProps,
InitialData
InitialData,
EditorConfig,
Colors,
Icons
} from 'src/types';
import { setWindowCursor, generateId } from 'src/utils';
import { modelSchema } from 'src/validation/model';
import { modelSchema } from 'src/schemas/model';
import { useModelStore, ModelProvider } from 'src/stores/modelStore';
import { SceneProvider } from 'src/stores/sceneStore';
import { GlobalStyles } from 'src/styles/GlobalStyles';
import { Renderer } from 'src/components/Renderer/Renderer';
import { useWindowUtils } from 'src/hooks/useWindowUtils';
import { UiOverlay } from 'src/components/UiOverlay/UiOverlay';
import { UiStateProvider, useUiStateStore } from 'src/stores/uiStateStore';
import {
@@ -174,10 +176,13 @@ const useIsoflow = () => {
export {
useIsoflow,
InitialData,
EditorConfig,
Model,
Icon,
ModelItem,
Rectangle,
Colors,
Icons,
Connector,
modelSchema,
IsoflowProps

View File

@@ -1,25 +1,27 @@
import React from 'react';
import { Box } from '@mui/material';
import { useScene } from 'src/hooks/useScene';
import { ColorSwatch } from './ColorSwatch';
interface Props {
colors: string[];
onChange: (color: string) => void;
activeColor: string;
}
export const ColorSelector = ({ colors, onChange, activeColor }: Props) => {
export const ColorSelector = ({ onChange, activeColor }: Props) => {
const { colors } = useScene();
return (
<Box>
{colors.map((color) => {
return (
<ColorSwatch
key={color}
hex={color}
key={color.id}
hex={color.value}
onClick={() => {
return onChange(color);
return onChange(color.id);
}}
isActive={activeColor === color}
isActive={activeColor === color.id}
/>
);
})}

View File

@@ -1,13 +1,6 @@
import React from 'react';
import { Connector, connectorStyleOptions } from 'src/types';
import {
useTheme,
Box,
Slider,
Select,
MenuItem,
TextField
} from '@mui/material';
import { Box, Slider, Select, MenuItem, TextField } from '@mui/material';
import { useConnector } from 'src/hooks/useConnector';
import { ColorSelector } from 'src/components/ColorSelector/ColorSelector';
import { useUiStateStore } from 'src/stores/uiStateStore';
@@ -21,7 +14,6 @@ interface Props {
}
export const ConnectorControls = ({ id }: Props) => {
const theme = useTheme();
const uiStateActions = useUiStateStore((state) => {
return state.actions;
});
@@ -43,7 +35,6 @@ export const ConnectorControls = ({ id }: Props) => {
</Section>
<Section>
<ColorSelector
colors={Object.values(theme.customVars.customPalette)}
onChange={(color) => {
return updateConnector(connector.id, { color });
}}

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { useTheme, Box } from '@mui/material';
import { Box } from '@mui/material';
import { useRectangle } from 'src/hooks/useRectangle';
import { ColorSelector } from 'src/components/ColorSelector/ColorSelector';
import { useUiStateStore } from 'src/stores/uiStateStore';
@@ -13,7 +13,6 @@ interface Props {
}
export const RectangleControls = ({ id }: Props) => {
const theme = useTheme();
const uiStateActions = useUiStateStore((state) => {
return state.actions;
});
@@ -24,7 +23,6 @@ export const RectangleControls = ({ id }: Props) => {
<ControlsContainer>
<Section>
<ColorSelector
colors={Object.values(theme.customVars.customPalette)}
onChange={(color) => {
updateRectangle(rectangle.id, { color });
}}

View File

@@ -78,6 +78,7 @@ export const MainMenu = () => {
const onExportAsJSON = useCallback(async () => {
const payload: Model = {
icons: model.icons,
colors: model.colors,
items: model.items,
title: model.title,
version: model.version,

View File

@@ -7,6 +7,7 @@ import { Svg } from 'src/components/Svg/Svg';
import { useIsoProjection } from 'src/hooks/useIsoProjection';
import { useConnector } from 'src/hooks/useConnector';
import { useScene } from 'src/hooks/useScene';
import { useColor } from 'src/hooks/useColor';
interface Props {
connector: ReturnType<typeof useScene>['connectors'][0];
@@ -14,6 +15,7 @@ interface Props {
export const Connector = ({ connector: _connector }: Props) => {
const theme = useTheme();
const color = useColor(_connector.color);
const { currentView } = useScene();
const connector = useConnector(_connector.id);
const { css, pxSize } = useIsoProjection({
@@ -89,7 +91,7 @@ export const Connector = ({ connector: _connector }: Props) => {
/>
<polyline
points={pathString}
stroke={getColorVariant(connector.color, 'dark', { grade: 1 })}
stroke={getColorVariant(color.value, 'dark', { grade: 1 })}
strokeWidth={connectorWidthPx}
strokeLinecap="round"
strokeLinejoin="round"

View File

@@ -1,20 +1,22 @@
import React from 'react';
import { Rectangle as RectangleI } from 'src/types';
import { useScene } from 'src/hooks/useScene';
import { IsoTileArea } from 'src/components/IsoTileArea/IsoTileArea';
import { getColorVariant } from 'src/utils';
import { DEFAULT_COLOR } from 'src/config';
import { useColor } from 'src/hooks/useColor';
type Props = RectangleI;
type Props = ReturnType<typeof useScene>['rectangles'][0];
export const Rectangle = ({ from, to, color: colorId }: Props) => {
const color = useColor(colorId);
export const Rectangle = ({ from, to, color = DEFAULT_COLOR }: Props) => {
return (
<IsoTileArea
from={from}
to={to}
fill={color}
fill={color.value}
cornerRadius={22}
stroke={{
color: getColorVariant(color, 'dark', { grade: 2 }),
color: getColorVariant(color.value, 'dark', { grade: 2 }),
width: 1
}}
/>

View File

@@ -1,9 +1,9 @@
import React from 'react';
import { Rectangle as RectangleI } from 'src/types';
import { useScene } from 'src/hooks/useScene';
import { Rectangle } from './Rectangle';
interface Props {
rectangles: RectangleI[];
rectangles: ReturnType<typeof useScene>['rectangles'];
}
export const Rectangles = ({ rectangles }: Props) => {

View File

@@ -7,7 +7,8 @@ import {
TextBox,
ViewItem,
View,
Rectangle
Rectangle,
Colors
} from 'src/types';
import { customVars } from './styles/theme';
import { CoordsUtils } from './utils';
@@ -23,7 +24,11 @@ export const PROJECTED_TILE_SIZE = {
height: UNPROJECTED_TILE_SIZE * TILE_PROJECTION_MULTIPLIERS.height
};
export const DEFAULT_COLOR = customVars.customPalette.blue;
export const DEFAULT_COLOR: Colors[0] = {
id: '__DEFAULT__',
value: customVars.customPalette.defaultColor
};
export const DEFAULT_FONT_FAMILY = 'Roboto, Arial, sans-serif';
export const VIEW_DEFAULTS: Required<Omit<View, 'id' | 'description'>> = {
@@ -41,7 +46,7 @@ export const VIEW_ITEM_DEFAULTS: Required<Omit<ViewItem, 'id' | 'tile'>> = {
export const CONNECTOR_DEFAULTS: Required<Omit<Connector, 'id'>> = {
width: 10,
description: '',
color: DEFAULT_COLOR,
color: DEFAULT_COLOR.id,
anchors: [],
style: 'SOLID'
};
@@ -62,7 +67,7 @@ export const TEXTBOX_FONT_WEIGHT = 'bold';
export const RECTANGLE_DEFAULTS: Required<
Omit<Rectangle, 'id' | 'from' | 'to'>
> = {
color: DEFAULT_COLOR
color: DEFAULT_COLOR.id
};
export const ZOOM_INCREMENT = 0.2;
@@ -74,6 +79,7 @@ export const INITIAL_DATA: Model = {
title: 'Untitled',
version: '',
icons: [],
colors: [],
items: [],
views: []
};
@@ -102,3 +108,5 @@ export const DEFAULT_ICON: Icon = {
};
export const DEFAULT_LABEL_HEIGHT = 20;
export const EDITOR_CONFIG = {};

View File

@@ -1,5 +1,5 @@
/* eslint-disable import/no-extraneous-dependencies */
import { InitialData } from 'src/Isoflow';
import { InitialData, Colors, Icons } from 'src/Isoflow';
import { flattenCollections } from '@isoflow/isopacks/dist/utils';
import isoflowIsopack from '@isoflow/isopacks/dist/isoflow';
import awsIsopack from '@isoflow/isopacks/dist/aws';
@@ -15,10 +15,38 @@ const isopacks = flattenCollections([
kubernetesIsopack
]);
// The data used in this visualisation example has been derived from the following blog post
// https://www.altexsoft.com/blog/travel/airport-technology-management-operations-software-solutions-and-vendors/
export const colors: Colors = [
{
id: 'color2',
value: '#bbadfb'
},
{
id: 'color3',
value: '#f4eb8e'
},
{
id: 'color4',
value: '#f0aca9'
},
{
id: 'color5',
value: '#fad6ac'
},
{
id: 'color6',
value: '#a8dc9d'
},
{
id: 'color7',
value: '#b3e5e3'
}
];
export const icons: Icons = isopacks;
export const initialData: InitialData = {
icons: isopacks,
icons,
colors,
items: [
{
id: 'item1',
@@ -40,6 +68,7 @@ export const initialData: InitialData = {
}
]
};
// export const initialData: InitialData = {
// title: 'Airport Management Software',
// icons: isopacks,

12
src/fixtures/colors.ts Normal file
View File

@@ -0,0 +1,12 @@
import { Colors } from 'src/types';
export const colors: Colors = [
{
id: 'color1',
value: '#000000'
},
{
id: 'color2',
value: '#ffffff'
}
];

View File

@@ -2,11 +2,13 @@ import { Model } from 'src/types';
import { icons } from './icons';
import { modelItems } from './modelItems';
import { views } from './views';
import { colors } from './colors';
export const model: Model = {
version: '1.0.0',
title: 'TestModel',
description: 'TestModelDescription',
version: '1.0.0',
colors,
icons,
items: modelItems,
views

View File

@@ -29,7 +29,11 @@ export const views: Model['views'] = [
}
],
rectangles: [
{ id: 'rectangle1', from: { x: 0, y: 0 }, to: { x: 2, y: 2 } }
{
id: 'rectangle1',
from: { x: 0, y: 0 },
to: { x: 2, y: 2 }
}
],
connectors: [
{

13
src/hooks/useColor.ts Normal file
View File

@@ -0,0 +1,13 @@
import { useMemo } from 'react';
import { getItemByIdOrThrow } from 'src/utils';
import { useScene } from 'src/hooks/useScene';
export const useColor = (colorId: string) => {
const { colors } = useScene();
const color = useMemo(() => {
return getItemByIdOrThrow(colors, colorId).value;
}, [colorId, colors]);
return color;
};

View File

@@ -16,6 +16,7 @@ import type { State } from 'src/stores/reducers/types';
import { getItemByIdOrThrow } from 'src/utils';
import {
CONNECTOR_DEFAULTS,
DEFAULT_COLOR,
RECTANGLE_DEFAULTS,
TEXTBOX_DEFAULTS
} from 'src/config';
@@ -41,6 +42,10 @@ export const useScene = () => {
return currentView.items ?? [];
}, [currentView.items]);
const colors = useMemo(() => {
return [DEFAULT_COLOR, ...model.colors];
}, [model.colors]);
const connectors = useMemo(() => {
return (currentView.connectors ?? []).map((connector) => {
const sceneConnector = scene.connectors[connector.id];
@@ -262,6 +267,7 @@ export const useScene = () => {
return {
items,
connectors,
colors,
rectangles,
textBoxes,
currentView,

View File

@@ -1,7 +1,7 @@
import { ModeActions } from 'src/types';
import { produce } from 'immer';
import { generateId, hasMovedTile, setWindowCursor } from 'src/utils';
import { DEFAULT_COLOR } from 'src/config';
import { generateId, hasMovedTile, setWindowCursor } from 'src/utils';
export const DrawRectangle: ModeActions = {
entry: () => {
@@ -31,7 +31,7 @@ export const DrawRectangle: ModeActions = {
scene.createRectangle({
id: newRectangleId,
color: DEFAULT_COLOR,
color: DEFAULT_COLOR.id,
from: uiState.mouse.position.tile,
to: uiState.mouse.position.tile
});

View File

@@ -1,5 +1,5 @@
// This file will be exported as it's own bundle (separate to the main bundle). This is because the main
// bundle requires `window` to be present and so can't be imported into a Node environment.
export { INITIAL_DATA } from 'src/config';
export { modelSchema } from 'src/validation/model';
export { modelSchema } from 'src/schemas/model';
export const version = PACKAGE_VERSION;

View File

@@ -13,6 +13,7 @@ describe('Model validation works correctly', () => {
test('Connector with anchor that references an invalid item fails validation', () => {
const invalidConnector: Connector = {
id: 'invalidConnector',
color: 'color1',
anchors: [
{ id: 'testAnch', ref: { item: 'node1' } },
{ id: 'testAnch2', ref: { item: 'invalidItem' } }
@@ -31,6 +32,7 @@ describe('Model validation works correctly', () => {
test('Connector with less than two anchors fails validation', () => {
const invalidConnector: Connector = {
id: 'invalidConnector',
color: 'color1',
anchors: []
};
@@ -46,6 +48,7 @@ describe('Model validation works correctly', () => {
test('Connector with anchor that references an invalid anchor fails validation', () => {
const invalidConnector: Connector = {
id: 'invalidConnector',
color: 'color1',
anchors: [
{ id: 'testAnch1', ref: { anchor: 'invalidAnchor' } },
{ id: 'testAnch2', ref: { anchor: 'anchor1' } }

9
src/schemas/colors.ts Normal file
View File

@@ -0,0 +1,9 @@
import { z } from 'zod';
import { id } from './common';
export const colorSchema = z.object({
id,
value: z.string().max(7)
});
export const colorsSchema = z.array(colorSchema);

View File

@@ -1,5 +1,5 @@
import { z } from 'zod';
import { coords, id, constrainedStrings, color } from '../common';
import { coords, id, constrainedStrings } from './common';
export const connectorStyleOptions = ['SOLID', 'DOTTED', 'DASHED'] as const;
@@ -17,7 +17,7 @@ export const anchorSchema = z.object({
export const connectorSchema = z.object({
id,
description: constrainedStrings.description.optional(),
color: color.optional(),
color: id.optional(),
width: z.number().optional(),
style: z.enum(connectorStyleOptions).optional(),
anchors: z.array(anchorSchema)

8
src/schemas/index.ts Normal file
View File

@@ -0,0 +1,8 @@
export * from './model';
export * from './colors';
export * from './icons';
export * from './modelItems';
export * from './views';
export * from './connector';
export * from './rectangle';
export * from './textBox'

View File

@@ -3,20 +3,22 @@ import { INITIAL_DATA } from '../config';
import { constrainedStrings } from './common';
import { modelItemsSchema } from './modelItems';
import { viewsSchema } from './views';
import { iconsSchema } from './icons';
import { validateModel } from './utils';
import { iconsSchema } from './icons';
import { colorsSchema } from './colors';
export const modelSchema = z
.object({
version: z.string().max(10),
title: constrainedStrings.name,
description: constrainedStrings.description.optional(),
icons: iconsSchema,
items: modelItemsSchema,
views: viewsSchema
views: viewsSchema,
icons: iconsSchema,
colors: colorsSchema
})
.superRefine((_Model, ctx) => {
const issues = validateModel({ ...INITIAL_DATA, ..._Model });
.superRefine((model, ctx) => {
const issues = validateModel({ ...INITIAL_DATA, ...model });
issues.forEach((issue) => {
ctx.addIssue({

View File

@@ -1,9 +1,9 @@
import { z } from 'zod';
import { id, color, coords } from '../common';
import { id, coords } from './common';
export const rectangleSchema = z.object({
id,
color: color.optional(),
color: id.optional(),
from: coords,
to: coords
});

View File

@@ -1,6 +1,6 @@
import { z } from 'zod';
import { ProjectionOrientationEnum } from 'src/types/common';
import { id, coords, constrainedStrings } from '../common';
import { id, coords, constrainedStrings } from './common';
export const textBoxSchema = z.object({
id,

View File

@@ -1,8 +1,8 @@
import { z } from 'zod';
import { id, constrainedStrings, coords } from './common';
import { rectangleSchema } from './annotations/rectangle';
import { connectorSchema } from './annotations/connector';
import { textBoxSchema } from './annotations/textBox';
import { rectangleSchema } from './rectangle';
import { connectorSchema } from './connector';
import { textBoxSchema } from './textBox';
export const viewItemSchema = z.object({
id,

View File

@@ -1,7 +1,7 @@
import { Connector } from 'src/types';
import { produce } from 'immer';
import { getItemByIdOrThrow, getConnectorPath, getAllAnchors } from 'src/utils';
import { validateConnector } from 'src/validation/utils';
import { validateConnector } from 'src/schemas/utils';
import { State } from './types';
export const deleteConnector = (

View File

@@ -1,7 +1,7 @@
import { produce } from 'immer';
import { ViewItem } from 'src/types';
import { getItemByIdOrThrow, getConnectorsByViewItem } from 'src/utils';
import { validateView } from 'src/validation/utils';
import { validateView } from 'src/schemas/utils';
import { State } from './types';
import { updateConnector, syncConnector } from './connector';

View File

@@ -33,13 +33,7 @@ export const customVars: CustomThemeVars = {
},
customPalette: {
diagramBg: '#f6faff',
blue: '#a0b9f8',
purple: '#bbadfb',
yellow: '#f4eb8e',
red: '#f0aca9',
orange: '#fad6ac',
green: '#a8dc9d',
torquise: '#b3e5e3'
defaultColor: '#a5b8f3'
}
};

View File

@@ -3,3 +3,4 @@ export * from './model';
export * from './scene';
export * from './ui';
export * from './interactions';
export * from './isoflowProps';

26
src/types/isoflowProps.ts Normal file
View File

@@ -0,0 +1,26 @@
import { Coords } from 'src/types';
import { StoreApi } from 'zustand';
import type { EditorModeEnum, MainMenuOptions } from './common';
import type { Model } from './model';
export type InitialData = Partial<Model> & {
zoom?: number;
scroll?: Coords;
};
export interface IsoflowProps {
initialData?: InitialData;
mainMenuOptions?: MainMenuOptions;
onModelUpdated?: (Model: Model) => void;
width?: number | string;
height?: number | string;
enableDebugTools?: boolean;
editorMode?: keyof typeof EditorModeEnum;
}
export type ModelStore = Model & {
actions: {
get: StoreApi<ModelStore>['getState'];
set: StoreApi<ModelStore>['setState'];
};
};

View File

@@ -1,26 +1,31 @@
import z from 'zod';
import { iconSchema } from 'src/validation/icons';
import { modelSchema } from 'src/validation/model';
import { modelItemSchema } from 'src/validation/modelItems';
import { viewSchema, viewItemSchema } from 'src/validation/views';
import {
iconSchema,
modelSchema,
modelItemSchema,
viewSchema,
viewItemSchema,
connectorSchema,
iconsSchema,
colorsSchema,
anchorSchema,
textBoxSchema,
rectangleSchema,
connectorStyleOptions
} from 'src/validation/annotations/connector';
import { textBoxSchema } from 'src/validation/annotations/textBox';
import { rectangleSchema } from 'src/validation/annotations/rectangle';
import { Coords } from 'src/types';
import { StoreApi } from 'zustand';
import type { EditorModeEnum, MainMenuOptions } from './common';
} from 'src/schemas';
export { connectorStyleOptions } from 'src/validation/annotations/connector';
export interface EditorConfig {
icons: Icons;
colors: Colors;
}
export { connectorStyleOptions } from 'src/schemas';
export type Model = z.infer<typeof modelSchema>;
export type ModelItems = Model['items'];
export type Views = Model['views'];
export type Icons = Model['icons'];
export type Icon = z.infer<typeof iconSchema>;
export type Icons = z.infer<typeof iconsSchema>;
export type Colors = z.infer<typeof colorsSchema>;
export type ModelItem = z.infer<typeof modelItemSchema>;
export type View = z.infer<typeof viewSchema>;
export type ViewItem = z.infer<typeof viewItemSchema>;
@@ -29,25 +34,3 @@ export type ConnectorAnchor = z.infer<typeof anchorSchema>;
export type Connector = z.infer<typeof connectorSchema>;
export type TextBox = z.infer<typeof textBoxSchema>;
export type Rectangle = z.infer<typeof rectangleSchema>;
export type InitialData = Partial<Model> & {
zoom?: number;
scroll?: Coords;
};
export interface IsoflowProps {
initialData?: InitialData;
mainMenuOptions?: MainMenuOptions;
onModelUpdated?: (Model: Model) => void;
width?: number | string;
height?: number | string;
enableDebugTools?: boolean;
editorMode?: keyof typeof EditorModeEnum;
}
export type ModelStore = Model & {
actions: {
get: StoreApi<ModelStore>['getState'];
set: StoreApi<ModelStore>['setState'];
};
};

View File

@@ -1,6 +1,6 @@
import { produce } from 'immer';
import { Model } from 'src/types';
import { validateModel } from 'src/validation/utils';
import { validateModel } from 'src/schemas/utils';
import { getItemByIdOrThrow } from './common';
export const fixModel = (model: Model): Model => {