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