style: match PSD dimension lines to reference style
This commit is contained in:
parent
cc53eee037
commit
d53308126d
@ -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}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user