style: match PSD dimension lines to reference style
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-04-02 21:33:55 +08:00
parent cc53eee037
commit d53308126d

View File

@ -83,6 +83,9 @@ const PREVIEW_MIN_HEIGHT = 340;
const LABEL_PLACEHOLDER = "例如:120cm / 50cm";
const IMAGE_EXTENSION_REGEX = /\.(png|jpg|jpeg|webp)$/i;
const DIMENSION_SUFFIX_REGEX = /-尺寸标注(?=\.)/;
const DIMENSION_LINE_COLOR = "#666b73";
const DIMENSION_TEXT_COLOR = "#2a2f35";
const DIMENSION_TEXT_HALO_COLOR = "rgba(255,255,255,0.92)";
function clamp(value: number, min: number, max: number) {
return Math.max(min, Math.min(max, value));
@ -219,17 +222,21 @@ function getPreviewAnnotationGeometry(
);
const vector = getAnnotationVector(start.x, start.y, end.x, end.y);
const label = getDimensionLabel(annotation);
const labelWidth = clamp(label.length * 11 + 26, 76, 240);
const labelHeight = 30;
const labelOffset = 22;
const labelOffset = 16;
const rawAngleRadians = Math.atan2(vector.dy, vector.dx);
const angleRadians =
rawAngleRadians > Math.PI / 2
? rawAngleRadians - Math.PI
: rawAngleRadians < -Math.PI / 2
? rawAngleRadians + Math.PI
: rawAngleRadians;
return {
start,
end,
vector,
label,
labelWidth,
labelHeight,
angleDegrees: (angleRadians * 180) / Math.PI,
labelCenterX: (start.x + end.x) / 2 + vector.nx * labelOffset,
labelCenterY: (start.y + end.y) / 2 + vector.ny * labelOffset,
};
@ -303,62 +310,19 @@ function downloadBlob(blob: Blob, fileName: string) {
URL.revokeObjectURL(objectUrl);
}
function drawArrowHead(params: {
function drawDimensionCap(params: {
context: CanvasRenderingContext2D;
point: Point;
direction: Point;
vector: ReturnType<typeof getAnnotationVector>;
size: number;
color: string;
}) {
const { context, point, direction, size, color } = params;
const directionLength = Math.max(1, Math.hypot(direction.x, direction.y));
const ux = direction.x / directionLength;
const uy = direction.y / directionLength;
const angle = Math.PI / 7;
const leftAngleX = ux * Math.cos(angle) - uy * Math.sin(angle);
const leftAngleY = ux * Math.sin(angle) + uy * Math.cos(angle);
const rightAngleX = ux * Math.cos(-angle) - uy * Math.sin(-angle);
const rightAngleY = ux * Math.sin(-angle) + uy * Math.cos(-angle);
const { context, point, vector, size } = params;
context.beginPath();
context.moveTo(point.x, point.y);
context.lineTo(point.x - leftAngleX * size, point.y - leftAngleY * size);
context.moveTo(point.x, point.y);
context.lineTo(point.x - rightAngleX * size, point.y - rightAngleY * size);
context.strokeStyle = color;
context.lineCap = "round";
context.moveTo(point.x + vector.nx * size, point.y + vector.ny * size);
context.lineTo(point.x - vector.nx * size, point.y - vector.ny * size);
context.stroke();
}
function fillRoundedRect(params: {
context: CanvasRenderingContext2D;
x: number;
y: number;
width: number;
height: number;
radius: number;
}) {
const { context, x, y, width, height, radius } = params;
const clampedRadius = Math.min(radius, width / 2, height / 2);
context.beginPath();
context.moveTo(x + clampedRadius, y);
context.lineTo(x + width - clampedRadius, y);
context.quadraticCurveTo(x + width, y, x + width, y + clampedRadius);
context.lineTo(x + width, y + height - clampedRadius);
context.quadraticCurveTo(
x + width,
y + height,
x + width - clampedRadius,
y + height
);
context.lineTo(x + clampedRadius, y + height);
context.quadraticCurveTo(x, y + height, x, y + height - clampedRadius);
context.lineTo(x, y + clampedRadius);
context.quadraticCurveTo(x, y, x + clampedRadius, y);
context.closePath();
context.fill();
}
function drawAnnotationToCanvas(
context: CanvasRenderingContext2D,
annotation: DimensionAnnotation,
@ -371,99 +335,62 @@ function drawAnnotationToCanvas(
annotation.x2,
annotation.y2
);
const rawAngleRadians = Math.atan2(vector.dy, vector.dx);
const angleRadians =
rawAngleRadians > Math.PI / 2
? rawAngleRadians - Math.PI
: rawAngleRadians < -Math.PI / 2
? rawAngleRadians + Math.PI
: rawAngleRadians;
const strokeWidth = clamp(
Math.round(Math.min(imageSize.width, imageSize.height) * 0.004),
2,
8
Math.min(imageSize.width, imageSize.height) * 0.0018,
1.25,
3
);
const tickLength = strokeWidth * 2.5;
const arrowSize = strokeWidth * 3.5;
const capLength = strokeWidth * 6;
const fontSize = clamp(
Math.round(Math.min(imageSize.width, imageSize.height) * 0.028),
16,
42
Math.round(Math.min(imageSize.width, imageSize.height) * 0.022),
14,
34
);
const labelPaddingX = fontSize * 0.7;
const labelPaddingY = fontSize * 0.45;
const labelOffset = fontSize * 1.4;
const labelOffset = fontSize * 0.95;
const centerX = (annotation.x1 + annotation.x2) / 2 + vector.nx * labelOffset;
const centerY = (annotation.y1 + annotation.y2) / 2 + vector.ny * labelOffset;
context.save();
context.strokeStyle = "#fb923c";
context.strokeStyle = DIMENSION_LINE_COLOR;
context.lineWidth = strokeWidth;
context.lineCap = "round";
context.lineJoin = "round";
context.shadowColor = "rgba(249, 115, 22, 0.22)";
context.shadowBlur = strokeWidth * 2;
context.beginPath();
context.moveTo(annotation.x1, annotation.y1);
context.lineTo(annotation.x2, annotation.y2);
context.stroke();
context.beginPath();
context.moveTo(
annotation.x1 + vector.nx * tickLength,
annotation.y1 + vector.ny * tickLength
);
context.lineTo(
annotation.x1 - vector.nx * tickLength,
annotation.y1 - vector.ny * tickLength
);
context.moveTo(
annotation.x2 + vector.nx * tickLength,
annotation.y2 + vector.ny * tickLength
);
context.lineTo(
annotation.x2 - vector.nx * tickLength,
annotation.y2 - vector.ny * tickLength
);
context.stroke();
drawArrowHead({
drawDimensionCap({
context,
point: { x: annotation.x1, y: annotation.y1 },
direction: {
x: annotation.x1 - annotation.x2,
y: annotation.y1 - annotation.y2,
},
size: arrowSize,
color: "#fb923c",
size: capLength,
vector,
});
drawArrowHead({
drawDimensionCap({
context,
point: { x: annotation.x2, y: annotation.y2 },
direction: {
x: annotation.x2 - annotation.x1,
y: annotation.y2 - annotation.y1,
},
size: arrowSize,
color: "#fb923c",
size: capLength,
vector,
});
context.shadowBlur = 0;
context.font = `600 ${fontSize}px "PingFang SC", "Microsoft YaHei", "Noto Sans SC", sans-serif`;
const textMetrics = context.measureText(label);
const labelWidth = textMetrics.width + labelPaddingX * 2;
const labelHeight = fontSize + labelPaddingY * 2;
const labelX = centerX - labelWidth / 2;
const labelY = centerY - labelHeight / 2;
context.fillStyle = "rgba(9, 9, 11, 0.88)";
fillRoundedRect({
context,
x: labelX,
y: labelY,
width: labelWidth,
height: labelHeight,
radius: fontSize * 0.55,
});
context.fillStyle = "#ffffff";
context.translate(centerX, centerY);
context.rotate(angleRadians);
context.font = `500 ${fontSize}px "PingFang SC", "Microsoft YaHei", "Noto Sans SC", sans-serif`;
context.textAlign = "center";
context.textBaseline = "middle";
context.fillText(label, centerX, centerY + 1);
context.lineWidth = Math.max(2, fontSize * 0.22);
context.strokeStyle = DIMENSION_TEXT_HALO_COLOR;
context.strokeText(label, 0, 0);
context.fillStyle = DIMENSION_TEXT_COLOR;
context.fillText(label, 0, 0);
context.restore();
}
@ -986,19 +913,6 @@ export function AgentPsdTemplateExportPreview({
className="absolute inset-0 h-full w-full"
onPointerDown={handleOverlayPointerDown}
>
<defs>
<marker
id="psd-dimension-arrow"
markerHeight="8"
markerWidth="8"
orient="auto-start-reverse"
refX="4"
refY="4"
>
<path d="M0,0 L8,4 L0,8 z" fill="#fb923c" />
</marker>
</defs>
{annotations.map((annotation) => {
const isSelected = annotation.id === selectedAnnotationId;
const geometry = getPreviewAnnotationGeometry(
@ -1006,7 +920,8 @@ export function AgentPsdTemplateExportPreview({
previewRect,
imageSize
);
const tickLength = isSelected ? 10 : 8;
const strokeWidth = isSelected ? 2 : 1.5;
const tickLength = isSelected ? 8 : 6.5;
return (
<g key={annotation.id}>
@ -1022,13 +937,11 @@ export function AgentPsdTemplateExportPreview({
y2={geometry.end.y}
/>
<line
markerEnd="url(#psd-dimension-arrow)"
markerStart="url(#psd-dimension-arrow)"
opacity={isSelected ? 1 : 0.92}
pointerEvents="none"
stroke="#fb923c"
stroke={DIMENSION_LINE_COLOR}
strokeLinecap="round"
strokeWidth={isSelected ? 3 : 2.5}
strokeWidth={strokeWidth}
x1={geometry.start.x}
x2={geometry.end.x}
y1={geometry.start.y}
@ -1036,7 +949,7 @@ export function AgentPsdTemplateExportPreview({
/>
<line
pointerEvents="none"
stroke="#fdba74"
stroke={DIMENSION_LINE_COLOR}
strokeLinecap="round"
strokeWidth={isSelected ? 2.5 : 2}
x1={geometry.start.x + geometry.vector.nx * tickLength}
@ -1046,7 +959,7 @@ export function AgentPsdTemplateExportPreview({
/>
<line
pointerEvents="none"
stroke="#fdba74"
stroke={DIMENSION_LINE_COLOR}
strokeLinecap="round"
strokeWidth={isSelected ? 2.5 : 2}
x1={geometry.end.x + geometry.vector.nx * tickLength}
@ -1059,27 +972,18 @@ export function AgentPsdTemplateExportPreview({
handleAnnotationBodyPointerDown(annotation, event)
}
>
<rect
fill="rgba(9,9,11,0.88)"
height={geometry.labelHeight}
rx={15}
stroke={
isSelected
? "rgba(251,146,60,0.7)"
: "rgba(255,255,255,0.14)"
}
strokeWidth={isSelected ? 1.5 : 1}
width={geometry.labelWidth}
x={geometry.labelCenterX - geometry.labelWidth / 2}
y={geometry.labelCenterY - geometry.labelHeight / 2}
/>
<text
dominantBaseline="middle"
fill="#ffffff"
fill={DIMENSION_TEXT_COLOR}
fontSize={13}
fontWeight={600}
fontWeight={500}
paintOrder="stroke"
pointerEvents="none"
stroke={DIMENSION_TEXT_HALO_COLOR}
strokeLinejoin="round"
strokeWidth={2.8}
textAnchor="middle"
transform={`rotate(${geometry.angleDegrees} ${geometry.labelCenterX} ${geometry.labelCenterY})`}
x={geometry.labelCenterX}
y={geometry.labelCenterY + 0.5}
>
@ -1099,9 +1003,9 @@ export function AgentPsdTemplateExportPreview({
event
)
}
r={6}
stroke="#fb923c"
strokeWidth={2}
r={4}
stroke={DIMENSION_LINE_COLOR}
strokeWidth={1.5}
/>
<circle
cx={geometry.end.x}
@ -1114,9 +1018,9 @@ export function AgentPsdTemplateExportPreview({
event
)
}
r={6}
stroke="#fb923c"
strokeWidth={2}
r={4}
stroke={DIMENSION_LINE_COLOR}
strokeWidth={1.5}
/>
</>
) : null}