统一组件图标布局并完善选择吸附交互
This commit is contained in:
1 parent
40c72422ff
commit
7671418582
16 files changed
+4359
-598
No files matched your search
@@ -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() {
|
||||
return (
|
||||
<SymbolSvg domain="pneumatic">
|
||||
<path d="M2 24h34" stroke={SYMBOL_STROKE} strokeWidth="2" />
|
||||
<path
|
||||
d="m35 17 14 14m0-14L35 31"
|
||||
stroke={SYMBOL_ACCENT}
|
||||
strokeLinecap="round"
|
||||
strokeWidth="3"
|
||||
/>
|
||||
<g data-pnpl01-part="symbol">
|
||||
<path
|
||||
d={`M${PNPL01_PORT_ANCHOR.x} ${PNPL01_PORT_ANCHOR.y}H${PNPL01_CROSS_CENTER.x}`}
|
||||
data-pnpl01-part="extension"
|
||||
stroke={PNPL01_STROKE}
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -12,9 +12,63 @@ const MECHANICAL_LINE_WIDTH = 1.8;
|
||||
const MECMAS21_STROKE = "#00af00";
|
||||
const MECMAS21_HATCH = "#66cf66";
|
||||
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_FILL = "#cf9db6";
|
||||
const PNRP17_LIGHT_FILL = "#f5ebf0";
|
||||
// PNRP17 使用最终像素线宽;几何缩放不再改变参考图中的粗细层级。
|
||||
const PNRP17_PRIMARY_STROKE_WIDTH = 2;
|
||||
const PNRP17_DETAIL_STROKE_WIDTH = 1.5;
|
||||
|
||||
const mechanicalStroke = {
|
||||
fill: "none",
|
||||
@@ -34,16 +88,16 @@ function MechanicalSocket({ side }: { side: "left" | "right" }) {
|
||||
strokeLinejoin="miter"
|
||||
>
|
||||
<path
|
||||
d={facingLeft ? "M2 24h3.2" : "M58.8 24H62"}
|
||||
d={facingLeft ? "M2 24h4.2" : "M57.8 24H62"}
|
||||
strokeWidth="0.9"
|
||||
/>
|
||||
<path
|
||||
d={
|
||||
facingLeft
|
||||
? "M15.1 20.9H6.2v6.2h8.9"
|
||||
: "M48.9 20.9h8.9v6.2h-8.9"
|
||||
? "M14 18.5H6.2v11H14"
|
||||
: "M50 18.5h7.8v11H50"
|
||||
}
|
||||
strokeWidth="2"
|
||||
strokeWidth="2.5"
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
@@ -52,18 +106,28 @@ function MechanicalSocket({ side }: { side: "left" | "right" }) {
|
||||
export function AmesimF000Symbol() {
|
||||
return (
|
||||
<SymbolSvg className="component-symbol-amesim-f000" domain="mechanical">
|
||||
<path d="M41 24h21" {...mechanicalStroke} />
|
||||
<rect
|
||||
fill={SYMBOL_FILL}
|
||||
height="28"
|
||||
rx="1"
|
||||
stroke={SYMBOL_STROKE}
|
||||
strokeWidth="2.2"
|
||||
width="18"
|
||||
x="23"
|
||||
y="12"
|
||||
/>
|
||||
<path d="M27 26h10" stroke={SYMBOL_ACCENT} strokeWidth="2" />
|
||||
<g data-f000-part="symbol">
|
||||
<path
|
||||
d={`M${F000_FRAME_RIGHT_X} ${F000_PORT_ANCHOR.y}H${F000_PORT_ANCHOR.x}`}
|
||||
data-f000-part="extension"
|
||||
stroke={SYMBOL_STROKE}
|
||||
strokeLinecap="butt"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -71,37 +135,64 @@ export function AmesimF000Symbol() {
|
||||
export function AmesimForcSymbol() {
|
||||
return (
|
||||
<SymbolSvg className="component-symbol-amesim-forc" domain="mechanical">
|
||||
<g
|
||||
fill="none"
|
||||
stroke={SYMBOL_ACCENT}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.8"
|
||||
>
|
||||
<path d="M2 24h15" />
|
||||
<path d="m11 19 6 5-6 5" />
|
||||
<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"
|
||||
stroke={FORC_STROKE}
|
||||
strokeLinecap="butt"
|
||||
strokeLinejoin="miter"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<rect
|
||||
data-forc-part="mechanical-socket"
|
||||
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
|
||||
cx="30"
|
||||
cy="24"
|
||||
data-forc-part="force-circle"
|
||||
fill="#ffffff"
|
||||
r="12"
|
||||
stroke={FORC_STROKE}
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<text
|
||||
data-forc-part="label"
|
||||
dominantBaseline="central"
|
||||
fill={FORC_STROKE}
|
||||
fontFamily="Arial, sans-serif"
|
||||
fontSize="15"
|
||||
fontWeight="600"
|
||||
textAnchor="middle"
|
||||
x="30"
|
||||
y="24"
|
||||
>
|
||||
F
|
||||
</text>
|
||||
</g>
|
||||
<circle
|
||||
cx="32"
|
||||
cy="24"
|
||||
fill={SYMBOL_FILL}
|
||||
r="12"
|
||||
stroke={SYMBOL_STROKE}
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<text
|
||||
dominantBaseline="central"
|
||||
fill={SYMBOL_STROKE}
|
||||
fontFamily="Arial, sans-serif"
|
||||
fontSize="13"
|
||||
fontWeight="600"
|
||||
textAnchor="middle"
|
||||
x="32"
|
||||
y="24"
|
||||
>
|
||||
F
|
||||
</text>
|
||||
<path d="M44 24h6M50 18v12M50 18h8M50 30h8M58 18v12M58 24h4" {...mechanicalStroke} />
|
||||
</SymbolSvg>
|
||||
);
|
||||
}
|
||||
@@ -141,7 +232,7 @@ export function AmesimMecmas21Symbol({
|
||||
<path
|
||||
d="M-1 1 1-1M0 5.05 5.05 0M4.05 6.05 6.05 4.05"
|
||||
stroke={MECMAS21_HATCH}
|
||||
strokeWidth="1.55"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
</pattern>
|
||||
</defs>
|
||||
@@ -149,23 +240,24 @@ export function AmesimMecmas21Symbol({
|
||||
data-mecmas-friction={frictionEnabled ? "enabled" : "disabled"}
|
||||
data-mecmas-limits={limitsEnabled ? "finite" : "unlimited"}
|
||||
data-mecmas-variant={variant}
|
||||
transform={MECMAS21_CONTENT_TRANSFORM}
|
||||
>
|
||||
{showGuides ? (
|
||||
<g data-mecmas-part="guides">
|
||||
<rect
|
||||
data-mecmas-part="upper-guide"
|
||||
fill={`url(#${hatchingId})`}
|
||||
height="5.25"
|
||||
height="8.5"
|
||||
stroke={MECMAS21_STROKE}
|
||||
strokeWidth="1.2"
|
||||
width="60"
|
||||
x="2"
|
||||
y="2.6"
|
||||
width="61"
|
||||
x="1.5"
|
||||
y="-5.5"
|
||||
/>
|
||||
{limitsEnabled ? (
|
||||
<g data-mecmas-part="finite-lower-guide">
|
||||
<path
|
||||
d="M2 32.6H10V39.6H54V32.6H62V45.4H2Z"
|
||||
d="M1.5 34.5H9V45H55V34.5H62.5V53.5H1.5Z"
|
||||
fill={`url(#${hatchingId})`}
|
||||
stroke={MECMAS21_STROKE}
|
||||
strokeLinejoin="miter"
|
||||
@@ -176,19 +268,19 @@ export function AmesimMecmas21Symbol({
|
||||
<rect
|
||||
data-mecmas-part="unlimited-lower-guide"
|
||||
fill={`url(#${hatchingId})`}
|
||||
height="5.25"
|
||||
height="8.5"
|
||||
stroke={MECMAS21_STROKE}
|
||||
strokeWidth="1.2"
|
||||
width="60"
|
||||
x="2"
|
||||
y="40.23"
|
||||
width="61"
|
||||
x="1.5"
|
||||
y="45"
|
||||
/>
|
||||
)}
|
||||
</g>
|
||||
) : null}
|
||||
{frictionEnabled ? (
|
||||
<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"
|
||||
stroke={MECMAS21_STROKE}
|
||||
strokeLinecap="butt"
|
||||
@@ -201,12 +293,12 @@ export function AmesimMecmas21Symbol({
|
||||
<rect
|
||||
data-mecmas-part="mass"
|
||||
fill={MECMAS21_FILL}
|
||||
height="23.1"
|
||||
height="34"
|
||||
stroke={MECMAS21_STROKE}
|
||||
strokeWidth="2"
|
||||
width="34"
|
||||
x="15"
|
||||
y="12.45"
|
||||
strokeWidth="2.5"
|
||||
width="36"
|
||||
x="14"
|
||||
y="7"
|
||||
/>
|
||||
<text
|
||||
dominantBaseline="central"
|
||||
@@ -216,23 +308,33 @@ export function AmesimMecmas21Symbol({
|
||||
fontWeight="500"
|
||||
textAnchor="middle"
|
||||
x="32"
|
||||
y="29.2"
|
||||
y="31"
|
||||
>
|
||||
M
|
||||
</text>
|
||||
<path
|
||||
d="M18.94 18.8h5.88M21.9 16.6V21"
|
||||
d="M18.8 16.8H25M21.9 13.8v6"
|
||||
fill="none"
|
||||
stroke={MECMAS21_STROKE}
|
||||
strokeLinecap="square"
|
||||
strokeWidth="1.05"
|
||||
/>
|
||||
<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}
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@@ -322,27 +424,27 @@ export function AmesimPnrp17Symbol() {
|
||||
fill={PNRP17_FILL}
|
||||
stroke={PNRP17_STROKE}
|
||||
strokeLinejoin="miter"
|
||||
strokeWidth="1.2"
|
||||
strokeWidth={PNRP17_PRIMARY_STROKE_WIDTH}
|
||||
/>
|
||||
<path
|
||||
d="M8.2 26.7h12.7v13.8h2v6.7H8.2Z"
|
||||
fill={PNRP17_FILL}
|
||||
stroke={PNRP17_STROKE}
|
||||
strokeLinejoin="miter"
|
||||
strokeWidth="1.2"
|
||||
strokeWidth={PNRP17_PRIMARY_STROKE_WIDTH}
|
||||
/>
|
||||
<path
|
||||
d="M28 40.5h28.5v6.7H28Z"
|
||||
fill={PNRP17_FILL}
|
||||
stroke={PNRP17_STROKE}
|
||||
strokeLinejoin="miter"
|
||||
strokeWidth="1.2"
|
||||
strokeWidth={PNRP17_PRIMARY_STROKE_WIDTH}
|
||||
/>
|
||||
<rect
|
||||
fill={PNRP17_LIGHT_FILL}
|
||||
height="34"
|
||||
stroke={PNRP17_STROKE}
|
||||
strokeWidth="1.2"
|
||||
strokeWidth={PNRP17_PRIMARY_STROKE_WIDTH}
|
||||
width="7.7"
|
||||
x="40.8"
|
||||
y="7.2"
|
||||
@@ -352,7 +454,7 @@ export function AmesimPnrp17Symbol() {
|
||||
fill={PNRP17_LIGHT_FILL}
|
||||
height="2.8"
|
||||
stroke={PNRP17_STROKE}
|
||||
strokeWidth="1.2"
|
||||
strokeWidth={PNRP17_PRIMARY_STROKE_WIDTH}
|
||||
width="31.5"
|
||||
x="9.3"
|
||||
y="22.7"
|
||||
@@ -361,7 +463,7 @@ export function AmesimPnrp17Symbol() {
|
||||
fill={PNRP17_LIGHT_FILL}
|
||||
height="2.8"
|
||||
stroke={PNRP17_STROKE}
|
||||
strokeWidth="1.2"
|
||||
strokeWidth={PNRP17_PRIMARY_STROKE_WIDTH}
|
||||
width="7.7"
|
||||
x="47.8"
|
||||
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"
|
||||
stroke={PNRP17_STROKE}
|
||||
strokeLinecap="butt"
|
||||
strokeWidth="1.2"
|
||||
strokeWidth={PNRP17_PRIMARY_STROKE_WIDTH}
|
||||
/>
|
||||
{[15.3, 33].map((centerY) => (
|
||||
<g
|
||||
@@ -379,7 +481,7 @@ export function AmesimPnrp17Symbol() {
|
||||
stroke={PNRP17_STROKE}
|
||||
strokeLinecap="butt"
|
||||
strokeLinejoin="miter"
|
||||
strokeWidth="1.1"
|
||||
strokeWidth={PNRP17_DETAIL_STROKE_WIDTH}
|
||||
>
|
||||
<path d={`M28.8 ${centerY}h-3.4`} />
|
||||
<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" }) {
|
||||
return (
|
||||
<SymbolSvg className="component-symbol-medium-svg" domain="medium">
|
||||
<SymbolSvg
|
||||
className="component-symbol-medium-svg"
|
||||
domain="medium"
|
||||
>
|
||||
<circle
|
||||
cx="32"
|
||||
cy="24"
|
||||
fill={SYMBOL_FILL}
|
||||
r="21"
|
||||
stroke={SYMBOL_STROKE}
|
||||
r={MEDIUM_SYMBOL_RADIUS}
|
||||
stroke={MEDIUM_SYMBOL_COLOR}
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<text
|
||||
className="component-symbol-medium-label"
|
||||
data-symbol-label={label}
|
||||
dominantBaseline="central"
|
||||
fill={SYMBOL_ACCENT}
|
||||
fill={MEDIUM_SYMBOL_COLOR}
|
||||
fontFamily="Arial, sans-serif"
|
||||
fontSize={label === "Air" ? 12 : 16}
|
||||
fontWeight="500"
|
||||
fontSize={MEDIUM_SYMBOL_LABEL_FONT_SIZE}
|
||||
fontWeight="600"
|
||||
textAnchor="middle"
|
||||
x="32"
|
||||
y="24"
|
||||
|
||||
@@ -8,85 +8,87 @@ import {
|
||||
|
||||
const PNEUMATIC_STROKE_WIDTH = 1.7;
|
||||
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({
|
||||
centerY = 25,
|
||||
leftPortY = centerY,
|
||||
}: {
|
||||
centerY?: number;
|
||||
leftPortY?: number;
|
||||
}) {
|
||||
const top = centerY - 10;
|
||||
const bottom = centerY + 10;
|
||||
const leftConnector =
|
||||
leftPortY === centerY
|
||||
? `M2 ${centerY}h18`
|
||||
: `M2 ${leftPortY}h14L20 ${centerY}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<path
|
||||
d={`${leftConnector}M44 ${centerY}h18M20 ${centerY}h24`}
|
||||
stroke={SYMBOL_STROKE}
|
||||
strokeLinecap="round"
|
||||
strokeWidth={PNEUMATIC_STROKE_WIDTH}
|
||||
/>
|
||||
<path
|
||||
d={`M20 ${top}c5 6 9 7 12 7s7-1 12-7M20 ${bottom}c5-6 9-7 12-7s7 1 12 7`}
|
||||
stroke={SYMBOL_STROKE}
|
||||
strokeLinecap="round"
|
||||
strokeWidth={PNEUMATIC_STROKE_WIDTH}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
function scaledOrificePoint(x: number, y: number) {
|
||||
return {
|
||||
x:
|
||||
ORIFICE_CONTENT_CENTER.x +
|
||||
(x - ORIFICE_CONTENT_CENTER.x) * ORIFICE_CONTENT_SCALE,
|
||||
y:
|
||||
ORIFICE_CONTENT_CENTER.y +
|
||||
(y - ORIFICE_CONTENT_CENTER.y) * ORIFICE_CONTENT_SCALE,
|
||||
};
|
||||
}
|
||||
|
||||
/** Constant-coefficient pneumatic orifice (AMESim PNOR001). */
|
||||
export function AmesimPnor001Symbol() {
|
||||
return (
|
||||
<SymbolSvg className="component-symbol-amesim-pnor001-svg" domain="pneumatic">
|
||||
<OrificeBody centerY={24} />
|
||||
</SymbolSvg>
|
||||
);
|
||||
}
|
||||
export const PNVO001_PORT_ANCHORS = {
|
||||
physicalLeft: scaledOrificePoint(0, 24),
|
||||
physicalRight: scaledOrificePoint(64, 24),
|
||||
signal: scaledOrificePoint(24.25, 56),
|
||||
} as const;
|
||||
|
||||
function Pnvo001Body({
|
||||
signalControlled = false,
|
||||
}: {
|
||||
signalControlled?: boolean;
|
||||
}) {
|
||||
type OrificeControl = "none" | "fixed" | "signal";
|
||||
|
||||
function Pnvo001Body({ control }: { control: OrificeControl }) {
|
||||
return (
|
||||
<>
|
||||
<g
|
||||
data-orifice-control={control}
|
||||
data-orifice-family="pnvo001"
|
||||
transform={ORIFICE_CONTENT_TRANSFORM}
|
||||
>
|
||||
<path
|
||||
d="M0 24.5h64"
|
||||
d="M0 24H64"
|
||||
data-orifice-part="flow-path"
|
||||
stroke={PNVO001_STROKE}
|
||||
strokeLinecap="butt"
|
||||
strokeWidth="2.1"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
<path
|
||||
d="M15.5 14.4Q33.4 27 51.2 14.4M15.5 34.8Q33.2 22.4 51.2 34.8"
|
||||
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="3.1"
|
||||
strokeWidth="2.5"
|
||||
/>
|
||||
<path
|
||||
d="M19 46.7 44.5 10"
|
||||
stroke={PNVO001_STROKE}
|
||||
strokeLinecap="butt"
|
||||
strokeWidth="2.6"
|
||||
/>
|
||||
{signalControlled ? (
|
||||
{control === "none" ? null : (
|
||||
<>
|
||||
<path
|
||||
d="M16.5 54.5 42 3.5"
|
||||
data-orifice-part="opening-arrow"
|
||||
stroke={PNVO001_STROKE}
|
||||
strokeLinecap="butt"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
<path
|
||||
d="M48 -8 45.8 7.9 37 2.4Z"
|
||||
data-orifice-part="opening-arrow-head"
|
||||
fill={PNVO001_STROKE}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{control === "signal" ? (
|
||||
<path
|
||||
d="M26 48V36.6l7.2 10.1"
|
||||
d="M24.25 56V39L32 54.5"
|
||||
data-orifice-part="signal-control"
|
||||
stroke={PNVO001_STROKE}
|
||||
strokeLinecap="butt"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2.3"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
) : null}
|
||||
<path d="M50.6 0 38.6 8.2 48 12.3Z" fill={PNVO001_STROKE} />
|
||||
</>
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
/** 定系数气动孔口(AMESim PNOR001)。 */
|
||||
export function AmesimPnor001Symbol() {
|
||||
return (
|
||||
<SymbolSvg className="component-symbol-amesim-pnor001-svg" domain="pneumatic">
|
||||
<Pnvo001Body control="none" />
|
||||
</SymbolSvg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -97,7 +99,7 @@ export function AmesimPnvo001FixedSymbol() {
|
||||
className="component-symbol-amesim-pnvo001-fixed-svg"
|
||||
domain="pneumatic"
|
||||
>
|
||||
<Pnvo001Body />
|
||||
<Pnvo001Body control="fixed" />
|
||||
</SymbolSvg>
|
||||
);
|
||||
}
|
||||
@@ -109,7 +111,7 @@ export function AmesimPnvo001SignalSymbol() {
|
||||
className="component-symbol-amesim-pnvo001-signal-svg"
|
||||
domain="pneumatic"
|
||||
>
|
||||
<Pnvo001Body signalControlled />
|
||||
<Pnvo001Body control="signal" />
|
||||
</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 =
|
||||
| "neutral"
|
||||
@@ -11,25 +13,48 @@ type SymbolSvgProps = {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
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_FILL = "var(--component-symbol-fill, #ffffff)";
|
||||
export const SYMBOL_ACCENT = "var(--component-symbol-accent, currentColor)";
|
||||
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({
|
||||
children,
|
||||
className,
|
||||
domain = "neutral",
|
||||
viewBox = "0 0 64 48",
|
||||
strokeScaling = "independent",
|
||||
}: SymbolSvgProps) {
|
||||
const viewBox = useContext(SymbolViewBoxContext);
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className={["component-symbol-svg", className].filter(Boolean).join(" ")}
|
||||
data-symbol-domain={domain}
|
||||
data-symbol-stroke-scaling={strokeScaling}
|
||||
fill="none"
|
||||
focusable="false"
|
||||
viewBox={viewBox}
|
||||
|
||||
@@ -6,6 +6,31 @@ import {
|
||||
} from "./primitives";
|
||||
|
||||
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() {
|
||||
return (
|
||||
@@ -25,23 +50,25 @@ function SignalOutput() {
|
||||
export function AmesimStep0Symbol() {
|
||||
return (
|
||||
<SymbolSvg className="component-symbol-amesim-step0" domain="signal">
|
||||
<circle
|
||||
cx="27"
|
||||
cy="24"
|
||||
fill={SYMBOL_FILL}
|
||||
r="18"
|
||||
stroke={SYMBOL_STROKE}
|
||||
strokeWidth={SIGNAL_LINE_WIDTH}
|
||||
/>
|
||||
<path
|
||||
d="M14 30h8V17h18"
|
||||
fill="none"
|
||||
stroke={SYMBOL_ACCENT}
|
||||
strokeLinecap="square"
|
||||
strokeLinejoin="miter"
|
||||
strokeWidth="2.2"
|
||||
/>
|
||||
<SignalOutput />
|
||||
<g data-signal-source="step0" transform={SIGNAL_CONTENT_TRANSFORM}>
|
||||
<circle
|
||||
cx="27"
|
||||
cy="24"
|
||||
fill={SYMBOL_FILL}
|
||||
r="18"
|
||||
stroke={SYMBOL_STROKE}
|
||||
strokeWidth={SIGNAL_LINE_WIDTH}
|
||||
/>
|
||||
<path
|
||||
d="M14 30h8V17h18"
|
||||
fill="none"
|
||||
stroke={SYMBOL_ACCENT}
|
||||
strokeLinecap="square"
|
||||
strokeLinejoin="miter"
|
||||
strokeWidth="2.2"
|
||||
/>
|
||||
<SignalOutput />
|
||||
</g>
|
||||
</SymbolSvg>
|
||||
);
|
||||
}
|
||||
@@ -49,23 +76,25 @@ export function AmesimStep0Symbol() {
|
||||
export function AmesimUd00Symbol() {
|
||||
return (
|
||||
<SymbolSvg className="component-symbol-amesim-ud00" domain="signal">
|
||||
<circle
|
||||
cx="27"
|
||||
cy="24"
|
||||
fill={SYMBOL_FILL}
|
||||
r="18"
|
||||
stroke={SYMBOL_STROKE}
|
||||
strokeWidth={SIGNAL_LINE_WIDTH}
|
||||
/>
|
||||
<path
|
||||
d="M13 30h5l5-10h5l5-9 5 19h3"
|
||||
fill="none"
|
||||
stroke={SYMBOL_ACCENT}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2.2"
|
||||
/>
|
||||
<SignalOutput />
|
||||
<g data-signal-source="ud00" transform={SIGNAL_CONTENT_TRANSFORM}>
|
||||
<circle
|
||||
cx="27"
|
||||
cy="24"
|
||||
fill={SYMBOL_FILL}
|
||||
r="18"
|
||||
stroke={SYMBOL_STROKE}
|
||||
strokeWidth={SIGNAL_LINE_WIDTH}
|
||||
/>
|
||||
<path
|
||||
d="M13 30h5l5-10h5l5-9 5 19h3"
|
||||
fill="none"
|
||||
stroke={SYMBOL_ACCENT}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2.2"
|
||||
/>
|
||||
<SignalOutput />
|
||||
</g>
|
||||
</SymbolSvg>
|
||||
);
|
||||
}
|
||||
@@ -1,56 +1,76 @@
|
||||
import {
|
||||
SYMBOL_ACCENT,
|
||||
SYMBOL_FILL,
|
||||
SYMBOL_STROKE,
|
||||
SymbolSvg,
|
||||
} 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 (
|
||||
<>
|
||||
<circle
|
||||
cx="32"
|
||||
cy="24"
|
||||
fill={SYMBOL_FILL}
|
||||
r="13"
|
||||
stroke={SYMBOL_STROKE}
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<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
|
||||
cx="32"
|
||||
cy="24"
|
||||
data-pnch023-part="chamber"
|
||||
fill={SYMBOL_FILL}
|
||||
r="21"
|
||||
stroke={SYMBOL_STROKE}
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</g>
|
||||
<text
|
||||
data-pnch023-part="label"
|
||||
dominantBaseline="central"
|
||||
fill={SYMBOL_ACCENT}
|
||||
fill={PNCH012_STROKE}
|
||||
fontFamily="Arial, sans-serif"
|
||||
fontSize="13"
|
||||
fontWeight="700"
|
||||
fontSize={PNCH_CHAMBER_LABEL_FONT_SIZE}
|
||||
fontWeight="600"
|
||||
textAnchor="middle"
|
||||
x="32"
|
||||
y="24"
|
||||
>
|
||||
Cp
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@@ -58,12 +78,54 @@ export function AmesimPnch023Symbol() {
|
||||
export function AmesimPnch012Symbol() {
|
||||
return (
|
||||
<SymbolSvg domain="pneumatic">
|
||||
<path
|
||||
d="M2 16h17M2 32h17M45 16h17M45 32h17"
|
||||
stroke={SYMBOL_STROKE}
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<ChamberCore variable />
|
||||
<g data-pnch012-part="symbol" transform={PNCH_CHAMBER_CONTENT_TRANSFORM}>
|
||||
<path
|
||||
d="M0 24H11M53 24H64M32-8V3M32 45V56"
|
||||
data-pnch012-part="ports"
|
||||
stroke={PNCH012_STROKE}
|
||||
strokeLinecap="butt"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -11,6 +11,41 @@ export type ComponentSymbolPortAnchors = Partial<
|
||||
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<
|
||||
Record<string, number | string>
|
||||
>;
|
||||
@@ -22,4 +57,6 @@ export type ComponentSymbolRendererProps = {
|
||||
export type SymbolDefinition = {
|
||||
presentation: ComponentSymbolPresentation;
|
||||
render: ComponentType<ComponentSymbolRendererProps>;
|
||||
layout: ComponentSymbolLayout;
|
||||
variantKey?: (parameters?: ComponentSymbolParameters) => string;
|
||||
};
|
||||
Reference in new issue
Block a user