import { isExpression, type Evaluator, type ExpressionOrValue, type Scope } from '@cloudpss/expression'; import type { Model, ModelGraphic, ModelId } from '../index.js'; // eslint-disable-next-line @typescript-eslint/no-unused-vars import type { fillPinMap } from '../index.js'; import type { PinConnectionType } from '../pin.js'; import { generateTrackId, type ArgumentMap } from '@cloudpss/expression/definitions'; import type { ReadonlyDeep } from 'type-fest'; /** 保持垂直居中对齐的偏移量,em */ export const TEXT_Y_OFFSET = 0.35; /** 从垂直居中对齐调整至顶端或底端对齐的偏移量,em */ export const TEXT_X_HEIGHT = 0.4; /** 生成引脚的前缀,使用 `ɵ` 以避免被 {@link fillPinMap} 删除 */ export const MERGE_DEMERGE_PIN_PREFIX = 'ɵid-'; /** * 分离合并元件的信息 */ export interface MergeDemergeComponent { /** * 元件的主要侧,包含唯一引脚,如合并元件的输出侧,分离元件的输入侧 */ /** 主要侧引脚的键名 */ primaryKey: string; /** 主要侧引脚名称参数名 */ primaryName: string; /** 主要侧引脚方向 */ primaryDir: 'left' | 'right'; /** 主要侧引脚 Dim X 参数名 */ primaryDimX: string; /** 主要侧引脚 Dim Y 参数名 */ primaryDimY: string; /** 主要侧引脚类型 */ primaryConnection: PinConnectionType; /** * 元件的次要侧,包含多个引脚,如合并元件的输入侧,分离元件的输出侧 */ /** 次要侧引脚方向 */ secondaryDir: 'left' | 'right'; /** 次要侧引脚类型 */ secondaryConnection: PinConnectionType; /** 次要侧引脚数组参数名 */ secondaryArgs: string; /** 次要侧显示引脚箭头 path */ secondaryArrow?: boolean; /** 元件图形 */ graphic: () => ModelGraphic; /** 引脚图形长度 */ pinLength: number; } /** * 分离合并元件的信息 */ export const MERGE_DEMERGE_COMPONENTS: Record = { ['model/CloudPSS/_ChannelMerge' as ModelId]: { primaryKey: 'OutName', primaryName: 'OutName', primaryDir: 'right', primaryDimX: 'OutDimX', primaryDimY: 'OutDimY', primaryConnection: 'output', secondaryDir: 'left', secondaryConnection: 'input', secondaryArgs: 'In', secondaryArrow: true, graphic: () => ({ height: 100, width: 40, markup: [ { // 防止无法拖动 tagName: 'rect', selector: 'fallback', attrs: { stroke: 'none', fill: 'transparent', }, }, { tagName: 'path', selector: 'body', }, { tagName: 'path', selector: 'line', }, { tagName: 'text', selector: 't-out', groupSelector: 't-pin', textContent: '$OutName', }, ], attrs: { // 防止无法拖动 fallback: { refX: 0.25, refWidth: 0.5, refHeight: 1, }, body: { strokeWidth: 'calc(2px*var(--stroke-width))', refPath: { d: 'M 0.5 0 v1', w: 1, h: 1 }, }, line: { stroke: 'var(--stroke)', strokeWidth: 'calc(1px*var(--stroke-width))', strokeOpacity: 'var(--stroke-opacity)', fill: 'transparent', refPath: { d: 'M 0.5 0.5 h 0.5', w: 1, h: 1 }, }, 'p-arrow': { stroke: 'var(--stroke)', strokeWidth: 'calc(1px*var(--stroke-width))', strokeOpacity: 'var(--stroke-opacity)', fill: 'transparent', d: `m-7-5 l5 5l-5 5`, }, 't-out': { refX: 0.5, refX2: 10, refY: 0.5, textAnchor: 'start', }, 't-pin': { y: `${TEXT_Y_OFFSET - 0.6}em`, fontSize: 10, }, }, pins: { OutName: { position: { x: 0.999_999_9, y: 0.5 }, }, }, preserveAspectRatio: false, }), pinLength: 20, }, ['model/CloudPSS/_ChannelDeMerge' as ModelId]: { primaryKey: 'InName', primaryName: 'InName', primaryDir: 'left', primaryDimX: 'InDimX', primaryDimY: 'InDimY', primaryConnection: 'input', secondaryDir: 'right', secondaryConnection: 'output', secondaryArgs: 'Out', graphic: () => ({ height: 100, width: 40, markup: [ { // 防止无法拖动 tagName: 'rect', selector: 'fallback', attrs: { stroke: 'none', fill: 'transparent', }, }, { tagName: 'path', selector: 'body', }, { tagName: 'path', selector: 'line', }, { tagName: 'path', selector: 'inputArrow', groupSelector: ['p-arrow'], }, { tagName: 'text', selector: 't-in', groupSelector: 't-pin', textContent: '$InName', }, ], attrs: { // 防止无法拖动 fallback: { refX: 0.25, refWidth: 0.5, refHeight: 1, }, body: { strokeWidth: 'calc(2px*var(--stroke-width))', refPath: { d: 'M 0.5 0 v1', w: 1, h: 1 }, }, line: { stroke: 'var(--stroke)', strokeWidth: 'calc(1px*var(--stroke-width))', strokeOpacity: 'var(--stroke-opacity)', fill: 'transparent', refPath: { d: 'M 0 0.5 h 0.5', w: 1, h: 1 }, }, inputArrow: { refX: 0.5, refY: 0.5, d: `M-7-5 l5 5l-5 5`, }, 'p-arrow': { stroke: 'var(--stroke)', strokeWidth: 'calc(1px*var(--stroke-width))', strokeOpacity: 'var(--stroke-opacity)', fill: 'transparent', }, 't-in': { refX: 0.5, refX2: -10, refY: 0.5, textAnchor: 'end', }, 't-pin': { y: `${TEXT_Y_OFFSET - 0.6}em`, fontSize: 10, }, }, pins: { InName: { position: { x: 0, y: 0.5 }, }, }, preserveAspectRatio: false, }), pinLength: 20, }, ['model/CloudPSS/_BusConnector' as ModelId]: { primaryKey: 'InName', primaryName: 'InName', primaryDir: 'left', primaryDimX: 'InDimX', primaryDimY: 'InDimY', primaryConnection: 'electrical', secondaryDir: 'right', secondaryConnection: 'electrical', secondaryArgs: 'Out', graphic: () => ({ height: 100, width: 20, markup: [ { // 防止无法拖动 tagName: 'rect', selector: 'fallback', attrs: { stroke: 'none', fill: 'transparent', }, }, { tagName: 'path', selector: 'body', }, { tagName: 'path', selector: 'line', }, { tagName: 'text', selector: 't-in', groupSelector: 't-pin', textContent: '$InName', }, ], attrs: { // 防止无法拖动 fallback: { refX: 0.25, refWidth: 0.5, refHeight: 1, }, body: { strokeWidth: 'calc(2px*var(--stroke-width))', refPath: { d: 'M 0.5 0 v1', w: 1, h: 1 }, }, line: { stroke: 'var(--stroke)', strokeWidth: 'calc(1px*var(--stroke-width))', strokeOpacity: 'var(--stroke-opacity)', fill: 'transparent', refPath: { d: 'M 0 0.5 h 0.5', w: 1, h: 1 }, }, 't-in': { refX: 0.5, refX2: -10, refY: 0.5, textAnchor: 'end', }, 't-pin': { y: `${TEXT_Y_OFFSET - 0.6}em`, fontSize: 10, }, }, pins: { InName: { position: { x: 0, y: 0.5 }, }, }, preserveAspectRatio: false, }), pinLength: 10, }, }; /** * 是否为分离合并元件 */ export function isMergeDemergeModel(model: T | null | undefined): model is T { if (!model) return false; const rid: ModelId = typeof model == 'string' ? model : model.rid; if (!rid) return false; const def = MERGE_DEMERGE_COMPONENTS[rid]; if (!def) return false; return true; } /** 次要侧引脚 ID 属性名 */ export const SECONDARY_ARG_ID = 'ɵid'; /** 次要侧引脚参数 */ export type SecondaryArg = { /** 引脚 Key */ [SECONDARY_ARG_ID]: number; /** 引脚名称 */ 0: string; /** 引脚起始 X 位置 */ 1: number; /** 引脚起始 Y 位置 */ 2: number; /** 引脚 Dim X */ 3: number; /** 引脚 Dim Y */ 4: number; }; /** 求值结果 */ type EvalResult = { primaryName: string; primaryDimX: number; primaryDimY: number; secondaryArgs: SecondaryArg[]; }; /** 求值 */ function evaluate(def: MergeDemergeComponent, args: ArgumentMap, evaluator?: Evaluator, scope?: Scope): EvalResult { let primaryName = args[def.primaryName] as ExpressionOrValue | null | undefined; let primaryDimX = args[def.primaryDimX] as ExpressionOrValue | null | undefined; let primaryDimY = args[def.primaryDimY] as ExpressionOrValue | null | undefined; let secondaryArgs = args[def.secondaryArgs] as ExpressionOrValue | null | undefined; if (evaluator) { if (isExpression(primaryName)) { primaryName = evaluator.evaluate(primaryName, scope); } if (isExpression(primaryDimX)) { primaryDimX = evaluator.evaluate(primaryDimX, scope); } if (isExpression(primaryDimY)) { primaryDimY = evaluator.evaluate(primaryDimY, scope); } if (isExpression(secondaryArgs)) { secondaryArgs = evaluator.evaluate(secondaryArgs, scope); } } primaryName = primaryName ? String(primaryName) : def.primaryKey; primaryDimX = Number(primaryDimX) || 1; primaryDimY = Number(primaryDimY) || 1; secondaryArgs = Array.isArray(secondaryArgs) ? secondaryArgs : []; for (const el of secondaryArgs) { el[SECONDARY_ARG_ID] ??= generateTrackId(); } return { primaryName, primaryDimX, primaryDimY, secondaryArgs, }; } /** * 生成分离合并元件的定义 * @returns 仅当 `model` 是分离合并元件时返回更新后的模型,否则返回 `undefined` */ export function mergeDemergeModel( model: Model | null | undefined, args: ArgumentMap = {}, scope?: Scope | (() => Scope | undefined), evaluator?: Evaluator, ): Model | undefined { if (!isMergeDemergeModel(model)) return undefined; const { rid } = model; const def = MERGE_DEMERGE_COMPONENTS[rid]; if (!def) return undefined; const m: ReadonlyDeep = model; const result = { ...m, revision: { ...m.revision, pins: [] as typeof model.revision.pins, parameters: [...m.revision.parameters], }, }; const { pins } = result.revision; if (typeof scope == 'function') scope = scope(); const eArgs = evaluate(def, args, evaluator, scope); pins.push({ key: def.primaryKey, name: eArgs.primaryName || def.primaryKey, description: '', dim: [eArgs.primaryDimX, eArgs.primaryDimY], visible: true, data: 'real', connection: def.primaryConnection, }); const pinDef = eArgs.secondaryArgs; for (let index = 0; index < pinDef.length; index++) { const element = pinDef[index]!; const id = element[SECONDARY_ARG_ID]; pins.push({ key: `${MERGE_DEMERGE_PIN_PREFIX}${id}`, name: element[0] || (index + 1).toString(), description: '', dim: [element[3], element[4]], visible: true, data: 'real', connection: def.secondaryConnection, }); } const graphic = def.graphic(); result.revision.graphic = graphic; if (pinDef.length === 0) { // 显示 3 引脚所需高度 graphic.height = 20 * (3 + 1); } else { // 设置图形引脚部分 graphic.height = 20 * (pinDef.length + 1); // 从引脚右上角出发 let path = `M${def.secondaryDir === 'left' ? def.pinLength : graphic.width} 0`; const portPath = `m-${def.pinLength} 20 h${def.pinLength}`; let pos = 0; for (const pin of pinDef) { pos += 20; path += portPath; const id = `${MERGE_DEMERGE_PIN_PREFIX}${pin[SECONDARY_ARG_ID]}`; graphic.pins[id] = { position: { x: def.secondaryDir === 'left' ? 0 : 0.999_999_9, y: pos / graphic.height, }, }; if (def.secondaryArrow) { const arrowSelector = `a-pin-${id}`; graphic.markup.unshift({ tagName: 'path', selector: arrowSelector, groupSelector: 'p-arrow', }); graphic.attrs[arrowSelector] = { refX: (def.secondaryDir === 'left' ? def.pinLength : graphic.width - def.pinLength) / graphic.width, refY: pos / graphic.height, }; } if (pin[0]) { const text = pin[0]; const textSelector = `t-pin-${id}`; graphic.markup.push({ tagName: 'text', selector: textSelector, groupSelector: 't-pin', }); graphic.attrs[textSelector] = { refX: (def.secondaryDir === 'left' ? def.pinLength : graphic.width - def.pinLength) / graphic.width, refX2: def.secondaryDir === 'left' ? -10 : 10, refY: pos / graphic.height, textContent: text, textAnchor: def.secondaryDir === 'left' ? 'end' : 'start', }; } } graphic.markup.push({ tagName: 'path', selector: `p-dynamic`, attrs: { stroke: 'var(--stroke)', strokeWidth: 'calc(1px*var(--stroke-width))', strokeOpacity: 'var(--stroke-opacity)', }, }); graphic.attrs[`p-dynamic`] = { refPath: { d: path, w: graphic.width, h: graphic.height }, }; } return result as Model; }