diff --git a/demos/demo4 (schema v2).json b/demos/demo4 (schema v2).json index 2b2ca4fb3..10381a922 100644 --- a/demos/demo4 (schema v2).json +++ b/demos/demo4 (schema v2).json @@ -206,13 +206,17 @@ "components": { "diod1": { "parameters": { - "pin": "12" + "pin": "12", + "labelColor": "#0008f5", + "label": "1" }, "type": "LED" }, "button1": { "parameters": { - "pin": "4" + "pin": "4", + "labelColor": "#c70000", + "label": "1" }, "type": "Button" } diff --git a/src/renderer/src/components/ComponentEditModal.tsx b/src/renderer/src/components/ComponentEditModal.tsx index 1bebf6306..8f2d9adf3 100644 --- a/src/renderer/src/components/ComponentEditModal.tsx +++ b/src/renderer/src/components/ComponentEditModal.tsx @@ -1,9 +1,9 @@ import React, { useEffect, useState } from 'react'; -import { Modal } from './Modal/Modal'; - -import { ComponentProto } from '@renderer/types/platform'; import { Component as ComponentData } from '@renderer/types/diagram'; +import { ComponentProto } from '@renderer/types/platform'; + +import { Modal } from './Modal/Modal'; interface ComponentEditModalProps { isOpen: boolean; @@ -77,15 +77,37 @@ export const ComponentEditModal: React.FC = ({ {proto.singletone ? ( '' ) : ( - + <> + + + + )} {Object.entries(proto.parameters ?? {}).map(([idx, param]) => { const name = param.name ?? idx; diff --git a/src/renderer/src/components/CreateModal.tsx b/src/renderer/src/components/CreateModal.tsx index 8c9f9c69e..300309daf 100644 --- a/src/renderer/src/components/CreateModal.tsx +++ b/src/renderer/src/components/CreateModal.tsx @@ -105,10 +105,7 @@ export const CreateModal: React.FC = ({ value: idx, label: (
- + {machine.platform.getFullComponentIcon(idx, 'mr-1 h-7 w-7')} {idx}
), @@ -240,8 +237,7 @@ export const CreateModal: React.FC = ({ const [argForm, setArgForm] = useState([]); const retrieveArgForm = (compo: string, method: string) => { - const compoType = machine.platform.resolveComponent(compo); - const component = machine.platform.data.components[compoType]; + const component = machine.platform.getComponent(compo); if (!component) return []; const argList: ArgumentProto[] | undefined = isEditingEvent @@ -794,10 +790,7 @@ export const CreateModal: React.FC = ({ 'm-2 flex min-h-[3rem] w-36 items-center justify-around rounded-lg border-2 bg-neutral-700 px-1' )} > - + {machine.platform.getFullComponentIcon(data.component)}
= ({ value: idx, label: (
- + {machine.platform.getFullComponentIcon(idx, 'mr-1 h-7 w-7')} {idx}
), @@ -128,8 +125,7 @@ export const CreateEventsModal: React.FC = ({ const [argForm, setArgForm] = useState([]); const retrieveArgForm = (compo: string, method: string) => { - const compoType = machine.platform.resolveComponent(compo); - const component = machine.platform.data.components[compoType]; + const component = machine.platform.getComponent(compo); if (!component) return []; const argList: ArgumentProto[] | undefined = isEditingEvent diff --git a/src/renderer/src/components/Explorer.tsx b/src/renderer/src/components/Explorer.tsx index 6c33d156b..a980cdd8f 100644 --- a/src/renderer/src/components/Explorer.tsx +++ b/src/renderer/src/components/Explorer.tsx @@ -3,7 +3,6 @@ import React, { useState } from 'react'; import { twMerge } from 'tailwind-merge'; import { ReactComponent as AddIcon } from '@renderer/assets/icons/new transition.svg'; -import UnknownIcon from '@renderer/assets/icons/unknown.svg'; import { ComponentEditModal, ComponentAddModal, ComponentDeleteModal } from '@renderer/components'; import { useAddComponent, useEditDeleteComponent } from '@renderer/hooks'; import { CanvasEditor } from '@renderer/lib/CanvasEditor'; @@ -86,12 +85,7 @@ export const Explorer: React.FC = ({ editor, manager }) => { onDoubleClick={() => onCompDblClick(key)} onContextMenu={() => onCompRightClick(key)} > - + {editor?.container.machine.platform?.getFullComponentIcon(key)}

{key}

)} diff --git a/src/renderer/src/lib/data/PlatformManager.ts b/src/renderer/src/lib/data/PlatformManager.tsx similarity index 82% rename from src/renderer/src/lib/data/PlatformManager.ts rename to src/renderer/src/lib/data/PlatformManager.tsx index dd90f335f..390b71304 100644 --- a/src/renderer/src/lib/data/PlatformManager.ts +++ b/src/renderer/src/lib/data/PlatformManager.tsx @@ -1,9 +1,15 @@ -import { Platform } from '@renderer/types/platform'; -import { icons, picto } from '../drawable/Picto'; import { Action, Condition, Event, Variable } from '@renderer/types/diagram'; -import { ComponentProto } from '@renderer/types/platform'; +import { Platform, ComponentProto } from '@renderer/types/platform'; + +import { MarkedIconData, icons, picto } from '../drawable/Picto'; import { stateStyle } from '../styles'; +export type VisualCompoData = { + component: string; + label?: string; + color?: string; +}; + export type ListEntry = { name: string; description?: string; @@ -46,11 +52,12 @@ export class PlatformManager { data!: Platform; /** - * Проекция названия компонента к его типу. + * Проекция названия компонента к его типу и метке. * Если платформа не видит проекцию, она будет считать - * переданное название типом компонента. + * переданное название типом компонента, + * а данные метки – пустыми. */ - nameToComponent: Map = new Map(); + nameToVisual: Map = new Map(); componentToIcon: Map = new Map(); eventToIcon: Map = new Map(); @@ -97,13 +104,17 @@ export class PlatformManager { } } - resolveComponent(name: string): string { - return this.nameToComponent.get(name) ?? name; + resolveComponent(name: string) { + return this.nameToVisual.get(name) ?? { component: name }; + } + + resolveComponentType(name: string): string { + return this.nameToVisual.get(name)?.component ?? name; } getComponent(name: string, isType?: boolean): ComponentProto | undefined { if (name == 'System') return systemComponent; - const query = isType ? name : this.resolveComponent(name); + const query = isType ? name : this.resolveComponentType(name); return this.data.components[query]; } @@ -153,7 +164,7 @@ export class PlatformManager { } getComponentIcon(name: string, isName?: boolean) { - const query = isName ? this.resolveComponent(name) : name; + const query = isName ? this.resolveComponentType(name) : name; const icon = this.componentToIcon.get(query); // console.log(['getComponentIcon', name, isName, icon]); if (icon && icons.has(icon)) { @@ -169,6 +180,17 @@ export class PlatformManager { return icons.get(query)!.src; } + getFullComponentIcon(name: string, className?: string): React.ReactNode { + const query = this.nameToVisual.get(name) ?? { component: name }; + const iconQuery = { + ...query, + icon: this.getComponentIcon(query.component, false), + }; + // console.log(['getComponentIcon', name, isName, query, icons.get(query)!.src]); + // return ; + return picto.getMarkedSvg(iconQuery, className); + } + getEventIcon(component: string, method: string) { const icon = this.eventToIcon.get(`${component}/${method}`); if (icon && icons.has(icon)) { @@ -179,7 +201,7 @@ export class PlatformManager { } getEventIconUrl(component: string, method: string, isName?: boolean) { - const compoQuery = isName ? this.resolveComponent(component) : component; + const compoQuery = isName ? this.resolveComponentType(component) : component; const query = this.getEventIcon(compoQuery, method); // console.log(['getEventIconUrl', component, isName, compoQuery, method, query, icons.get(query)!.src,]); return icons.get(query)!.src; @@ -195,7 +217,7 @@ export class PlatformManager { } getActionIconUrl(component: string, method: string, isName?: boolean) { - const compoQuery = isName ? this.resolveComponent(component) : component; + const compoQuery = isName ? this.resolveComponentType(component) : component; const query = this.getActionIcon(compoQuery, method); // console.log(['getActionIconUrl', component, isName, compoQuery, method, query, icons.get(query)!.src,]); return icons.get(query)!.src; @@ -211,25 +233,29 @@ export class PlatformManager { } getVariableIconUrl(component: string, method: string, isName?: boolean) { - const compoQuery = isName ? this.resolveComponent(component) : component; + const compoQuery = isName ? this.resolveComponentType(component) : component; const query = this.getVariableIcon(compoQuery, method); // console.log(['getEventIconUrl', component, isName, compoQuery, method, query, icons.get(query)!.src,]); return icons.get(query)!.src; } drawEvent(ctx: CanvasRenderingContext2D, ev: Event, x: number, y: number) { - let leftIcon: string | undefined = undefined; + let leftIcon: string | MarkedIconData | undefined = undefined; let rightIcon = 'unknown'; - let bgColor = '#3a426b'; - let fgColor = '#fff'; + const bgColor = '#3a426b'; + const fgColor = '#fff'; let argQuery: string = ''; if (ev.component === 'System') { // ev.method === 'onEnter' || ev.method === 'onExit' rightIcon = ev.method; } else { - const component = this.resolveComponent(ev.component); - leftIcon = this.getComponentIcon(component); + const compoData = this.resolveComponent(ev.component); + const component = compoData.component; + leftIcon = { + ...compoData, + icon: this.getComponentIcon(component), + }; rightIcon = this.getEventIcon(component, ev.method); const parameterList = this.data.components[component]?.signals[ev.method]?.parameters; @@ -262,18 +288,22 @@ export class PlatformManager { } drawAction(ctx: CanvasRenderingContext2D, ac: Action, x: number, y: number, alpha?: number) { - let leftIcon: string | undefined = undefined; + let leftIcon: string | MarkedIconData | undefined = undefined; let rightIcon = 'unknown'; - let bgColor = '#5b5f73'; - let fgColor = '#fff'; - let opacity = alpha ?? 1.0; + const bgColor = '#5b5f73'; + const fgColor = '#fff'; + const opacity = alpha ?? 1.0; let argQuery: string = ''; if (ac.component === 'System') { rightIcon = ac.method; } else { - const component = this.resolveComponent(ac.component); - leftIcon = this.getComponentIcon(component); + const compoData = this.resolveComponent(ac.component); + const component = compoData.component; + leftIcon = { + ...compoData, + icon: this.getComponentIcon(component), + }; rightIcon = this.getActionIcon(component, ac.method); const parameterList = this.data.components[component]?.methods[ac.method]?.parameters; @@ -335,12 +365,12 @@ export class PlatformManager { y: number, alpha?: number ) { - let bgColor = '#5b7173'; - let fgColor = '#fff'; - let opacity = alpha ?? 1.0; + const bgColor = '#5b7173'; + const fgColor = '#fff'; + const opacity = alpha ?? 1.0; if (ac.type == 'component') { - let leftIcon: string | undefined = undefined; + let leftIcon: string | MarkedIconData | undefined = undefined; let rightIcon = 'unknown'; // FIXME: столько проверок ради простой валидации... @@ -353,8 +383,12 @@ export class PlatformManager { if (vr.component === 'System') { rightIcon = vr.method; } else { - const component = this.resolveComponent(vr.component); - leftIcon = this.getComponentIcon(component); + const compoData = this.resolveComponent(vr.component); + const component = compoData.component; + leftIcon = { + ...compoData, + icon: this.getComponentIcon(component), + }; rightIcon = this.getVariableIcon(component, vr.method); } } @@ -379,7 +413,7 @@ export class PlatformManager { const mr = picto.eventMargin; const icoW = (picto.eventHeight + picto.eventMargin) / picto.scale; - let leftW = (this.measureCondition(ac.value[0]) + mr) / picto.scale; + const leftW = (this.measureCondition(ac.value[0]) + mr) / picto.scale; this.drawCondition(ctx, ac.value[0], x, y, alpha); picto.drawMono(ctx, x + leftW, y, { diff --git a/src/renderer/src/lib/data/StateMachine.ts b/src/renderer/src/lib/data/StateMachine.ts index 72ac58d0d..8b07d56f7 100644 --- a/src/renderer/src/lib/data/StateMachine.ts +++ b/src/renderer/src/lib/data/StateMachine.ts @@ -113,7 +113,11 @@ export class StateMachine extends EventEmitter { for (const name in items) { const component = items[name]; // this.components.set(name, new Component(component)); - this.platform.nameToComponent.set(name, component.type); + this.platform.nameToVisual.set(name, { + component: component.type, + label: component.parameters['label'], + color: component.parameters['labelColor'], + }); } } @@ -462,7 +466,9 @@ export class StateMachine extends EventEmitter { addComponent(name: string, type: string) { this.container.app.manager.addComponent(name, type); - this.platform.nameToComponent.set(name, type); + this.platform.nameToVisual.set(name, { + component: type, + }); this.container.isDirty = true; } @@ -470,6 +476,13 @@ export class StateMachine extends EventEmitter { editComponent(name: string, parameters: ComponentType['parameters'], newName?: string) { this.container.app.manager.editComponent(name, parameters); + const component = this.container.app.manager.data.elements.components[name]; + this.platform.nameToVisual.set(name, { + component: component.type, + label: component.parameters['label'], + color: component.parameters['labelColor'], + }); + if (newName) { this.renameComponent(name, newName); } @@ -479,10 +492,10 @@ export class StateMachine extends EventEmitter { private renameComponent(name: string, newName: string) { this.container.app.manager.renameComponent(name, newName); - const component = this.container.app.manager.data.elements.components[newName]; - this.platform.nameToComponent.set(newName, component.type); - this.platform.nameToComponent.delete(name); + const visualCompo = this.platform.nameToVisual.get(name)!; + this.platform.nameToVisual.set(newName, visualCompo); + this.platform.nameToVisual.delete(name); // А сейчас будет занимательное путешествие по схеме с заменой всего this.states.forEach((state) => { @@ -550,7 +563,7 @@ export class StateMachine extends EventEmitter { console.error('removeComponent purge not implemented yet'); } - this.platform.nameToComponent.delete(name); + this.platform.nameToVisual.delete(name); this.container.isDirty = true; } diff --git a/src/renderer/src/lib/drawable/Picto.ts b/src/renderer/src/lib/drawable/Picto.tsx similarity index 68% rename from src/renderer/src/lib/drawable/Picto.ts rename to src/renderer/src/lib/drawable/Picto.tsx index e19c17ea5..9134ab700 100644 --- a/src/renderer/src/lib/drawable/Picto.ts +++ b/src/renderer/src/lib/drawable/Picto.tsx @@ -1,10 +1,16 @@ import InitialIcon from '@renderer/assets/icons/initial state.svg'; -import UnknownIcon from '@renderer/assets/icons/unknown-alt.svg'; import EdgeHandle from '@renderer/assets/icons/new transition.svg'; +import UnknownIcon from '@renderer/assets/icons/unknown-alt.svg'; import { Rectangle } from '@renderer/types/graphics'; import { drawImageFit, preloadImagesMap } from '../utils'; +export type MarkedIconData = { + icon: string; + label?: string; + color?: string; +}; + let imagesLoaded = false; export const icons: Map = new Map(); @@ -62,7 +68,7 @@ export function preloadPicto(callback: () => void) { } export type PictoProps = { - leftIcon?: string; + leftIcon?: string | MarkedIconData; rightIcon: string; bgColor?: string; fgColor?: string; @@ -82,8 +88,17 @@ export class Picto { return imagesLoaded; } - drawImage(ctx: CanvasRenderingContext2D, iconName: string, bounds: Rectangle) { + /** + * Рисует масштабированный значок на canvas. + * + * @param ctx Контекст canvas, в котором рисуем + * @param iconData Название значка или контейнер с данными для метки + * @param bounds Координаты и размер рамки + */ + drawImage(ctx: CanvasRenderingContext2D, iconData: string | MarkedIconData, bounds: Rectangle) { // console.log([iconName, icons.has(iconName)]); + const isMarked = typeof iconData !== 'string'; + const iconName = isMarked ? iconData.icon : iconData; const image = icons.get(iconName); if (!image) return; @@ -93,10 +108,64 @@ export class Picto { width: bounds.width / this.scale, height: bounds.height / this.scale, }); + if (isMarked && iconData.label) { + const { x, y, width, height } = bounds; + const tX = x + width / this.scale; + const tY = y + (height - 1) / this.scale; + ctx.save(); + ctx.font = `600 ${16 / this.scale}px/0 Fira Mono`; + ctx.fillStyle = iconData.color ?? 'white'; + ctx.strokeStyle = 'white'; + ctx.lineWidth = 0.5 / this.scale; + ctx.textAlign = 'end'; + ctx.textBaseline = 'alphabetic'; + + ctx.fillText(iconData.label, tX, tY); + ctx.strokeText(iconData.label, tX, tY); + + ctx.restore(); + } ctx.closePath(); } - // TODO: все перечисленные ниже функции нужно вернуть в законные места + /** + * Генерирует SVG-ноду для значка с меткой. + * По сути, дублирует {@link drawImage} вне canvas. + * + * @param data Контейнер с данными значка + * @param className Атрибут class для генерируемой ноды (дополнительно) + * @returns JSX-нода со значком + */ + getMarkedSvg(data: MarkedIconData, className?: string) { + const icon = icons.get(data.icon); + return ( + + ; + {!data.label ? ( + '' + ) : ( + + {data.label} + + )} + + + ); + } eventWidth = 100; eventHeight = 40; @@ -150,10 +219,10 @@ export class Picto { } drawMono(ctx: CanvasRenderingContext2D, x: number, y: number, ps: PictoProps) { - let rightIcon = ps.rightIcon; - let bgColor = ps.bgColor ?? '#3a426b'; - let fgColor = ps.fgColor ?? '#fff'; - let opacity = ps.opacity ?? 1.0; + const rightIcon = ps.rightIcon; + const bgColor = ps.bgColor ?? '#3a426b'; + const fgColor = ps.fgColor ?? '#fff'; + const opacity = ps.opacity ?? 1.0; // Рамка this.drawRect(ctx, x, y, this.eventHeight, this.eventHeight, bgColor, fgColor, opacity); @@ -169,10 +238,10 @@ export class Picto { } drawText(ctx: CanvasRenderingContext2D, x: number, y: number, ps: PictoProps) { - let text = ps.rightIcon; - let bgColor = ps.bgColor ?? '#3a426b'; - let fgColor = ps.fgColor ?? '#fff'; - let opacity = ps.opacity ?? 1.0; + const text = ps.rightIcon; + const bgColor = ps.bgColor ?? '#3a426b'; + const fgColor = ps.fgColor ?? '#fff'; + const opacity = ps.opacity ?? 1.0; const baseFontSize = 24; const w = this.textPadding * 2 + text.length * this.pxPerChar; @@ -194,12 +263,21 @@ export class Picto { ctx.restore(); } + /** + * Рисует масштабированную пиктограмму на canvas. + * Главная функция в этом классе. + * + * @param ctx Контекст canvas, в котором рисуем + * @param x X-координата + * @param y Y-координата + * @param ps Контейнер с параметрами пиктограммы + */ drawPicto(ctx: CanvasRenderingContext2D, x: number, y: number, ps: PictoProps) { - let leftIcon = ps.leftIcon; - let rightIcon = ps.rightIcon; - let bgColor = ps.bgColor ?? '#3a426b'; - let fgColor = ps.fgColor ?? '#fff'; - let opacity = ps.opacity ?? 1.0; + const leftIcon = ps.leftIcon; + const rightIcon = ps.rightIcon; + const bgColor = ps.bgColor ?? '#3a426b'; + const fgColor = ps.fgColor ?? '#fff'; + const opacity = ps.opacity ?? 1.0; // Рамка this.drawBorder(ctx, x, y, bgColor, fgColor, opacity);