统一组件图标布局并完善选择吸附交互
This commit is contained in:
1 parent
40c72422ff
commit
7671418582
16 files changed
+4215
-454
No files matched your search
@@ -139,6 +139,8 @@ class ReactFlowSimulationConfig(BaseModel):
|
|||||||
class ReactFlowProjectPayload(BaseModel):
|
class ReactFlowProjectPayload(BaseModel):
|
||||||
name: str = "untitled"
|
name: str = "untitled"
|
||||||
mediumReferenceVersion: Literal[1] | None = None
|
mediumReferenceVersion: Literal[1] | None = None
|
||||||
|
# 仅用于前端展示布局迁移;求解器和 System XML 不读取该字段。
|
||||||
|
presentationLayoutVersion: Literal[1] | None = None
|
||||||
# Missing/None is reserved for projects written before canonical AMESim
|
# Missing/None is reserved for projects written before canonical AMESim
|
||||||
# option codes were introduced. Every current writer emits version 1.
|
# option codes were introduced. Every current writer emits version 1.
|
||||||
amesimParameterEncodingVersion: Literal[1] | None = Field(
|
amesimParameterEncodingVersion: Literal[1] | None = Field(
|
||||||
|
|||||||
+1278
-140
File diff suppressed because it is too large.
Load diff
@@ -1,8 +1,16 @@
|
|||||||
import { AmesimPnpl01Symbol } from "./componentSymbols/boundary";
|
import { useLayoutEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
AmesimPnpl01Symbol,
|
||||||
|
PNPL01_PORT_ANCHOR,
|
||||||
|
} from "./componentSymbols/boundary";
|
||||||
import {
|
import {
|
||||||
AmesimF000Symbol,
|
AmesimF000Symbol,
|
||||||
|
F000_PORT_ANCHOR,
|
||||||
AmesimForcSymbol,
|
AmesimForcSymbol,
|
||||||
|
FORC_PORT_ANCHORS,
|
||||||
AmesimLmechn1Symbol,
|
AmesimLmechn1Symbol,
|
||||||
|
MECMAS21_PORT_ANCHORS,
|
||||||
AmesimPnrp17Symbol,
|
AmesimPnrp17Symbol,
|
||||||
AmesimLstp00aSymbol,
|
AmesimLstp00aSymbol,
|
||||||
AmesimMecmas21Symbol,
|
AmesimMecmas21Symbol,
|
||||||
@@ -21,14 +29,31 @@ import {
|
|||||||
AmesimPnor001Symbol,
|
AmesimPnor001Symbol,
|
||||||
AmesimPnvo001FixedSymbol,
|
AmesimPnvo001FixedSymbol,
|
||||||
AmesimPnvo001SignalSymbol,
|
AmesimPnvo001SignalSymbol,
|
||||||
|
PNVO001_PORT_ANCHORS,
|
||||||
} from "./componentSymbols/pneumatic";
|
} from "./componentSymbols/pneumatic";
|
||||||
import { SymbolSvg } from "./componentSymbols/primitives";
|
import {
|
||||||
import { AmesimStep0Symbol, AmesimUd00Symbol } from "./componentSymbols/signals";
|
SymbolSvg,
|
||||||
import { AmesimPnch012Symbol, AmesimPnch023Symbol } from "./componentSymbols/storage";
|
SymbolViewBoxProvider,
|
||||||
|
} from "./componentSymbols/primitives";
|
||||||
|
import {
|
||||||
|
AmesimStep0Symbol,
|
||||||
|
AmesimUd00Symbol,
|
||||||
|
SIGNAL_OUTPUT_PORT_ANCHOR,
|
||||||
|
} from "./componentSymbols/signals";
|
||||||
|
import {
|
||||||
|
AmesimPnch012Symbol,
|
||||||
|
AmesimPnch023Symbol,
|
||||||
|
PNCH012_PORT_ANCHORS,
|
||||||
|
PNCH023_PORT_ANCHORS,
|
||||||
|
} from "./componentSymbols/storage";
|
||||||
import type {
|
import type {
|
||||||
|
ComponentSymbolLayout,
|
||||||
ComponentSymbolParameters,
|
ComponentSymbolParameters,
|
||||||
ComponentSymbolPortAnchors,
|
ComponentSymbolPortAnchors,
|
||||||
ComponentSymbolPresentation,
|
ComponentSymbolPresentation,
|
||||||
|
ComponentSymbolSize,
|
||||||
|
ComponentSymbolSizeTier,
|
||||||
|
ComponentSymbolViewBox,
|
||||||
SymbolDefinition,
|
SymbolDefinition,
|
||||||
} from "./componentSymbols/types";
|
} from "./componentSymbols/types";
|
||||||
|
|
||||||
@@ -42,6 +67,125 @@ type ComponentSymbolProps = {
|
|||||||
parameters?: ComponentSymbolParameters;
|
parameters?: ComponentSymbolParameters;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DEFAULT_SYMBOL_VIEW_BOX: ComponentSymbolViewBox = {
|
||||||
|
x: 0,
|
||||||
|
// 既有图形使用 y=0..48;上下各保留 8 个逻辑单位,统一为 64×64 方形画布。
|
||||||
|
y: -8,
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
};
|
||||||
|
|
||||||
|
const SYMBOL_SIZE_BY_TIER: Record<
|
||||||
|
Exclude<ComponentSymbolSizeTier, "custom">,
|
||||||
|
ComponentSymbolSize
|
||||||
|
> = {
|
||||||
|
compact: { width: 80, height: 80 },
|
||||||
|
small: { width: 96, height: 96 },
|
||||||
|
standard: { width: 112, height: 112 },
|
||||||
|
large: { width: 128, height: 128 },
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 组件库按工作区尺寸档位表达元件的相对大小,工作区画布不使用这组比例。 */
|
||||||
|
const PALETTE_OCCUPANCY_BY_TIER: Record<ComponentSymbolSizeTier, number> = {
|
||||||
|
compact: 0.45,
|
||||||
|
small: 0.75,
|
||||||
|
standard: 0.9,
|
||||||
|
large: 0.9,
|
||||||
|
custom: 0.9,
|
||||||
|
};
|
||||||
|
|
||||||
|
function fittedPaletteViewBox(
|
||||||
|
bounds: Pick<DOMRect, "x" | "y" | "width" | "height">,
|
||||||
|
occupancy: number,
|
||||||
|
): ComponentSymbolViewBox | null {
|
||||||
|
const longestSide = Math.max(bounds.width, bounds.height);
|
||||||
|
if (!Number.isFinite(longestSide) || longestSide <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const side = longestSide / occupancy;
|
||||||
|
const centerX = bounds.x + bounds.width / 2;
|
||||||
|
const centerY = bounds.y + bounds.height / 2;
|
||||||
|
const round = (value: number) => Math.round(value * 10_000) / 10_000;
|
||||||
|
return {
|
||||||
|
x: round(centerX - side / 2),
|
||||||
|
y: round(centerY - side / 2),
|
||||||
|
width: round(side),
|
||||||
|
height: round(side),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function sameViewBox(
|
||||||
|
first: ComponentSymbolViewBox,
|
||||||
|
second: ComponentSymbolViewBox,
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
first.x === second.x &&
|
||||||
|
first.y === second.y &&
|
||||||
|
first.width === second.width &&
|
||||||
|
first.height === second.height
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const LEGACY_HORIZONTAL_PORT_ANCHORS: ComponentSymbolPortAnchors = {
|
||||||
|
left: [{ x: 2, y: 24 }],
|
||||||
|
right: [{ x: 62, y: 24 }],
|
||||||
|
};
|
||||||
|
const LEGACY_PNVO001_SIGNAL_PORT_ANCHORS: ComponentSymbolPortAnchors = {
|
||||||
|
left: [
|
||||||
|
{ x: 26, y: 46 },
|
||||||
|
{ x: 2, y: 24 },
|
||||||
|
],
|
||||||
|
right: [{ x: 62, y: 24 }],
|
||||||
|
};
|
||||||
|
|
||||||
|
type CustomSymbolLayout = {
|
||||||
|
viewBox?: ComponentSymbolViewBox;
|
||||||
|
symbolSize: ComponentSymbolSize;
|
||||||
|
nodeSize?: ComponentSymbolSize;
|
||||||
|
};
|
||||||
|
|
||||||
|
function makeSymbolLayout(
|
||||||
|
tier: Exclude<ComponentSymbolSizeTier, "custom">,
|
||||||
|
portAnchors?: ComponentSymbolPortAnchors,
|
||||||
|
): ComponentSymbolLayout;
|
||||||
|
function makeSymbolLayout(
|
||||||
|
tier: "custom",
|
||||||
|
portAnchors: ComponentSymbolPortAnchors | undefined,
|
||||||
|
custom: CustomSymbolLayout,
|
||||||
|
): ComponentSymbolLayout;
|
||||||
|
function makeSymbolLayout(
|
||||||
|
tier: ComponentSymbolSizeTier,
|
||||||
|
portAnchors?: ComponentSymbolPortAnchors,
|
||||||
|
custom?: CustomSymbolLayout,
|
||||||
|
): ComponentSymbolLayout {
|
||||||
|
const symbolSize =
|
||||||
|
tier === "custom"
|
||||||
|
? custom?.symbolSize ?? SYMBOL_SIZE_BY_TIER.large
|
||||||
|
: SYMBOL_SIZE_BY_TIER[tier];
|
||||||
|
return {
|
||||||
|
tier,
|
||||||
|
viewBox: custom?.viewBox ?? DEFAULT_SYMBOL_VIEW_BOX,
|
||||||
|
symbolSize,
|
||||||
|
// 两侧各保留 10px 给接口 Handle;无接口的自定义图标可显式收紧节点框。
|
||||||
|
nodeSize: custom?.nodeSize ?? {
|
||||||
|
width: symbolSize.width + 20,
|
||||||
|
height: symbolSize.height,
|
||||||
|
},
|
||||||
|
portAnchors,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 无接口元件不需要预留 Handle 空间,节点占地与对应档位的方形图标画布一致。 */
|
||||||
|
function makePortlessSymbolLayout(
|
||||||
|
tier: Exclude<ComponentSymbolSizeTier, "custom">,
|
||||||
|
): ComponentSymbolLayout {
|
||||||
|
const layout = makeSymbolLayout(tier);
|
||||||
|
return {
|
||||||
|
...layout,
|
||||||
|
nodeSize: layout.symbolSize,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function CylinderSymbol() {
|
function CylinderSymbol() {
|
||||||
return (
|
return (
|
||||||
<SymbolSvg>
|
<SymbolSvg>
|
||||||
@@ -164,93 +308,108 @@ function JunctionSymbol() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const symbolRegistry = {
|
const symbolRegistry = {
|
||||||
cylinder: { presentation: "framed", render: CylinderSymbol },
|
|
||||||
tank: { presentation: "framed", render: TankSymbol },
|
|
||||||
pipe: { presentation: "framed", render: PipeSymbol },
|
|
||||||
orifice: { presentation: "framed", render: OrificeSymbol },
|
|
||||||
tee: { presentation: "framed", render: TeeSymbol },
|
|
||||||
generic: { presentation: "framed", render: GenericSymbol },
|
|
||||||
signal: { presentation: "framed", render: SignalSymbol },
|
|
||||||
junction: { presentation: "framed", render: JunctionSymbol },
|
|
||||||
amesim_pnpl01: { presentation: "framed", render: AmesimPnpl01Symbol },
|
|
||||||
amesim_pnch023: { presentation: "framed", render: AmesimPnch023Symbol },
|
|
||||||
amesim_pnch012: { presentation: "framed", render: AmesimPnch012Symbol },
|
|
||||||
amesim_pnor001: { presentation: "framed", render: AmesimPnor001Symbol },
|
|
||||||
amesim_pnvo001_fixed: {
|
|
||||||
presentation: "framed",
|
|
||||||
render: AmesimPnvo001FixedSymbol,
|
|
||||||
},
|
|
||||||
amesim_pnvo001: {
|
|
||||||
presentation: "framed",
|
|
||||||
render: AmesimPnvo001SignalSymbol,
|
|
||||||
},
|
|
||||||
amesim_pnl00r: { presentation: "framed", render: AmesimPnl00rSymbol },
|
|
||||||
amesim_pnl0001: { presentation: "framed", render: AmesimPnl0001Symbol },
|
|
||||||
amesim_pnl0002: { presentation: "framed", render: AmesimPnl0002Symbol },
|
|
||||||
amesim_pnl0003: { presentation: "framed", render: AmesimPnl0003Symbol },
|
|
||||||
amesim_pn3node2: { presentation: "framed", render: AmesimPn3Node2Symbol },
|
|
||||||
amesim_p4node2: { presentation: "framed", render: AmesimP4Node2Symbol },
|
|
||||||
amesim_step0: { presentation: "framed", render: AmesimStep0Symbol },
|
|
||||||
amesim_ud00: { presentation: "framed", render: AmesimUd00Symbol },
|
|
||||||
amesim_f000: { presentation: "framed", render: AmesimF000Symbol },
|
|
||||||
amesim_forc: { presentation: "framed", render: AmesimForcSymbol },
|
|
||||||
amesim_mecmas21: { presentation: "framed", render: AmesimMecmas21Symbol },
|
|
||||||
amesim_lstp00a: { presentation: "framed", render: AmesimLstp00aSymbol },
|
|
||||||
amesim_lmechn1: { presentation: "framed", render: AmesimLmechn1Symbol },
|
|
||||||
amesim_pnrp17: { presentation: "framed", render: AmesimPnrp17Symbol },
|
|
||||||
amesim_ideal_air_medium: {
|
|
||||||
presentation: "bare",
|
|
||||||
render: AmesimIdealAirMediumSymbol,
|
|
||||||
},
|
|
||||||
amesim_helium_medium: {
|
|
||||||
presentation: "bare",
|
|
||||||
render: AmesimHeliumMediumSymbol,
|
|
||||||
},
|
|
||||||
} satisfies Record<string, SymbolDefinition>;
|
|
||||||
|
|
||||||
type RegisteredComponentSymbol = keyof typeof symbolRegistry;
|
|
||||||
|
|
||||||
const symbolPortAnchors: Partial<
|
|
||||||
Record<RegisteredComponentSymbol, ComponentSymbolPortAnchors>
|
|
||||||
> = {
|
|
||||||
cylinder: {
|
cylinder: {
|
||||||
|
presentation: "framed",
|
||||||
|
render: CylinderSymbol,
|
||||||
|
layout: makeSymbolLayout("small", {
|
||||||
left: [{ x: 3, y: 24 }],
|
left: [{ x: 3, y: 24 }],
|
||||||
right: [{ x: 61, y: 24 }],
|
right: [{ x: 61, y: 24 }],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
tank: {
|
||||||
|
presentation: "framed",
|
||||||
|
render: TankSymbol,
|
||||||
|
layout: makeSymbolLayout("small", { left: [{ x: 2, y: 24 }] }),
|
||||||
},
|
},
|
||||||
tank: { left: [{ x: 2, y: 24 }] },
|
|
||||||
pipe: {
|
pipe: {
|
||||||
|
presentation: "framed",
|
||||||
|
render: PipeSymbol,
|
||||||
|
layout: makeSymbolLayout("compact", {
|
||||||
left: [{ x: 2, y: 24 }],
|
left: [{ x: 2, y: 24 }],
|
||||||
right: [{ x: 62, y: 24 }],
|
right: [{ x: 62, y: 24 }],
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
orifice: {
|
orifice: {
|
||||||
|
presentation: "framed",
|
||||||
|
render: OrificeSymbol,
|
||||||
|
layout: makeSymbolLayout("compact", {
|
||||||
left: [{ x: 2, y: 24 }],
|
left: [{ x: 2, y: 24 }],
|
||||||
right: [{ x: 62, y: 24 }],
|
right: [{ x: 62, y: 24 }],
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
tee: {
|
tee: {
|
||||||
|
presentation: "framed",
|
||||||
|
render: TeeSymbol,
|
||||||
|
layout: makeSymbolLayout("compact", {
|
||||||
left: [{ x: 2, y: 24 }],
|
left: [{ x: 2, y: 24 }],
|
||||||
right: [
|
right: [
|
||||||
{ x: 62, y: 12 },
|
{ x: 62, y: 12 },
|
||||||
{ x: 62, y: 36 },
|
{ x: 62, y: 36 },
|
||||||
],
|
],
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
generic: {
|
generic: {
|
||||||
|
presentation: "framed",
|
||||||
|
render: GenericSymbol,
|
||||||
|
layout: makeSymbolLayout("small", {
|
||||||
left: [{ x: 2, y: 24 }],
|
left: [{ x: 2, y: 24 }],
|
||||||
right: [{ x: 62, y: 24 }],
|
right: [{ x: 62, y: 24 }],
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
signal: {
|
signal: {
|
||||||
|
presentation: "framed",
|
||||||
|
render: SignalSymbol,
|
||||||
|
layout: makeSymbolLayout("small", {
|
||||||
left: [{ x: 2, y: 24 }],
|
left: [{ x: 2, y: 24 }],
|
||||||
right: [{ x: 62, y: 24 }],
|
right: [{ x: 62, y: 24 }],
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
junction: {
|
junction: {
|
||||||
|
presentation: "framed",
|
||||||
|
render: JunctionSymbol,
|
||||||
|
layout: makeSymbolLayout("compact", {
|
||||||
|
left: [{ x: 2, y: 24 }],
|
||||||
|
right: [{ x: 62, y: 24 }],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
amesim_pnpl01: {
|
||||||
|
presentation: "framed",
|
||||||
|
render: AmesimPnpl01Symbol,
|
||||||
|
layout: {
|
||||||
|
...makeSymbolLayout("compact", { left: [PNPL01_PORT_ANCHOR] }),
|
||||||
|
legacyPortAnchors: { left: [{ x: 2, y: 24 }] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
amesim_pnch023: {
|
||||||
|
presentation: "framed",
|
||||||
|
render: AmesimPnch023Symbol,
|
||||||
|
layout: {
|
||||||
|
...makeSymbolLayout("small", {
|
||||||
|
left: [PNCH023_PORT_ANCHORS.left],
|
||||||
|
right: [PNCH023_PORT_ANCHORS.right],
|
||||||
|
}),
|
||||||
|
legacyPortAnchors: {
|
||||||
left: [{ x: 2, y: 24 }],
|
left: [{ x: 2, y: 24 }],
|
||||||
right: [{ x: 62, y: 24 }],
|
right: [{ x: 62, y: 24 }],
|
||||||
},
|
},
|
||||||
amesim_pnpl01: { left: [{ x: 2, y: 24 }] },
|
},
|
||||||
amesim_pnch023: {
|
|
||||||
left: [{ x: 2, y: 24 }],
|
|
||||||
right: [{ x: 62, y: 24 }],
|
|
||||||
},
|
},
|
||||||
amesim_pnch012: {
|
amesim_pnch012: {
|
||||||
|
presentation: "framed",
|
||||||
|
render: AmesimPnch012Symbol,
|
||||||
|
layout: {
|
||||||
|
...makeSymbolLayout("small", {
|
||||||
|
// 保留第一对左右接口,将第二对对应到参考图的上下接口。
|
||||||
|
left: [
|
||||||
|
PNCH012_PORT_ANCHORS.left,
|
||||||
|
PNCH012_PORT_ANCHORS.top,
|
||||||
|
],
|
||||||
|
right: [
|
||||||
|
PNCH012_PORT_ANCHORS.right,
|
||||||
|
PNCH012_PORT_ANCHORS.bottom,
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
legacyPortAnchors: {
|
||||||
left: [
|
left: [
|
||||||
{ x: 2, y: 16 },
|
{ x: 2, y: 16 },
|
||||||
{ x: 2, y: 32 },
|
{ x: 2, y: 32 },
|
||||||
@@ -260,79 +419,204 @@ const symbolPortAnchors: Partial<
|
|||||||
{ x: 62, y: 32 },
|
{ x: 62, y: 32 },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
amesim_pnor001: {
|
amesim_pnor001: {
|
||||||
left: [{ x: 2, y: 24 }],
|
presentation: "framed",
|
||||||
right: [{ x: 62, y: 24 }],
|
render: AmesimPnor001Symbol,
|
||||||
|
layout: {
|
||||||
|
...makeSymbolLayout("small", {
|
||||||
|
left: [PNVO001_PORT_ANCHORS.physicalLeft],
|
||||||
|
right: [PNVO001_PORT_ANCHORS.physicalRight],
|
||||||
|
}),
|
||||||
|
legacyPortAnchors: LEGACY_HORIZONTAL_PORT_ANCHORS,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
amesim_pnvo001_fixed: {
|
amesim_pnvo001_fixed: {
|
||||||
left: [{ x: 2, y: 24 }],
|
presentation: "framed",
|
||||||
right: [{ x: 62, y: 24 }],
|
render: AmesimPnvo001FixedSymbol,
|
||||||
|
layout: {
|
||||||
|
...makeSymbolLayout("small", {
|
||||||
|
left: [PNVO001_PORT_ANCHORS.physicalLeft],
|
||||||
|
right: [PNVO001_PORT_ANCHORS.physicalRight],
|
||||||
|
}),
|
||||||
|
legacyPortAnchors: LEGACY_HORIZONTAL_PORT_ANCHORS,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
amesim_pnvo001: {
|
amesim_pnvo001: {
|
||||||
|
presentation: "framed",
|
||||||
|
render: AmesimPnvo001SignalSymbol,
|
||||||
|
layout: {
|
||||||
|
...makeSymbolLayout("small", {
|
||||||
left: [
|
left: [
|
||||||
{ x: 26, y: 46 },
|
PNVO001_PORT_ANCHORS.signal,
|
||||||
{ x: 2, y: 24 },
|
PNVO001_PORT_ANCHORS.physicalLeft,
|
||||||
],
|
],
|
||||||
right: [{ x: 62, y: 24 }],
|
right: [PNVO001_PORT_ANCHORS.physicalRight],
|
||||||
|
}),
|
||||||
|
legacyPortAnchors: LEGACY_PNVO001_SIGNAL_PORT_ANCHORS,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
amesim_pnl00r: {
|
amesim_pnl00r: {
|
||||||
|
presentation: "framed",
|
||||||
|
render: AmesimPnl00rSymbol,
|
||||||
|
layout: makeSymbolLayout("compact", {
|
||||||
left: [{ x: 2, y: 24 }],
|
left: [{ x: 2, y: 24 }],
|
||||||
right: [{ x: 62, y: 24 }],
|
right: [{ x: 62, y: 24 }],
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
amesim_pnl0001: {
|
amesim_pnl0001: {
|
||||||
|
presentation: "framed",
|
||||||
|
render: AmesimPnl0001Symbol,
|
||||||
|
layout: makeSymbolLayout("small", {
|
||||||
left: [{ x: 2, y: 24 }],
|
left: [{ x: 2, y: 24 }],
|
||||||
right: [{ x: 62, y: 24 }],
|
right: [{ x: 62, y: 24 }],
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
amesim_pnl0002: {
|
amesim_pnl0002: {
|
||||||
|
presentation: "framed",
|
||||||
|
render: AmesimPnl0002Symbol,
|
||||||
|
layout: makeSymbolLayout("small", {
|
||||||
left: [{ x: 2, y: 24 }],
|
left: [{ x: 2, y: 24 }],
|
||||||
right: [{ x: 62, y: 24 }],
|
right: [{ x: 62, y: 24 }],
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
amesim_pnl0003: {
|
amesim_pnl0003: {
|
||||||
|
presentation: "framed",
|
||||||
|
render: AmesimPnl0003Symbol,
|
||||||
|
layout: makeSymbolLayout("small", {
|
||||||
left: [{ x: 2, y: 24 }],
|
left: [{ x: 2, y: 24 }],
|
||||||
right: [{ x: 62, y: 24 }],
|
right: [{ x: 62, y: 24 }],
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
amesim_pn3node2: {
|
amesim_pn3node2: {
|
||||||
|
presentation: "framed",
|
||||||
|
render: AmesimPn3Node2Symbol,
|
||||||
|
layout: makeSymbolLayout("compact", {
|
||||||
left: [{ x: 3, y: 24 }],
|
left: [{ x: 3, y: 24 }],
|
||||||
right: [
|
right: [
|
||||||
{ x: 61, y: 12 },
|
{ x: 61, y: 12 },
|
||||||
{ x: 61, y: 36 },
|
{ x: 61, y: 36 },
|
||||||
],
|
],
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
amesim_p4node2: {
|
amesim_p4node2: {
|
||||||
|
presentation: "framed",
|
||||||
|
render: AmesimP4Node2Symbol,
|
||||||
|
layout: makeSymbolLayout("small", {
|
||||||
left: [{ x: 3, y: 24 }],
|
left: [{ x: 3, y: 24 }],
|
||||||
right: [
|
right: [
|
||||||
{ x: 61, y: 8 },
|
{ x: 61, y: 8 },
|
||||||
{ x: 61, y: 24 },
|
{ x: 61, y: 24 },
|
||||||
{ x: 61, y: 40 },
|
{ x: 61, y: 40 },
|
||||||
],
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
amesim_step0: {
|
||||||
|
presentation: "framed",
|
||||||
|
render: AmesimStep0Symbol,
|
||||||
|
layout: {
|
||||||
|
...makeSymbolLayout("standard", { right: [SIGNAL_OUTPUT_PORT_ANCHOR] }),
|
||||||
|
legacyPortAnchors: { right: [{ x: 60, y: 24 }] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
amesim_ud00: {
|
||||||
|
presentation: "framed",
|
||||||
|
render: AmesimUd00Symbol,
|
||||||
|
layout: {
|
||||||
|
...makeSymbolLayout("standard", { right: [SIGNAL_OUTPUT_PORT_ANCHOR] }),
|
||||||
|
legacyPortAnchors: { right: [{ x: 60, y: 24 }] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
amesim_f000: {
|
||||||
|
presentation: "framed",
|
||||||
|
render: AmesimF000Symbol,
|
||||||
|
layout: {
|
||||||
|
...makeSymbolLayout("compact", { right: [F000_PORT_ANCHOR] }),
|
||||||
|
legacyPortAnchors: { right: [{ x: 62, y: 24 }] },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
amesim_step0: { right: [{ x: 60, y: 24 }] },
|
|
||||||
amesim_ud00: { right: [{ x: 60, y: 24 }] },
|
|
||||||
amesim_f000: { right: [{ x: 62, y: 24 }] },
|
|
||||||
amesim_forc: {
|
amesim_forc: {
|
||||||
left: [{ x: 2, y: 24 }],
|
presentation: "framed",
|
||||||
right: [{ x: 62, y: 24 }],
|
render: AmesimForcSymbol,
|
||||||
|
layout: {
|
||||||
|
...makeSymbolLayout("standard", {
|
||||||
|
left: [FORC_PORT_ANCHORS.left],
|
||||||
|
right: [FORC_PORT_ANCHORS.right],
|
||||||
|
}),
|
||||||
|
legacyPortAnchors: LEGACY_HORIZONTAL_PORT_ANCHORS,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
amesim_mecmas21: {
|
amesim_mecmas21: {
|
||||||
left: [{ x: 2, y: 24 }],
|
presentation: "framed",
|
||||||
right: [{ x: 62, y: 24 }],
|
render: AmesimMecmas21Symbol,
|
||||||
|
layout: {
|
||||||
|
...makeSymbolLayout("standard", {
|
||||||
|
left: [MECMAS21_PORT_ANCHORS.left],
|
||||||
|
right: [MECMAS21_PORT_ANCHORS.right],
|
||||||
|
}),
|
||||||
|
legacyPortAnchors: LEGACY_HORIZONTAL_PORT_ANCHORS,
|
||||||
|
},
|
||||||
|
variantKey: (parameters) =>
|
||||||
|
`${parameters?.useFriction ?? ""}:${parameters?.stoptype ?? ""}`,
|
||||||
},
|
},
|
||||||
amesim_lstp00a: {
|
amesim_lstp00a: {
|
||||||
|
presentation: "framed",
|
||||||
|
render: AmesimLstp00aSymbol,
|
||||||
|
layout: makeSymbolLayout("standard", {
|
||||||
left: [{ x: 2, y: 24 }],
|
left: [{ x: 2, y: 24 }],
|
||||||
right: [{ x: 62, y: 24 }],
|
right: [{ x: 62, y: 24 }],
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
amesim_lmechn1: {
|
amesim_lmechn1: {
|
||||||
|
presentation: "framed",
|
||||||
|
render: AmesimLmechn1Symbol,
|
||||||
|
layout: makeSymbolLayout(
|
||||||
|
"custom",
|
||||||
|
{
|
||||||
left: [7, 12, 17, 22, 27, 32, 37, 42].map((y) => ({ x: 5, y })),
|
left: [7, 12, 17, 22, 27, 32, 37, 42].map((y) => ({ x: 5, y })),
|
||||||
right: [{ x: 59, y: 24 }],
|
right: [{ x: 59, y: 24 }],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
symbolSize: { width: 128, height: 128 },
|
||||||
|
nodeSize: { width: 148, height: 128 },
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
amesim_pnrp17: {
|
amesim_pnrp17: {
|
||||||
|
presentation: "framed",
|
||||||
|
render: AmesimPnrp17Symbol,
|
||||||
|
// PNRP17 的当前 112×112 画布作为 standard 档位基准。
|
||||||
|
layout: makeSymbolLayout("standard", {
|
||||||
left: [
|
left: [
|
||||||
{ x: 25.5, y: 48 },
|
{ x: 25.5, y: 48 },
|
||||||
{ x: 2, y: 4.4 },
|
{ x: 2, y: 4.4 },
|
||||||
{ x: 2, y: 24.1 },
|
{ x: 2, y: 24.1 },
|
||||||
],
|
],
|
||||||
right: [{ x: 62, y: 4.4 }, { x: 62, y: 24.1 }],
|
right: [
|
||||||
|
{ x: 62, y: 4.4 },
|
||||||
|
{ x: 62, y: 24.1 },
|
||||||
|
],
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
|
amesim_ideal_air_medium: {
|
||||||
|
presentation: "bare",
|
||||||
|
render: AmesimIdealAirMediumSymbol,
|
||||||
|
layout: makePortlessSymbolLayout("standard"),
|
||||||
|
},
|
||||||
|
amesim_helium_medium: {
|
||||||
|
presentation: "bare",
|
||||||
|
render: AmesimHeliumMediumSymbol,
|
||||||
|
layout: makePortlessSymbolLayout("standard"),
|
||||||
|
},
|
||||||
|
} satisfies Record<string, SymbolDefinition>;
|
||||||
|
|
||||||
|
type RegisteredComponentSymbol = keyof typeof symbolRegistry;
|
||||||
|
|
||||||
|
const fallbackComponentSymbolLayout: ComponentSymbolLayout = {
|
||||||
|
...symbolRegistry.generic.layout,
|
||||||
|
// 未注册图标只显示通用边框和名称,接口应落在边框边缘而不是 SVG 锚点。
|
||||||
|
portAnchors: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
function isRegisteredComponentSymbol(symbol: string): symbol is RegisteredComponentSymbol {
|
function isRegisteredComponentSymbol(symbol: string): symbol is RegisteredComponentSymbol {
|
||||||
@@ -351,12 +635,27 @@ export function componentSymbolPresentation(
|
|||||||
: "framed";
|
: "framed";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function componentSymbolLayout(symbol: string): ComponentSymbolLayout {
|
||||||
|
return isRegisteredComponentSymbol(symbol)
|
||||||
|
? symbolRegistry[symbol].layout
|
||||||
|
: fallbackComponentSymbolLayout;
|
||||||
|
}
|
||||||
|
|
||||||
export function componentSymbolPortAnchors(
|
export function componentSymbolPortAnchors(
|
||||||
symbol: string,
|
symbol: string,
|
||||||
): ComponentSymbolPortAnchors | undefined {
|
): ComponentSymbolPortAnchors | undefined {
|
||||||
return isRegisteredComponentSymbol(symbol)
|
return componentSymbolLayout(symbol).portAnchors;
|
||||||
? symbolPortAnchors[symbol]
|
}
|
||||||
: undefined;
|
|
||||||
|
export function componentSymbolVariantKey(
|
||||||
|
symbol: string,
|
||||||
|
parameters?: ComponentSymbolParameters,
|
||||||
|
): string {
|
||||||
|
if (!isRegisteredComponentSymbol(symbol)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const definition: SymbolDefinition = symbolRegistry[symbol];
|
||||||
|
return definition.variantKey?.(parameters) ?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ComponentSymbol({
|
export function ComponentSymbol({
|
||||||
@@ -368,8 +667,16 @@ export function ComponentSymbol({
|
|||||||
const resolvedSymbol: RegisteredComponentSymbol = isRegisteredComponentSymbol(symbol)
|
const resolvedSymbol: RegisteredComponentSymbol = isRegisteredComponentSymbol(symbol)
|
||||||
? symbol
|
? symbol
|
||||||
: "generic";
|
: "generic";
|
||||||
const definition = symbolRegistry[resolvedSymbol];
|
const definition: SymbolDefinition = symbolRegistry[resolvedSymbol];
|
||||||
const SymbolRenderer = definition.render;
|
const SymbolRenderer = definition.render;
|
||||||
|
const { layout } = definition;
|
||||||
|
const rootRef = useRef<HTMLSpanElement>(null);
|
||||||
|
const [paletteViewBox, setPaletteViewBox] = useState<ComponentSymbolViewBox>(
|
||||||
|
layout.viewBox,
|
||||||
|
);
|
||||||
|
const paletteOccupancy = PALETTE_OCCUPANCY_BY_TIER[layout.tier];
|
||||||
|
const variantKey = definition.variantKey?.(parameters) ?? "";
|
||||||
|
const renderedViewBox = surface === "palette" ? paletteViewBox : layout.viewBox;
|
||||||
const rootClassName = [
|
const rootClassName = [
|
||||||
"component-symbol",
|
"component-symbol",
|
||||||
`component-symbol-${surface}`,
|
`component-symbol-${surface}`,
|
||||||
@@ -379,14 +686,59 @@ export function ComponentSymbol({
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(" ");
|
.join(" ");
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (surface !== "palette") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const svg = rootRef.current?.querySelector<SVGSVGElement>("svg");
|
||||||
|
if (!svg) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let bounds: DOMRect;
|
||||||
|
try {
|
||||||
|
bounds = svg.getBBox();
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const fittedViewBox = fittedPaletteViewBox(bounds, paletteOccupancy);
|
||||||
|
if (!fittedViewBox) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setPaletteViewBox((currentViewBox) =>
|
||||||
|
sameViewBox(currentViewBox, fittedViewBox)
|
||||||
|
? currentViewBox
|
||||||
|
: fittedViewBox,
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
layout.tier,
|
||||||
|
layout.viewBox.height,
|
||||||
|
layout.viewBox.width,
|
||||||
|
layout.viewBox.x,
|
||||||
|
layout.viewBox.y,
|
||||||
|
paletteOccupancy,
|
||||||
|
resolvedSymbol,
|
||||||
|
surface,
|
||||||
|
variantKey,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className={rootClassName}
|
className={rootClassName}
|
||||||
data-component-symbol={resolvedSymbol}
|
data-component-symbol={resolvedSymbol}
|
||||||
|
data-node-height={layout.nodeSize.height}
|
||||||
|
data-node-width={layout.nodeSize.width}
|
||||||
|
data-palette-occupancy={surface === "palette" ? paletteOccupancy : undefined}
|
||||||
|
data-symbol-height={layout.symbolSize.height}
|
||||||
data-symbol-presentation={definition.presentation}
|
data-symbol-presentation={definition.presentation}
|
||||||
|
data-symbol-size-tier={layout.tier}
|
||||||
|
data-symbol-width={layout.symbolSize.width}
|
||||||
|
data-symbol-view-box={`${renderedViewBox.x} ${renderedViewBox.y} ${renderedViewBox.width} ${renderedViewBox.height}`}
|
||||||
|
ref={rootRef}
|
||||||
>
|
>
|
||||||
|
<SymbolViewBoxProvider viewBox={renderedViewBox}>
|
||||||
<SymbolRenderer parameters={parameters} />
|
<SymbolRenderer parameters={parameters} />
|
||||||
|
</SymbolViewBoxProvider>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -132,6 +132,7 @@ export type SimulationResultsSnapshot = {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
project: {
|
project: {
|
||||||
name: string;
|
name: string;
|
||||||
|
presentationLayoutVersion?: 1 | null;
|
||||||
nodes: ResultProjectNode[];
|
nodes: ResultProjectNode[];
|
||||||
edges: ResultProjectEdge[];
|
edges: ResultProjectEdge[];
|
||||||
simulation: ResultSimulationConfig;
|
simulation: ResultSimulationConfig;
|
||||||
@@ -3910,6 +3911,9 @@ function isSimulationResultsSnapshotValue(
|
|||||||
if (
|
if (
|
||||||
!isRecord(project) ||
|
!isRecord(project) ||
|
||||||
typeof project.name !== "string" ||
|
typeof project.name !== "string" ||
|
||||||
|
(project.presentationLayoutVersion !== undefined &&
|
||||||
|
project.presentationLayoutVersion !== null &&
|
||||||
|
project.presentationLayoutVersion !== 1) ||
|
||||||
!Array.isArray(project.nodes) ||
|
!Array.isArray(project.nodes) ||
|
||||||
!project.nodes.every(isResultProjectNode) ||
|
!project.nodes.every(isResultProjectNode) ||
|
||||||
!Array.isArray(project.edges) ||
|
!Array.isArray(project.edges) ||
|
||||||
|
|||||||
@@ -1,15 +1,42 @@
|
|||||||
import { SYMBOL_ACCENT, SYMBOL_STROKE, SymbolSvg } from "./primitives";
|
import { SymbolSvg } from "./primitives";
|
||||||
|
|
||||||
|
const PNPL01_STROKE = "#8b134f";
|
||||||
|
const PNPL01_CONTENT_SCALE = 0.25;
|
||||||
|
const PNPL01_CONTENT_CENTER = { x: 32, y: 24 } as const;
|
||||||
|
const PNPL01_CONTENT_TRANSFORM = `translate(${PNPL01_CONTENT_CENTER.x} ${PNPL01_CONTENT_CENTER.y}) scale(${PNPL01_CONTENT_SCALE}) translate(${-PNPL01_CONTENT_CENTER.x} ${-PNPL01_CONTENT_CENTER.y})`;
|
||||||
|
const PNPL01_CROSS_CENTER = {
|
||||||
|
x:
|
||||||
|
PNPL01_CONTENT_CENTER.x +
|
||||||
|
(47 - PNPL01_CONTENT_CENTER.x) * PNPL01_CONTENT_SCALE,
|
||||||
|
y: PNPL01_CONTENT_CENTER.y,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const PNPL01_PORT_ANCHOR = {
|
||||||
|
x: 16,
|
||||||
|
y: PNPL01_CONTENT_CENTER.y,
|
||||||
|
} as const;
|
||||||
|
|
||||||
export function AmesimPnpl01Symbol() {
|
export function AmesimPnpl01Symbol() {
|
||||||
return (
|
return (
|
||||||
<SymbolSvg domain="pneumatic">
|
<SymbolSvg domain="pneumatic">
|
||||||
<path d="M2 24h34" stroke={SYMBOL_STROKE} strokeWidth="2" />
|
<g data-pnpl01-part="symbol">
|
||||||
<path
|
<path
|
||||||
d="m35 17 14 14m0-14L35 31"
|
d={`M${PNPL01_PORT_ANCHOR.x} ${PNPL01_PORT_ANCHOR.y}H${PNPL01_CROSS_CENTER.x}`}
|
||||||
stroke={SYMBOL_ACCENT}
|
data-pnpl01-part="extension"
|
||||||
strokeLinecap="round"
|
stroke={PNPL01_STROKE}
|
||||||
strokeWidth="3"
|
strokeLinecap="butt"
|
||||||
|
strokeWidth="1.5"
|
||||||
/>
|
/>
|
||||||
|
<g data-pnpl01-part="body" transform={PNPL01_CONTENT_TRANSFORM}>
|
||||||
|
<path
|
||||||
|
d="M30 7 64 41M64 7 30 41"
|
||||||
|
data-pnpl01-part="cross"
|
||||||
|
stroke={PNPL01_STROKE}
|
||||||
|
strokeLinecap="butt"
|
||||||
|
strokeWidth="4"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
</SymbolSvg>
|
</SymbolSvg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -12,9 +12,63 @@ const MECHANICAL_LINE_WIDTH = 1.8;
|
|||||||
const MECMAS21_STROKE = "#00af00";
|
const MECMAS21_STROKE = "#00af00";
|
||||||
const MECMAS21_HATCH = "#66cf66";
|
const MECMAS21_HATCH = "#66cf66";
|
||||||
const MECMAS21_FILL = "#ffffff";
|
const MECMAS21_FILL = "#ffffff";
|
||||||
|
const MECMAS21_CONTENT_SCALE = 0.72;
|
||||||
|
const MECMAS21_CONTENT_CENTER = { x: 32, y: 24 } as const;
|
||||||
|
const MECMAS21_CONTENT_TRANSFORM = `translate(${MECMAS21_CONTENT_CENTER.x} ${MECMAS21_CONTENT_CENTER.y}) scale(${MECMAS21_CONTENT_SCALE}) translate(${-MECMAS21_CONTENT_CENTER.x} ${-MECMAS21_CONTENT_CENTER.y})`;
|
||||||
|
const MECMAS21_SELECTION_ENVELOPE = {
|
||||||
|
x: 9.608,
|
||||||
|
y: 2.328,
|
||||||
|
width: 44.784,
|
||||||
|
height: 43.344,
|
||||||
|
} as const;
|
||||||
|
const F000_CONTENT_SCALE = 0.25;
|
||||||
|
const F000_CONTENT_CENTER = { x: 32, y: 24 } as const;
|
||||||
|
const F000_CONTENT_TRANSFORM = `translate(${F000_CONTENT_CENTER.x} ${F000_CONTENT_CENTER.y}) scale(${F000_CONTENT_SCALE}) translate(${-F000_CONTENT_CENTER.x} ${-F000_CONTENT_CENTER.y})`;
|
||||||
|
const F000_FRAME_RIGHT_X =
|
||||||
|
F000_CONTENT_CENTER.x +
|
||||||
|
(52 - F000_CONTENT_CENTER.x) * F000_CONTENT_SCALE;
|
||||||
|
export const F000_PORT_ANCHOR = {
|
||||||
|
x: 48,
|
||||||
|
y: 24,
|
||||||
|
} as const;
|
||||||
|
const FORC_STROKE = "#00af00";
|
||||||
|
const FORC_CONTENT_SCALE = 0.64;
|
||||||
|
const FORC_CONTENT_CENTER = { x: 32, y: 24 } as const;
|
||||||
|
const FORC_CONTENT_TRANSFORM = `translate(${FORC_CONTENT_CENTER.x} ${FORC_CONTENT_CENTER.y}) scale(${FORC_CONTENT_SCALE}) translate(${-FORC_CONTENT_CENTER.x} ${-FORC_CONTENT_CENTER.y})`;
|
||||||
|
export const FORC_PORT_ANCHORS = {
|
||||||
|
left: {
|
||||||
|
x:
|
||||||
|
FORC_CONTENT_CENTER.x +
|
||||||
|
(2 - FORC_CONTENT_CENTER.x) * FORC_CONTENT_SCALE,
|
||||||
|
y: FORC_CONTENT_CENTER.y,
|
||||||
|
},
|
||||||
|
right: {
|
||||||
|
x:
|
||||||
|
FORC_CONTENT_CENTER.x +
|
||||||
|
(62 - FORC_CONTENT_CENTER.x) * FORC_CONTENT_SCALE,
|
||||||
|
y: FORC_CONTENT_CENTER.y,
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
export const MECMAS21_PORT_ANCHORS = {
|
||||||
|
left: {
|
||||||
|
x:
|
||||||
|
MECMAS21_CONTENT_CENTER.x +
|
||||||
|
(2 - MECMAS21_CONTENT_CENTER.x) * MECMAS21_CONTENT_SCALE,
|
||||||
|
y: 24,
|
||||||
|
},
|
||||||
|
right: {
|
||||||
|
x:
|
||||||
|
MECMAS21_CONTENT_CENTER.x +
|
||||||
|
(62 - MECMAS21_CONTENT_CENTER.x) * MECMAS21_CONTENT_SCALE,
|
||||||
|
y: 24,
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
const PNRP17_STROKE = "#8b134f";
|
const PNRP17_STROKE = "#8b134f";
|
||||||
const PNRP17_FILL = "#cf9db6";
|
const PNRP17_FILL = "#cf9db6";
|
||||||
const PNRP17_LIGHT_FILL = "#f5ebf0";
|
const PNRP17_LIGHT_FILL = "#f5ebf0";
|
||||||
|
// PNRP17 使用最终像素线宽;几何缩放不再改变参考图中的粗细层级。
|
||||||
|
const PNRP17_PRIMARY_STROKE_WIDTH = 2;
|
||||||
|
const PNRP17_DETAIL_STROKE_WIDTH = 1.5;
|
||||||
|
|
||||||
const mechanicalStroke = {
|
const mechanicalStroke = {
|
||||||
fill: "none",
|
fill: "none",
|
||||||
@@ -34,16 +88,16 @@ function MechanicalSocket({ side }: { side: "left" | "right" }) {
|
|||||||
strokeLinejoin="miter"
|
strokeLinejoin="miter"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d={facingLeft ? "M2 24h3.2" : "M58.8 24H62"}
|
d={facingLeft ? "M2 24h4.2" : "M57.8 24H62"}
|
||||||
strokeWidth="0.9"
|
strokeWidth="0.9"
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d={
|
d={
|
||||||
facingLeft
|
facingLeft
|
||||||
? "M15.1 20.9H6.2v6.2h8.9"
|
? "M14 18.5H6.2v11H14"
|
||||||
: "M48.9 20.9h8.9v6.2h-8.9"
|
: "M50 18.5h7.8v11H50"
|
||||||
}
|
}
|
||||||
strokeWidth="2"
|
strokeWidth="2.5"
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
);
|
);
|
||||||
@@ -52,18 +106,28 @@ function MechanicalSocket({ side }: { side: "left" | "right" }) {
|
|||||||
export function AmesimF000Symbol() {
|
export function AmesimF000Symbol() {
|
||||||
return (
|
return (
|
||||||
<SymbolSvg className="component-symbol-amesim-f000" domain="mechanical">
|
<SymbolSvg className="component-symbol-amesim-f000" domain="mechanical">
|
||||||
<path d="M41 24h21" {...mechanicalStroke} />
|
<g data-f000-part="symbol">
|
||||||
<rect
|
<path
|
||||||
fill={SYMBOL_FILL}
|
d={`M${F000_FRAME_RIGHT_X} ${F000_PORT_ANCHOR.y}H${F000_PORT_ANCHOR.x}`}
|
||||||
height="28"
|
data-f000-part="extension"
|
||||||
rx="1"
|
|
||||||
stroke={SYMBOL_STROKE}
|
stroke={SYMBOL_STROKE}
|
||||||
strokeWidth="2.2"
|
strokeLinecap="butt"
|
||||||
width="18"
|
strokeWidth="1.5"
|
||||||
x="23"
|
|
||||||
y="12"
|
|
||||||
/>
|
/>
|
||||||
<path d="M27 26h10" stroke={SYMBOL_ACCENT} strokeWidth="2" />
|
<g data-f000-part="body" transform={F000_CONTENT_TRANSFORM}>
|
||||||
|
<rect
|
||||||
|
data-f000-part="frame"
|
||||||
|
fill="#ffffff"
|
||||||
|
height="34"
|
||||||
|
stroke={SYMBOL_STROKE}
|
||||||
|
strokeLinejoin="miter"
|
||||||
|
strokeWidth="2.5"
|
||||||
|
width="52"
|
||||||
|
x="0"
|
||||||
|
y="7"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
</SymbolSvg>
|
</SymbolSvg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -71,37 +135,64 @@ export function AmesimF000Symbol() {
|
|||||||
export function AmesimForcSymbol() {
|
export function AmesimForcSymbol() {
|
||||||
return (
|
return (
|
||||||
<SymbolSvg className="component-symbol-amesim-forc" domain="mechanical">
|
<SymbolSvg className="component-symbol-amesim-forc" domain="mechanical">
|
||||||
<g
|
<g data-forc-part="symbol" transform={FORC_CONTENT_TRANSFORM}>
|
||||||
|
<path
|
||||||
|
d="M2 24H18"
|
||||||
|
data-forc-part="left-extension"
|
||||||
|
stroke={FORC_STROKE}
|
||||||
|
strokeLinecap="butt"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M11.5 19.8 18 24 11.5 28.2"
|
||||||
|
data-forc-part="input-arrow"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke={SYMBOL_ACCENT}
|
stroke={FORC_STROKE}
|
||||||
strokeLinecap="round"
|
strokeLinecap="butt"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="miter"
|
||||||
strokeWidth="1.8"
|
strokeWidth="2"
|
||||||
>
|
/>
|
||||||
<path d="M2 24h15" />
|
<rect
|
||||||
<path d="m11 19 6 5-6 5" />
|
data-forc-part="mechanical-socket"
|
||||||
</g>
|
fill="#ffffff"
|
||||||
|
height="10"
|
||||||
|
stroke={FORC_STROKE}
|
||||||
|
strokeLinejoin="miter"
|
||||||
|
strokeWidth="2"
|
||||||
|
width="14"
|
||||||
|
x="39"
|
||||||
|
y="19"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M53 24H62"
|
||||||
|
data-forc-part="right-extension"
|
||||||
|
stroke={FORC_STROKE}
|
||||||
|
strokeLinecap="butt"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
/>
|
||||||
<circle
|
<circle
|
||||||
cx="32"
|
cx="30"
|
||||||
cy="24"
|
cy="24"
|
||||||
fill={SYMBOL_FILL}
|
data-forc-part="force-circle"
|
||||||
|
fill="#ffffff"
|
||||||
r="12"
|
r="12"
|
||||||
stroke={SYMBOL_STROKE}
|
stroke={FORC_STROKE}
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
/>
|
/>
|
||||||
<text
|
<text
|
||||||
|
data-forc-part="label"
|
||||||
dominantBaseline="central"
|
dominantBaseline="central"
|
||||||
fill={SYMBOL_STROKE}
|
fill={FORC_STROKE}
|
||||||
fontFamily="Arial, sans-serif"
|
fontFamily="Arial, sans-serif"
|
||||||
fontSize="13"
|
fontSize="15"
|
||||||
fontWeight="600"
|
fontWeight="600"
|
||||||
textAnchor="middle"
|
textAnchor="middle"
|
||||||
x="32"
|
x="30"
|
||||||
y="24"
|
y="24"
|
||||||
>
|
>
|
||||||
F
|
F
|
||||||
</text>
|
</text>
|
||||||
<path d="M44 24h6M50 18v12M50 18h8M50 30h8M58 18v12M58 24h4" {...mechanicalStroke} />
|
</g>
|
||||||
</SymbolSvg>
|
</SymbolSvg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -141,7 +232,7 @@ export function AmesimMecmas21Symbol({
|
|||||||
<path
|
<path
|
||||||
d="M-1 1 1-1M0 5.05 5.05 0M4.05 6.05 6.05 4.05"
|
d="M-1 1 1-1M0 5.05 5.05 0M4.05 6.05 6.05 4.05"
|
||||||
stroke={MECMAS21_HATCH}
|
stroke={MECMAS21_HATCH}
|
||||||
strokeWidth="1.55"
|
strokeWidth="1"
|
||||||
/>
|
/>
|
||||||
</pattern>
|
</pattern>
|
||||||
</defs>
|
</defs>
|
||||||
@@ -149,23 +240,24 @@ export function AmesimMecmas21Symbol({
|
|||||||
data-mecmas-friction={frictionEnabled ? "enabled" : "disabled"}
|
data-mecmas-friction={frictionEnabled ? "enabled" : "disabled"}
|
||||||
data-mecmas-limits={limitsEnabled ? "finite" : "unlimited"}
|
data-mecmas-limits={limitsEnabled ? "finite" : "unlimited"}
|
||||||
data-mecmas-variant={variant}
|
data-mecmas-variant={variant}
|
||||||
|
transform={MECMAS21_CONTENT_TRANSFORM}
|
||||||
>
|
>
|
||||||
{showGuides ? (
|
{showGuides ? (
|
||||||
<g data-mecmas-part="guides">
|
<g data-mecmas-part="guides">
|
||||||
<rect
|
<rect
|
||||||
data-mecmas-part="upper-guide"
|
data-mecmas-part="upper-guide"
|
||||||
fill={`url(#${hatchingId})`}
|
fill={`url(#${hatchingId})`}
|
||||||
height="5.25"
|
height="8.5"
|
||||||
stroke={MECMAS21_STROKE}
|
stroke={MECMAS21_STROKE}
|
||||||
strokeWidth="1.2"
|
strokeWidth="1.2"
|
||||||
width="60"
|
width="61"
|
||||||
x="2"
|
x="1.5"
|
||||||
y="2.6"
|
y="-5.5"
|
||||||
/>
|
/>
|
||||||
{limitsEnabled ? (
|
{limitsEnabled ? (
|
||||||
<g data-mecmas-part="finite-lower-guide">
|
<g data-mecmas-part="finite-lower-guide">
|
||||||
<path
|
<path
|
||||||
d="M2 32.6H10V39.6H54V32.6H62V45.4H2Z"
|
d="M1.5 34.5H9V45H55V34.5H62.5V53.5H1.5Z"
|
||||||
fill={`url(#${hatchingId})`}
|
fill={`url(#${hatchingId})`}
|
||||||
stroke={MECMAS21_STROKE}
|
stroke={MECMAS21_STROKE}
|
||||||
strokeLinejoin="miter"
|
strokeLinejoin="miter"
|
||||||
@@ -176,19 +268,19 @@ export function AmesimMecmas21Symbol({
|
|||||||
<rect
|
<rect
|
||||||
data-mecmas-part="unlimited-lower-guide"
|
data-mecmas-part="unlimited-lower-guide"
|
||||||
fill={`url(#${hatchingId})`}
|
fill={`url(#${hatchingId})`}
|
||||||
height="5.25"
|
height="8.5"
|
||||||
stroke={MECMAS21_STROKE}
|
stroke={MECMAS21_STROKE}
|
||||||
strokeWidth="1.2"
|
strokeWidth="1.2"
|
||||||
width="60"
|
width="61"
|
||||||
x="2"
|
x="1.5"
|
||||||
y="40.23"
|
y="45"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</g>
|
</g>
|
||||||
) : null}
|
) : null}
|
||||||
{frictionEnabled ? (
|
{frictionEnabled ? (
|
||||||
<path
|
<path
|
||||||
d="M16.49 9.93h4.9m3.81 0h4.9m3.93 0h4.9m3.79 0h4.9M16.49 38.07h4.9m3.81 0h4.9m3.93 0h4.9m3.79 0h4.9"
|
d="M16.5 5.25h4.9m3.8 0h4.9m3.9 0h4.9m3.8 0h4.9M16.5 42.75h4.9m3.8 0h4.9m3.9 0h4.9m3.8 0h4.9"
|
||||||
data-mecmas-part="friction-markers"
|
data-mecmas-part="friction-markers"
|
||||||
stroke={MECMAS21_STROKE}
|
stroke={MECMAS21_STROKE}
|
||||||
strokeLinecap="butt"
|
strokeLinecap="butt"
|
||||||
@@ -201,12 +293,12 @@ export function AmesimMecmas21Symbol({
|
|||||||
<rect
|
<rect
|
||||||
data-mecmas-part="mass"
|
data-mecmas-part="mass"
|
||||||
fill={MECMAS21_FILL}
|
fill={MECMAS21_FILL}
|
||||||
height="23.1"
|
height="34"
|
||||||
stroke={MECMAS21_STROKE}
|
stroke={MECMAS21_STROKE}
|
||||||
strokeWidth="2"
|
strokeWidth="2.5"
|
||||||
width="34"
|
width="36"
|
||||||
x="15"
|
x="14"
|
||||||
y="12.45"
|
y="7"
|
||||||
/>
|
/>
|
||||||
<text
|
<text
|
||||||
dominantBaseline="central"
|
dominantBaseline="central"
|
||||||
@@ -216,23 +308,33 @@ export function AmesimMecmas21Symbol({
|
|||||||
fontWeight="500"
|
fontWeight="500"
|
||||||
textAnchor="middle"
|
textAnchor="middle"
|
||||||
x="32"
|
x="32"
|
||||||
y="29.2"
|
y="31"
|
||||||
>
|
>
|
||||||
M
|
M
|
||||||
</text>
|
</text>
|
||||||
<path
|
<path
|
||||||
d="M18.94 18.8h5.88M21.9 16.6V21"
|
d="M18.8 16.8H25M21.9 13.8v6"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke={MECMAS21_STROKE}
|
stroke={MECMAS21_STROKE}
|
||||||
strokeLinecap="square"
|
strokeLinecap="square"
|
||||||
strokeWidth="1.05"
|
strokeWidth="1.05"
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d="M45.92 18.81 39.66 15.31 39.54 17.98H26.55v1.74h12.99l.12 2.93Z"
|
d="M46 16.8 39.5 12v3.6h-13V18h13v3.6Z"
|
||||||
fill={MECMAS21_STROKE}
|
fill={MECMAS21_STROKE}
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
|
{/* 固定选择包络:参数切换为仅中央滑块时,虚线框仍保持完整状态大小。 */}
|
||||||
|
<rect
|
||||||
|
data-mecmas-part="selection-envelope"
|
||||||
|
fill="transparent"
|
||||||
|
height={MECMAS21_SELECTION_ENVELOPE.height}
|
||||||
|
pointerEvents="none"
|
||||||
|
width={MECMAS21_SELECTION_ENVELOPE.width}
|
||||||
|
x={MECMAS21_SELECTION_ENVELOPE.x}
|
||||||
|
y={MECMAS21_SELECTION_ENVELOPE.y}
|
||||||
|
/>
|
||||||
</SymbolSvg>
|
</SymbolSvg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -322,27 +424,27 @@ export function AmesimPnrp17Symbol() {
|
|||||||
fill={PNRP17_FILL}
|
fill={PNRP17_FILL}
|
||||||
stroke={PNRP17_STROKE}
|
stroke={PNRP17_STROKE}
|
||||||
strokeLinejoin="miter"
|
strokeLinejoin="miter"
|
||||||
strokeWidth="1.2"
|
strokeWidth={PNRP17_PRIMARY_STROKE_WIDTH}
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d="M8.2 26.7h12.7v13.8h2v6.7H8.2Z"
|
d="M8.2 26.7h12.7v13.8h2v6.7H8.2Z"
|
||||||
fill={PNRP17_FILL}
|
fill={PNRP17_FILL}
|
||||||
stroke={PNRP17_STROKE}
|
stroke={PNRP17_STROKE}
|
||||||
strokeLinejoin="miter"
|
strokeLinejoin="miter"
|
||||||
strokeWidth="1.2"
|
strokeWidth={PNRP17_PRIMARY_STROKE_WIDTH}
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d="M28 40.5h28.5v6.7H28Z"
|
d="M28 40.5h28.5v6.7H28Z"
|
||||||
fill={PNRP17_FILL}
|
fill={PNRP17_FILL}
|
||||||
stroke={PNRP17_STROKE}
|
stroke={PNRP17_STROKE}
|
||||||
strokeLinejoin="miter"
|
strokeLinejoin="miter"
|
||||||
strokeWidth="1.2"
|
strokeWidth={PNRP17_PRIMARY_STROKE_WIDTH}
|
||||||
/>
|
/>
|
||||||
<rect
|
<rect
|
||||||
fill={PNRP17_LIGHT_FILL}
|
fill={PNRP17_LIGHT_FILL}
|
||||||
height="34"
|
height="34"
|
||||||
stroke={PNRP17_STROKE}
|
stroke={PNRP17_STROKE}
|
||||||
strokeWidth="1.2"
|
strokeWidth={PNRP17_PRIMARY_STROKE_WIDTH}
|
||||||
width="7.7"
|
width="7.7"
|
||||||
x="40.8"
|
x="40.8"
|
||||||
y="7.2"
|
y="7.2"
|
||||||
@@ -352,7 +454,7 @@ export function AmesimPnrp17Symbol() {
|
|||||||
fill={PNRP17_LIGHT_FILL}
|
fill={PNRP17_LIGHT_FILL}
|
||||||
height="2.8"
|
height="2.8"
|
||||||
stroke={PNRP17_STROKE}
|
stroke={PNRP17_STROKE}
|
||||||
strokeWidth="1.2"
|
strokeWidth={PNRP17_PRIMARY_STROKE_WIDTH}
|
||||||
width="31.5"
|
width="31.5"
|
||||||
x="9.3"
|
x="9.3"
|
||||||
y="22.7"
|
y="22.7"
|
||||||
@@ -361,7 +463,7 @@ export function AmesimPnrp17Symbol() {
|
|||||||
fill={PNRP17_LIGHT_FILL}
|
fill={PNRP17_LIGHT_FILL}
|
||||||
height="2.8"
|
height="2.8"
|
||||||
stroke={PNRP17_STROKE}
|
stroke={PNRP17_STROKE}
|
||||||
strokeWidth="1.2"
|
strokeWidth={PNRP17_PRIMARY_STROKE_WIDTH}
|
||||||
width="7.7"
|
width="7.7"
|
||||||
x="47.8"
|
x="47.8"
|
||||||
y="22.7"
|
y="22.7"
|
||||||
@@ -370,7 +472,7 @@ export function AmesimPnrp17Symbol() {
|
|||||||
d="M2 4.4h7.3m46.2 0H62M2 24.1h7.3m46.2 0H62M25.5 43.4V48"
|
d="M2 4.4h7.3m46.2 0H62M2 24.1h7.3m46.2 0H62M25.5 43.4V48"
|
||||||
stroke={PNRP17_STROKE}
|
stroke={PNRP17_STROKE}
|
||||||
strokeLinecap="butt"
|
strokeLinecap="butt"
|
||||||
strokeWidth="1.2"
|
strokeWidth={PNRP17_PRIMARY_STROKE_WIDTH}
|
||||||
/>
|
/>
|
||||||
{[15.3, 33].map((centerY) => (
|
{[15.3, 33].map((centerY) => (
|
||||||
<g
|
<g
|
||||||
@@ -379,7 +481,7 @@ export function AmesimPnrp17Symbol() {
|
|||||||
stroke={PNRP17_STROKE}
|
stroke={PNRP17_STROKE}
|
||||||
strokeLinecap="butt"
|
strokeLinecap="butt"
|
||||||
strokeLinejoin="miter"
|
strokeLinejoin="miter"
|
||||||
strokeWidth="1.1"
|
strokeWidth={PNRP17_DETAIL_STROKE_WIDTH}
|
||||||
>
|
>
|
||||||
<path d={`M28.8 ${centerY}h-3.4`} />
|
<path d={`M28.8 ${centerY}h-3.4`} />
|
||||||
<path
|
<path
|
||||||
|
|||||||
@@ -1,24 +1,31 @@
|
|||||||
import { SYMBOL_ACCENT, SYMBOL_FILL, SYMBOL_STROKE, SymbolSvg } from "./primitives";
|
import { SYMBOL_FILL, SymbolSvg } from "./primitives";
|
||||||
|
|
||||||
|
const MEDIUM_SYMBOL_COLOR = "#8b134f";
|
||||||
|
const MEDIUM_SYMBOL_RADIUS = 19.2;
|
||||||
|
const MEDIUM_SYMBOL_LABEL_FONT_SIZE = 13;
|
||||||
|
|
||||||
function MediumSymbol({ label }: { label: "Air" | "He" }) {
|
function MediumSymbol({ label }: { label: "Air" | "He" }) {
|
||||||
return (
|
return (
|
||||||
<SymbolSvg className="component-symbol-medium-svg" domain="medium">
|
<SymbolSvg
|
||||||
|
className="component-symbol-medium-svg"
|
||||||
|
domain="medium"
|
||||||
|
>
|
||||||
<circle
|
<circle
|
||||||
cx="32"
|
cx="32"
|
||||||
cy="24"
|
cy="24"
|
||||||
fill={SYMBOL_FILL}
|
fill={SYMBOL_FILL}
|
||||||
r="21"
|
r={MEDIUM_SYMBOL_RADIUS}
|
||||||
stroke={SYMBOL_STROKE}
|
stroke={MEDIUM_SYMBOL_COLOR}
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
/>
|
/>
|
||||||
<text
|
<text
|
||||||
className="component-symbol-medium-label"
|
className="component-symbol-medium-label"
|
||||||
data-symbol-label={label}
|
data-symbol-label={label}
|
||||||
dominantBaseline="central"
|
dominantBaseline="central"
|
||||||
fill={SYMBOL_ACCENT}
|
fill={MEDIUM_SYMBOL_COLOR}
|
||||||
fontFamily="Arial, sans-serif"
|
fontFamily="Arial, sans-serif"
|
||||||
fontSize={label === "Air" ? 12 : 16}
|
fontSize={MEDIUM_SYMBOL_LABEL_FONT_SIZE}
|
||||||
fontWeight="500"
|
fontWeight="600"
|
||||||
textAnchor="middle"
|
textAnchor="middle"
|
||||||
x="32"
|
x="32"
|
||||||
y="24"
|
y="24"
|
||||||
|
|||||||
@@ -8,88 +8,90 @@ import {
|
|||||||
|
|
||||||
const PNEUMATIC_STROKE_WIDTH = 1.7;
|
const PNEUMATIC_STROKE_WIDTH = 1.7;
|
||||||
const PNVO001_STROKE = "#8b134f";
|
const PNVO001_STROKE = "#8b134f";
|
||||||
|
const ORIFICE_CONTENT_SCALE = 0.6;
|
||||||
|
const ORIFICE_CONTENT_CENTER = { x: 32, y: 24 } as const;
|
||||||
|
const ORIFICE_CONTENT_TRANSFORM = `translate(${ORIFICE_CONTENT_CENTER.x} ${ORIFICE_CONTENT_CENTER.y}) scale(${ORIFICE_CONTENT_SCALE}) translate(${-ORIFICE_CONTENT_CENTER.x} ${-ORIFICE_CONTENT_CENTER.y})`;
|
||||||
|
|
||||||
function OrificeBody({
|
function scaledOrificePoint(x: number, y: number) {
|
||||||
centerY = 25,
|
return {
|
||||||
leftPortY = centerY,
|
x:
|
||||||
}: {
|
ORIFICE_CONTENT_CENTER.x +
|
||||||
centerY?: number;
|
(x - ORIFICE_CONTENT_CENTER.x) * ORIFICE_CONTENT_SCALE,
|
||||||
leftPortY?: number;
|
y:
|
||||||
}) {
|
ORIFICE_CONTENT_CENTER.y +
|
||||||
const top = centerY - 10;
|
(y - ORIFICE_CONTENT_CENTER.y) * ORIFICE_CONTENT_SCALE,
|
||||||
const bottom = centerY + 10;
|
};
|
||||||
const leftConnector =
|
}
|
||||||
leftPortY === centerY
|
|
||||||
? `M2 ${centerY}h18`
|
|
||||||
: `M2 ${leftPortY}h14L20 ${centerY}`;
|
|
||||||
|
|
||||||
|
export const PNVO001_PORT_ANCHORS = {
|
||||||
|
physicalLeft: scaledOrificePoint(0, 24),
|
||||||
|
physicalRight: scaledOrificePoint(64, 24),
|
||||||
|
signal: scaledOrificePoint(24.25, 56),
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
type OrificeControl = "none" | "fixed" | "signal";
|
||||||
|
|
||||||
|
function Pnvo001Body({ control }: { control: OrificeControl }) {
|
||||||
return (
|
return (
|
||||||
|
<g
|
||||||
|
data-orifice-control={control}
|
||||||
|
data-orifice-family="pnvo001"
|
||||||
|
transform={ORIFICE_CONTENT_TRANSFORM}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M0 24H64"
|
||||||
|
data-orifice-part="flow-path"
|
||||||
|
stroke={PNVO001_STROKE}
|
||||||
|
strokeLinecap="butt"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M14.4 11Q31.7 27.5 48.7 11M14.4 38Q31.7 18.2 48.7 38"
|
||||||
|
data-orifice-part="curves"
|
||||||
|
stroke={PNVO001_STROKE}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2.5"
|
||||||
|
/>
|
||||||
|
{control === "none" ? null : (
|
||||||
<>
|
<>
|
||||||
<path
|
<path
|
||||||
d={`${leftConnector}M44 ${centerY}h18M20 ${centerY}h24`}
|
d="M16.5 54.5 42 3.5"
|
||||||
stroke={SYMBOL_STROKE}
|
data-orifice-part="opening-arrow"
|
||||||
strokeLinecap="round"
|
stroke={PNVO001_STROKE}
|
||||||
strokeWidth={PNEUMATIC_STROKE_WIDTH}
|
strokeLinecap="butt"
|
||||||
|
strokeWidth="1.5"
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d={`M20 ${top}c5 6 9 7 12 7s7-1 12-7M20 ${bottom}c5-6 9-7 12-7s7 1 12 7`}
|
d="M48 -8 45.8 7.9 37 2.4Z"
|
||||||
stroke={SYMBOL_STROKE}
|
data-orifice-part="opening-arrow-head"
|
||||||
strokeLinecap="round"
|
fill={PNVO001_STROKE}
|
||||||
strokeWidth={PNEUMATIC_STROKE_WIDTH}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
)}
|
||||||
|
{control === "signal" ? (
|
||||||
|
<path
|
||||||
|
d="M24.25 56V39L32 54.5"
|
||||||
|
data-orifice-part="signal-control"
|
||||||
|
stroke={PNVO001_STROKE}
|
||||||
|
strokeLinecap="butt"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</g>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Constant-coefficient pneumatic orifice (AMESim PNOR001). */
|
/** 定系数气动孔口(AMESim PNOR001)。 */
|
||||||
export function AmesimPnor001Symbol() {
|
export function AmesimPnor001Symbol() {
|
||||||
return (
|
return (
|
||||||
<SymbolSvg className="component-symbol-amesim-pnor001-svg" domain="pneumatic">
|
<SymbolSvg className="component-symbol-amesim-pnor001-svg" domain="pneumatic">
|
||||||
<OrificeBody centerY={24} />
|
<Pnvo001Body control="none" />
|
||||||
</SymbolSvg>
|
</SymbolSvg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Pnvo001Body({
|
|
||||||
signalControlled = false,
|
|
||||||
}: {
|
|
||||||
signalControlled?: boolean;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<path
|
|
||||||
d="M0 24.5h64"
|
|
||||||
stroke={PNVO001_STROKE}
|
|
||||||
strokeLinecap="butt"
|
|
||||||
strokeWidth="2.1"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M15.5 14.4Q33.4 27 51.2 14.4M15.5 34.8Q33.2 22.4 51.2 34.8"
|
|
||||||
stroke={PNVO001_STROKE}
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth="3.1"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M19 46.7 44.5 10"
|
|
||||||
stroke={PNVO001_STROKE}
|
|
||||||
strokeLinecap="butt"
|
|
||||||
strokeWidth="2.6"
|
|
||||||
/>
|
|
||||||
{signalControlled ? (
|
|
||||||
<path
|
|
||||||
d="M26 48V36.6l7.2 10.1"
|
|
||||||
stroke={PNVO001_STROKE}
|
|
||||||
strokeLinecap="butt"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth="2.3"
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
<path d="M50.6 0 38.6 8.2 48 12.3Z" fill={PNVO001_STROKE} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** PNVO001 convenience variant whose opening is fixed by a parameter. */
|
/** PNVO001 convenience variant whose opening is fixed by a parameter. */
|
||||||
export function AmesimPnvo001FixedSymbol() {
|
export function AmesimPnvo001FixedSymbol() {
|
||||||
return (
|
return (
|
||||||
@@ -97,7 +99,7 @@ export function AmesimPnvo001FixedSymbol() {
|
|||||||
className="component-symbol-amesim-pnvo001-fixed-svg"
|
className="component-symbol-amesim-pnvo001-fixed-svg"
|
||||||
domain="pneumatic"
|
domain="pneumatic"
|
||||||
>
|
>
|
||||||
<Pnvo001Body />
|
<Pnvo001Body control="fixed" />
|
||||||
</SymbolSvg>
|
</SymbolSvg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -109,7 +111,7 @@ export function AmesimPnvo001SignalSymbol() {
|
|||||||
className="component-symbol-amesim-pnvo001-signal-svg"
|
className="component-symbol-amesim-pnvo001-signal-svg"
|
||||||
domain="pneumatic"
|
domain="pneumatic"
|
||||||
>
|
>
|
||||||
<Pnvo001Body signalControlled />
|
<Pnvo001Body control="signal" />
|
||||||
</SymbolSvg>
|
</SymbolSvg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import type { ReactNode } from "react";
|
import { createContext, useContext, type ReactNode } from "react";
|
||||||
|
|
||||||
|
import type { ComponentSymbolViewBox } from "./types";
|
||||||
|
|
||||||
export type SymbolDomain =
|
export type SymbolDomain =
|
||||||
| "neutral"
|
| "neutral"
|
||||||
@@ -11,25 +13,48 @@ type SymbolSvgProps = {
|
|||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
domain?: SymbolDomain;
|
domain?: SymbolDomain;
|
||||||
viewBox?: string;
|
strokeScaling?: "independent" | "geometry";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type SymbolViewBoxProviderProps = {
|
||||||
|
children: ReactNode;
|
||||||
|
viewBox: ComponentSymbolViewBox;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SymbolViewBoxContext = createContext("0 -8 64 64");
|
||||||
|
|
||||||
export const SYMBOL_STROKE = "var(--component-symbol-stroke, currentColor)";
|
export const SYMBOL_STROKE = "var(--component-symbol-stroke, currentColor)";
|
||||||
export const SYMBOL_FILL = "var(--component-symbol-fill, #ffffff)";
|
export const SYMBOL_FILL = "var(--component-symbol-fill, #ffffff)";
|
||||||
export const SYMBOL_ACCENT = "var(--component-symbol-accent, currentColor)";
|
export const SYMBOL_ACCENT = "var(--component-symbol-accent, currentColor)";
|
||||||
export const SYMBOL_MUTED = "var(--component-symbol-muted, #64748b)";
|
export const SYMBOL_MUTED = "var(--component-symbol-muted, #64748b)";
|
||||||
|
|
||||||
|
/** 由图标注册表统一提供 SVG 画布,避免渲染器与接口布局分别维护 viewBox。 */
|
||||||
|
export function SymbolViewBoxProvider({
|
||||||
|
children,
|
||||||
|
viewBox,
|
||||||
|
}: SymbolViewBoxProviderProps) {
|
||||||
|
return (
|
||||||
|
<SymbolViewBoxContext.Provider
|
||||||
|
value={`${viewBox.x} ${viewBox.y} ${viewBox.width} ${viewBox.height}`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SymbolViewBoxContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function SymbolSvg({
|
export function SymbolSvg({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
domain = "neutral",
|
domain = "neutral",
|
||||||
viewBox = "0 0 64 48",
|
strokeScaling = "independent",
|
||||||
}: SymbolSvgProps) {
|
}: SymbolSvgProps) {
|
||||||
|
const viewBox = useContext(SymbolViewBoxContext);
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className={["component-symbol-svg", className].filter(Boolean).join(" ")}
|
className={["component-symbol-svg", className].filter(Boolean).join(" ")}
|
||||||
data-symbol-domain={domain}
|
data-symbol-domain={domain}
|
||||||
|
data-symbol-stroke-scaling={strokeScaling}
|
||||||
fill="none"
|
fill="none"
|
||||||
focusable="false"
|
focusable="false"
|
||||||
viewBox={viewBox}
|
viewBox={viewBox}
|
||||||
|
|||||||
@@ -6,6 +6,31 @@ import {
|
|||||||
} from "./primitives";
|
} from "./primitives";
|
||||||
|
|
||||||
const SIGNAL_LINE_WIDTH = 1.8;
|
const SIGNAL_LINE_WIDTH = 1.8;
|
||||||
|
const SIGNAL_CANVAS_SIZE = 64;
|
||||||
|
const SIGNAL_WORKSPACE_OCCUPANCY = 0.6;
|
||||||
|
const SIGNAL_SOURCE_BOUNDS = {
|
||||||
|
left: 9,
|
||||||
|
right: 60,
|
||||||
|
top: 6,
|
||||||
|
bottom: 42,
|
||||||
|
} as const;
|
||||||
|
const SIGNAL_SOURCE_CENTER = {
|
||||||
|
x: (SIGNAL_SOURCE_BOUNDS.left + SIGNAL_SOURCE_BOUNDS.right) / 2,
|
||||||
|
y: (SIGNAL_SOURCE_BOUNDS.top + SIGNAL_SOURCE_BOUNDS.bottom) / 2,
|
||||||
|
} as const;
|
||||||
|
const SIGNAL_CANVAS_CENTER = { x: 32, y: 24 } as const;
|
||||||
|
const SIGNAL_CONTENT_SCALE =
|
||||||
|
(SIGNAL_CANVAS_SIZE * SIGNAL_WORKSPACE_OCCUPANCY) /
|
||||||
|
(SIGNAL_SOURCE_BOUNDS.right - SIGNAL_SOURCE_BOUNDS.left);
|
||||||
|
const SIGNAL_CONTENT_TRANSFORM = `translate(${SIGNAL_CANVAS_CENTER.x} ${SIGNAL_CANVAS_CENTER.y}) scale(${SIGNAL_CONTENT_SCALE}) translate(${-SIGNAL_SOURCE_CENTER.x} ${-SIGNAL_SOURCE_CENTER.y})`;
|
||||||
|
|
||||||
|
export const SIGNAL_OUTPUT_PORT_ANCHOR = {
|
||||||
|
x:
|
||||||
|
SIGNAL_CANVAS_CENTER.x +
|
||||||
|
(SIGNAL_SOURCE_BOUNDS.right - SIGNAL_SOURCE_CENTER.x) *
|
||||||
|
SIGNAL_CONTENT_SCALE,
|
||||||
|
y: SIGNAL_CANVAS_CENTER.y,
|
||||||
|
} as const;
|
||||||
|
|
||||||
function SignalOutput() {
|
function SignalOutput() {
|
||||||
return (
|
return (
|
||||||
@@ -25,6 +50,7 @@ function SignalOutput() {
|
|||||||
export function AmesimStep0Symbol() {
|
export function AmesimStep0Symbol() {
|
||||||
return (
|
return (
|
||||||
<SymbolSvg className="component-symbol-amesim-step0" domain="signal">
|
<SymbolSvg className="component-symbol-amesim-step0" domain="signal">
|
||||||
|
<g data-signal-source="step0" transform={SIGNAL_CONTENT_TRANSFORM}>
|
||||||
<circle
|
<circle
|
||||||
cx="27"
|
cx="27"
|
||||||
cy="24"
|
cy="24"
|
||||||
@@ -42,6 +68,7 @@ export function AmesimStep0Symbol() {
|
|||||||
strokeWidth="2.2"
|
strokeWidth="2.2"
|
||||||
/>
|
/>
|
||||||
<SignalOutput />
|
<SignalOutput />
|
||||||
|
</g>
|
||||||
</SymbolSvg>
|
</SymbolSvg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -49,6 +76,7 @@ export function AmesimStep0Symbol() {
|
|||||||
export function AmesimUd00Symbol() {
|
export function AmesimUd00Symbol() {
|
||||||
return (
|
return (
|
||||||
<SymbolSvg className="component-symbol-amesim-ud00" domain="signal">
|
<SymbolSvg className="component-symbol-amesim-ud00" domain="signal">
|
||||||
|
<g data-signal-source="ud00" transform={SIGNAL_CONTENT_TRANSFORM}>
|
||||||
<circle
|
<circle
|
||||||
cx="27"
|
cx="27"
|
||||||
cy="24"
|
cy="24"
|
||||||
@@ -66,6 +94,7 @@ export function AmesimUd00Symbol() {
|
|||||||
strokeWidth="2.2"
|
strokeWidth="2.2"
|
||||||
/>
|
/>
|
||||||
<SignalOutput />
|
<SignalOutput />
|
||||||
|
</g>
|
||||||
</SymbolSvg>
|
</SymbolSvg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,56 +1,76 @@
|
|||||||
import {
|
import {
|
||||||
SYMBOL_ACCENT,
|
|
||||||
SYMBOL_FILL,
|
SYMBOL_FILL,
|
||||||
SYMBOL_STROKE,
|
SYMBOL_STROKE,
|
||||||
SymbolSvg,
|
SymbolSvg,
|
||||||
} from "./primitives";
|
} from "./primitives";
|
||||||
|
|
||||||
function ChamberCore({ variable }: { variable: boolean }) {
|
const PNCH012_STROKE = "#8b134f";
|
||||||
|
const PNCH_CHAMBER_CONTENT_SCALE = 0.45;
|
||||||
|
const PNCH_CHAMBER_CONTENT_CENTER = { x: 32, y: 24 } as const;
|
||||||
|
const PNCH_CHAMBER_CONTENT_TRANSFORM = `translate(${PNCH_CHAMBER_CONTENT_CENTER.x} ${PNCH_CHAMBER_CONTENT_CENTER.y}) scale(${PNCH_CHAMBER_CONTENT_SCALE}) translate(${-PNCH_CHAMBER_CONTENT_CENTER.x} ${-PNCH_CHAMBER_CONTENT_CENTER.y})`;
|
||||||
|
const PNCH_CHAMBER_LABEL_RENDERED_SIZE = 15;
|
||||||
|
const PNCH_CHAMBER_SMALL_CANVAS_SCALE = 96 / 64;
|
||||||
|
// SVG 字号使用 viewBox 逻辑单位;抵消 small 画布缩放后,工作区最终显示为 15px。
|
||||||
|
const PNCH_CHAMBER_LABEL_FONT_SIZE =
|
||||||
|
PNCH_CHAMBER_LABEL_RENDERED_SIZE / PNCH_CHAMBER_SMALL_CANVAS_SCALE;
|
||||||
|
|
||||||
|
function scalePnchChamberAnchor(x: number, y: number) {
|
||||||
|
return {
|
||||||
|
x:
|
||||||
|
PNCH_CHAMBER_CONTENT_CENTER.x +
|
||||||
|
(x - PNCH_CHAMBER_CONTENT_CENTER.x) * PNCH_CHAMBER_CONTENT_SCALE,
|
||||||
|
y:
|
||||||
|
PNCH_CHAMBER_CONTENT_CENTER.y +
|
||||||
|
(y - PNCH_CHAMBER_CONTENT_CENTER.y) * PNCH_CHAMBER_CONTENT_SCALE,
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PNCH012_PORT_ANCHORS = {
|
||||||
|
bottom: scalePnchChamberAnchor(32, 56),
|
||||||
|
left: scalePnchChamberAnchor(0, 24),
|
||||||
|
right: scalePnchChamberAnchor(64, 24),
|
||||||
|
top: scalePnchChamberAnchor(32, -8),
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const PNCH023_PORT_ANCHORS = {
|
||||||
|
left: PNCH012_PORT_ANCHORS.left,
|
||||||
|
right: PNCH012_PORT_ANCHORS.right,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export function AmesimPnch023Symbol() {
|
||||||
return (
|
return (
|
||||||
<>
|
<SymbolSvg domain="pneumatic">
|
||||||
|
<g data-pnch023-part="symbol" transform={PNCH_CHAMBER_CONTENT_TRANSFORM}>
|
||||||
|
<path
|
||||||
|
d="M0 24H11M53 24H64"
|
||||||
|
data-pnch023-part="ports"
|
||||||
|
stroke={SYMBOL_STROKE}
|
||||||
|
strokeLinecap="butt"
|
||||||
|
strokeWidth="2"
|
||||||
|
/>
|
||||||
<circle
|
<circle
|
||||||
cx="32"
|
cx="32"
|
||||||
cy="24"
|
cy="24"
|
||||||
|
data-pnch023-part="chamber"
|
||||||
fill={SYMBOL_FILL}
|
fill={SYMBOL_FILL}
|
||||||
r="13"
|
r="21"
|
||||||
stroke={SYMBOL_STROKE}
|
stroke={SYMBOL_STROKE}
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
/>
|
/>
|
||||||
|
</g>
|
||||||
<text
|
<text
|
||||||
|
data-pnch023-part="label"
|
||||||
dominantBaseline="central"
|
dominantBaseline="central"
|
||||||
fill={SYMBOL_ACCENT}
|
fill={PNCH012_STROKE}
|
||||||
fontFamily="Arial, sans-serif"
|
fontFamily="Arial, sans-serif"
|
||||||
fontSize="13"
|
fontSize={PNCH_CHAMBER_LABEL_FONT_SIZE}
|
||||||
fontWeight="700"
|
fontWeight="600"
|
||||||
textAnchor="middle"
|
textAnchor="middle"
|
||||||
x="32"
|
x="32"
|
||||||
y="24"
|
y="24"
|
||||||
>
|
>
|
||||||
Cp
|
Cp
|
||||||
</text>
|
</text>
|
||||||
{variable ? (
|
|
||||||
<g
|
|
||||||
fill="none"
|
|
||||||
stroke={SYMBOL_ACCENT}
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth="1.5"
|
|
||||||
>
|
|
||||||
<path d="m20 13-5-5m0 0h4m-4 0v4" />
|
|
||||||
<path d="m44 13 5-5m0 0h-4m4 0v4" />
|
|
||||||
<path d="m20 35-5 5m0 0h4m-4 0v-4" />
|
|
||||||
<path d="m44 35 5 5m0 0h-4m4 0v-4" />
|
|
||||||
</g>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AmesimPnch023Symbol() {
|
|
||||||
return (
|
|
||||||
<SymbolSvg domain="pneumatic">
|
|
||||||
<path d="M2 24h17M45 24h17" stroke={SYMBOL_STROKE} strokeWidth="2" />
|
|
||||||
<ChamberCore variable={false} />
|
|
||||||
</SymbolSvg>
|
</SymbolSvg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -58,12 +78,54 @@ export function AmesimPnch023Symbol() {
|
|||||||
export function AmesimPnch012Symbol() {
|
export function AmesimPnch012Symbol() {
|
||||||
return (
|
return (
|
||||||
<SymbolSvg domain="pneumatic">
|
<SymbolSvg domain="pneumatic">
|
||||||
|
<g data-pnch012-part="symbol" transform={PNCH_CHAMBER_CONTENT_TRANSFORM}>
|
||||||
<path
|
<path
|
||||||
d="M2 16h17M2 32h17M45 16h17M45 32h17"
|
d="M0 24H11M53 24H64M32-8V3M32 45V56"
|
||||||
stroke={SYMBOL_STROKE}
|
data-pnch012-part="ports"
|
||||||
strokeWidth="2"
|
stroke={PNCH012_STROKE}
|
||||||
|
strokeLinecap="butt"
|
||||||
|
strokeWidth="1.5"
|
||||||
/>
|
/>
|
||||||
<ChamberCore variable />
|
<circle
|
||||||
|
cx="32"
|
||||||
|
cy="24"
|
||||||
|
data-pnch012-part="chamber"
|
||||||
|
fill={SYMBOL_FILL}
|
||||||
|
r="21"
|
||||||
|
stroke={PNCH012_STROKE}
|
||||||
|
strokeWidth="1.5"
|
||||||
|
/>
|
||||||
|
<g
|
||||||
|
data-pnch012-part="volume-arrows"
|
||||||
|
fill="none"
|
||||||
|
stroke={PNCH012_STROKE}
|
||||||
|
strokeLinecap="butt"
|
||||||
|
strokeLinejoin="miter"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
>
|
||||||
|
{[0, 90, 180, 270].map((rotation) => (
|
||||||
|
<path
|
||||||
|
d="M4-3 10 3M3 9H16V-4L3 9Z"
|
||||||
|
data-pnch012-part="volume-arrow"
|
||||||
|
key={rotation}
|
||||||
|
transform={`rotate(${rotation} 32 24)`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<text
|
||||||
|
data-pnch012-part="label"
|
||||||
|
dominantBaseline="central"
|
||||||
|
fill={PNCH012_STROKE}
|
||||||
|
fontFamily="Arial, sans-serif"
|
||||||
|
fontSize={PNCH_CHAMBER_LABEL_FONT_SIZE}
|
||||||
|
fontWeight="600"
|
||||||
|
textAnchor="middle"
|
||||||
|
x="32"
|
||||||
|
y="24"
|
||||||
|
>
|
||||||
|
Cp
|
||||||
|
</text>
|
||||||
</SymbolSvg>
|
</SymbolSvg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,41 @@ export type ComponentSymbolPortAnchors = Partial<
|
|||||||
Record<"left" | "right", readonly ComponentSymbolPortAnchor[]>
|
Record<"left" | "right", readonly ComponentSymbolPortAnchor[]>
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export type ComponentSymbolSizeTier =
|
||||||
|
| "compact"
|
||||||
|
| "small"
|
||||||
|
| "standard"
|
||||||
|
| "large"
|
||||||
|
| "custom";
|
||||||
|
|
||||||
|
export type ComponentSymbolViewBox = {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ComponentSymbolSize = {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 元件在建模区中的唯一布局来源。
|
||||||
|
*
|
||||||
|
* viewBox 描述 SVG 逻辑坐标,symbolSize 描述图标显示槽,nodeSize 描述
|
||||||
|
* React Flow 实际占地。接口锚点与三者放在一起,避免调整图标后出现接口漂移。
|
||||||
|
*/
|
||||||
|
export type ComponentSymbolLayout = {
|
||||||
|
tier: ComponentSymbolSizeTier;
|
||||||
|
viewBox: ComponentSymbolViewBox;
|
||||||
|
symbolSize: ComponentSymbolSize;
|
||||||
|
nodeSize: ComponentSymbolSize;
|
||||||
|
portAnchors?: ComponentSymbolPortAnchors;
|
||||||
|
/** 仅用于识别 presentationLayoutVersion 之前保存的固定尺寸接触连接。 */
|
||||||
|
legacyPortAnchors?: ComponentSymbolPortAnchors;
|
||||||
|
};
|
||||||
|
|
||||||
export type ComponentSymbolParameters = Readonly<
|
export type ComponentSymbolParameters = Readonly<
|
||||||
Record<string, number | string>
|
Record<string, number | string>
|
||||||
>;
|
>;
|
||||||
@@ -22,4 +57,6 @@ export type ComponentSymbolRendererProps = {
|
|||||||
export type SymbolDefinition = {
|
export type SymbolDefinition = {
|
||||||
presentation: ComponentSymbolPresentation;
|
presentation: ComponentSymbolPresentation;
|
||||||
render: ComponentType<ComponentSymbolRendererProps>;
|
render: ComponentType<ComponentSymbolRendererProps>;
|
||||||
|
layout: ComponentSymbolLayout;
|
||||||
|
variantKey?: (parameters?: ComponentSymbolParameters) => string;
|
||||||
};
|
};
|
||||||
+21
-37
@@ -1203,6 +1203,15 @@ textarea {
|
|||||||
shape-rendering: geometricPrecision;
|
shape-rendering: geometricPrecision;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 图形占比由 viewBox 或内部 transform 控制;描边宽度始终按声明值渲染。
|
||||||
|
* 这样调整元件几何缩放时,strokeWidth 不会被连带放大或缩小。
|
||||||
|
*/
|
||||||
|
.component-symbol-svg[data-symbol-stroke-scaling="independent"]
|
||||||
|
:where(path, line, polyline, polygon, rect, circle, ellipse, use, text) {
|
||||||
|
vector-effect: non-scaling-stroke;
|
||||||
|
}
|
||||||
|
|
||||||
.component-symbol-svg[data-symbol-domain="neutral"] {
|
.component-symbol-svg[data-symbol-domain="neutral"] {
|
||||||
--component-symbol-stroke: #334155;
|
--component-symbol-stroke: #334155;
|
||||||
--component-symbol-fill: #f8fafc;
|
--component-symbol-fill: #f8fafc;
|
||||||
@@ -1331,36 +1340,16 @@ textarea {
|
|||||||
|
|
||||||
.component-drag-preview {
|
.component-drag-preview {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: -1000px;
|
top: -10000px;
|
||||||
left: -1000px;
|
left: -10000px;
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: 30px 12px;
|
|
||||||
place-items: center;
|
place-items: center;
|
||||||
width: 72px;
|
|
||||||
height: 58px;
|
|
||||||
padding: 5px 6px 4px;
|
|
||||||
border: 1px solid #7c8998;
|
|
||||||
border-radius: 5px;
|
|
||||||
color: #1f2933;
|
|
||||||
background: #ffffff;
|
|
||||||
box-shadow: 0 2px 5px rgba(15, 23, 42, 0.16);
|
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.component-drag-preview .palette-icon {
|
.component-drag-preview > .component-symbol {
|
||||||
width: 30px;
|
flex: none;
|
||||||
height: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.component-drag-preview-label {
|
|
||||||
max-width: 60px;
|
|
||||||
overflow: hidden;
|
|
||||||
font-size: 9px;
|
|
||||||
font-weight: 650;
|
|
||||||
line-height: 12px;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1426,13 +1415,8 @@ textarea {
|
|||||||
|
|
||||||
.sim-node {
|
.sim-node {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 132px;
|
width: var(--node-frame-width, 148px);
|
||||||
height: 84px;
|
height: var(--node-frame-height, 128px);
|
||||||
}
|
|
||||||
|
|
||||||
.sim-node.orientation-vertical {
|
|
||||||
width: 84px;
|
|
||||||
height: 132px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sim-node-frame {
|
.sim-node-frame {
|
||||||
@@ -1441,8 +1425,8 @@ textarea {
|
|||||||
left: 50%;
|
left: 50%;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
width: 132px;
|
width: var(--node-frame-width, 148px);
|
||||||
height: 84px;
|
height: var(--node-frame-height, 128px);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border: 2px solid #4b5563;
|
border: 2px solid #4b5563;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
@@ -1492,16 +1476,16 @@ textarea {
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
width: 112px;
|
width: var(--node-symbol-width, 128px);
|
||||||
height: 84px;
|
height: var(--node-symbol-height, 128px);
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-symbol {
|
.node-symbol {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
width: 112px;
|
width: var(--node-symbol-width, 128px);
|
||||||
height: 84px;
|
height: var(--node-symbol-height, 128px);
|
||||||
place-items: center;
|
place-items: center;
|
||||||
transform-origin: center;
|
transform-origin: center;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -271,3 +271,106 @@ test("仅图标模式仍可拖入氦气介质并在画布显示专用图标", as
|
|||||||
heliumCanvasSymbol.locator('[data-symbol-label="He"]'),
|
heliumCanvasSymbol.locator('[data-symbol-label="He"]'),
|
||||||
).toHaveText("He");
|
).toHaveText("He");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("从组件库拖入时使用工作区图标作为待拖入预览", async ({ page }) => {
|
||||||
|
await page.goto("/");
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const dataTransferPrototype = DataTransfer.prototype;
|
||||||
|
const originalSetDragImage = dataTransferPrototype.setDragImage;
|
||||||
|
dataTransferPrototype.setDragImage = function (
|
||||||
|
this: DataTransfer,
|
||||||
|
image: Element,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
) {
|
||||||
|
const bounds = image.getBoundingClientRect();
|
||||||
|
const style = window.getComputedStyle(image);
|
||||||
|
const symbol = image.querySelector<HTMLElement>(".component-symbol");
|
||||||
|
const svg = image.querySelector<SVGSVGElement>(".component-symbol-svg");
|
||||||
|
(
|
||||||
|
window as Window & {
|
||||||
|
__componentDragPreview?: {
|
||||||
|
backgroundColor: string;
|
||||||
|
borderTopWidth: string;
|
||||||
|
hasLegacyCardLabel: boolean;
|
||||||
|
height: number;
|
||||||
|
hotspotX: number;
|
||||||
|
hotspotY: number;
|
||||||
|
rootClassName: string;
|
||||||
|
symbolClassName: string;
|
||||||
|
viewBox: string | null;
|
||||||
|
width: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
).__componentDragPreview = {
|
||||||
|
backgroundColor: style.backgroundColor,
|
||||||
|
borderTopWidth: style.borderTopWidth,
|
||||||
|
hasLegacyCardLabel: Boolean(
|
||||||
|
image.querySelector(".component-drag-preview-label"),
|
||||||
|
),
|
||||||
|
height: bounds.height,
|
||||||
|
hotspotX: x,
|
||||||
|
hotspotY: y,
|
||||||
|
rootClassName: image.className,
|
||||||
|
symbolClassName: symbol?.className ?? "",
|
||||||
|
viewBox: svg?.getAttribute("viewBox") ?? null,
|
||||||
|
width: bounds.width,
|
||||||
|
};
|
||||||
|
dataTransferPrototype.setDragImage = originalSetDragImage;
|
||||||
|
return originalSetDragImage.call(this, image, x, y);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const heliumItem = page
|
||||||
|
.locator("aside.palette")
|
||||||
|
.getByRole("button", { name: "氦气介质定义", exact: true });
|
||||||
|
const canvas = page.locator(".flow-canvas .react-flow__pane");
|
||||||
|
await heliumItem.dragTo(canvas, {
|
||||||
|
targetPosition: { x: 420, y: 220 },
|
||||||
|
});
|
||||||
|
|
||||||
|
const preview = await page.evaluate(
|
||||||
|
() =>
|
||||||
|
(
|
||||||
|
window as Window & {
|
||||||
|
__componentDragPreview?: {
|
||||||
|
backgroundColor: string;
|
||||||
|
borderTopWidth: string;
|
||||||
|
hasLegacyCardLabel: boolean;
|
||||||
|
height: number;
|
||||||
|
hotspotX: number;
|
||||||
|
hotspotY: number;
|
||||||
|
rootClassName: string;
|
||||||
|
symbolClassName: string;
|
||||||
|
viewBox: string | null;
|
||||||
|
width: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
).__componentDragPreview,
|
||||||
|
);
|
||||||
|
expect(preview).toBeDefined();
|
||||||
|
expect(preview!.rootClassName).toContain("component-drag-preview");
|
||||||
|
expect(preview!.symbolClassName).toContain("component-symbol-canvas");
|
||||||
|
expect(preview!.symbolClassName).not.toContain("component-symbol-palette");
|
||||||
|
expect(preview!.hasLegacyCardLabel).toBe(false);
|
||||||
|
expect(preview!.borderTopWidth).toBe("0px");
|
||||||
|
expect(preview!.backgroundColor).toBe("rgba(0, 0, 0, 0)");
|
||||||
|
expect(Math.abs(preview!.hotspotX - preview!.width / 2)).toBeLessThanOrEqual(0.5);
|
||||||
|
expect(Math.abs(preview!.hotspotY - preview!.height / 2)).toBeLessThanOrEqual(0.5);
|
||||||
|
|
||||||
|
const heliumNode = page.locator(
|
||||||
|
'.flow-canvas .react-flow__node[data-id="amesim_helium_medium_1"]',
|
||||||
|
);
|
||||||
|
const nodeBounds = await heliumNode.boundingBox();
|
||||||
|
expect(nodeBounds).not.toBeNull();
|
||||||
|
expect(preview!.width).toBeCloseTo(nodeBounds!.width, 1);
|
||||||
|
expect(preview!.height).toBeCloseTo(nodeBounds!.height, 1);
|
||||||
|
await expect
|
||||||
|
.poll(() =>
|
||||||
|
heliumNode
|
||||||
|
.locator(".component-symbol-canvas .component-symbol-svg")
|
||||||
|
.getAttribute("viewBox"),
|
||||||
|
)
|
||||||
|
.toBe(preview!.viewBox);
|
||||||
|
await expect(page.locator("body > .component-drag-preview")).toHaveCount(0);
|
||||||
|
});
|
||||||
@@ -66,5 +66,39 @@ class ReactFlowProjectMediumReferenceVersionTests(unittest.TestCase):
|
|||||||
self.assertIn('<Parameter name="property_model" value="0.0"', xml)
|
self.assertIn('<Parameter name="property_model" value="0.0"', xml)
|
||||||
|
|
||||||
|
|
||||||
|
class ReactFlowPresentationLayoutVersionTests(unittest.TestCase):
|
||||||
|
def test_layout_version_is_preserved_but_not_written_to_xml(self) -> None:
|
||||||
|
project = ReactFlowProjectPayload(
|
||||||
|
name="versioned-presentation-layout",
|
||||||
|
presentationLayoutVersion=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(project.presentationLayoutVersion, 1)
|
||||||
|
self.assertEqual(
|
||||||
|
pydantic_to_jsonable(project)["presentationLayoutVersion"],
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
xml = build_reactflow_system_xml(project).decode("utf-8")
|
||||||
|
self.assertNotIn("presentationLayoutVersion", xml)
|
||||||
|
|
||||||
|
def test_layout_version_rejects_unsupported_values(self) -> None:
|
||||||
|
for version in (0, 2, -1, "1"):
|
||||||
|
with self.subTest(version=version):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
ReactFlowProjectPayload(
|
||||||
|
name="unsupported-presentation-layout",
|
||||||
|
presentationLayoutVersion=version,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_legacy_layout_version_remains_missing(self) -> None:
|
||||||
|
project = ReactFlowProjectPayload(name="legacy-presentation-layout")
|
||||||
|
|
||||||
|
self.assertIsNone(project.presentationLayoutVersion)
|
||||||
|
self.assertNotIn(
|
||||||
|
"presentationLayoutVersion",
|
||||||
|
pydantic_to_jsonable(project),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
Reference in new issue
Block a user