Bin
2025-12-16 9e0b2ba2c317b1a86212f24cbae3195ad1f3dbfa
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
import type React from "react";
import { Circle } from "react-konva";
import type Konva from "konva";
import type { BezierPoint } from "../types";
import { HIT_RADIUS } from "../constants";
 
interface VectorPointsProps {
  initialPoints: BezierPoint[];
  selectedPointIndex: number | null;
  selectedPoints: Set<number>;
  transform: { zoom: number; offsetX: number; offsetY: number };
  fitScale: number;
  pointRefs: React.MutableRefObject<{ [key: number]: Konva.Circle | null }>;
  selected?: boolean;
  disabled?: boolean;
  transformMode?: boolean;
  pointRadius?: {
    enabled?: number;
    disabled?: number;
  };
  pointFill?: string;
  pointStroke?: string;
  pointStrokeSelected?: string;
  pointStrokeWidth?: number;
  activePointId?: string | null;
  maxPoints?: number;
  onPointClick?: (e: Konva.KonvaEventObject<MouseEvent>, pointIndex: number) => void;
}
 
export const VectorPoints: React.FC<VectorPointsProps> = ({
  initialPoints,
  selectedPointIndex,
  selectedPoints,
  transform,
  fitScale,
  pointRefs,
  selected = true,
  disabled = false,
  transformMode = false,
  pointRadius,
  pointFill = "#ffffff",
  pointStroke = "#3b82f6",
  pointStrokeSelected = "#ffffff",
  pointStrokeWidth = 2,
  activePointId = null,
  maxPoints,
  onPointClick,
}) => {
  // CRITICAL: For single-point regions, we need to allow clicks even when not selected
  // Single-point regions have no segments to click on, so clicking the point must trigger region selection
  // BUT: Never allow clicks when disabled or in transform mode
  const isSinglePointRegion = initialPoints.length === 1;
  const shouldListenToClicks = !disabled && !transformMode && (selected || isSinglePointRegion);
 
  return (
    <>
      {initialPoints.map((point, index) => {
        // Scale up radius to compensate for Layer scaling
        const scale = transform.zoom * fitScale;
        // Use configurable radius with fallbacks to defaults
        const enabledRadius = pointRadius?.enabled ?? 6;
        const disabledRadius = pointRadius?.disabled ?? 4;
        const baseRadius = selected ? enabledRadius : disabledRadius;
        // Check if maxPoints is reached
        const isMaxPointsReached = maxPoints !== undefined && initialPoints.length >= maxPoints;
        // Check if multiple points are selected
        const isMultiSelection = selectedPoints.size > 1;
        // Point is explicitly selected if it's in selectedPoints or is the selectedPointIndex
        const isExplicitlySelected = selectedPointIndex === index || selectedPoints.has(index);
        // Active point should only be rendered as selected if:
        // - It's explicitly selected, OR
        // - (selected AND maxPoints not reached AND not in multi-selection AND it's the active point)
        const isSelected =
          isExplicitlySelected ||
          (selected &&
            !isMaxPointsReached &&
            !isMultiSelection &&
            activePointId !== null &&
            point.id === activePointId);
        // Make selected points larger
        const radiusMultiplier = isSelected ? 1.3 : 1;
        const scaledRadius = (baseRadius * radiusMultiplier) / scale;
 
        return (
          <>
            {/* White outline ring for selected points - rendered outside the colored stroke */}
            {selected && isSelected && (
              <Circle
                key={`point-outline-${index}-${point.x}-${point.y}`}
                x={point.x}
                y={point.y}
                radius={scaledRadius}
                fill="transparent"
                stroke={pointStrokeSelected}
                strokeScaleEnabled={false}
                strokeWidth={pointStrokeWidth + 5}
                listening={false}
                name={`point-outline-${index}`}
              />
            )}
            {/* Main point circle with colored stroke */}
            <Circle
              key={`point-${index}-${point.x}-${point.y}`}
              ref={(node) => {
                pointRefs.current[index] = node;
              }}
              x={point.x}
              y={point.y}
              radius={scaledRadius}
              fill={pointFill}
              stroke={pointStroke}
              strokeScaleEnabled={false}
              strokeWidth={pointStrokeWidth}
              listening={shouldListenToClicks}
              name={`point-${index}`}
              // Use custom hit function to create a larger clickable area around the point
              // This makes points easier to click even when the cursor is not exactly over the point
              hitFunc={(context, shape) => {
                // Calculate a larger hit radius using the constant (scaled for current zoom)
                const hitRadius = HIT_RADIUS.SELECTION / scale;
                context.beginPath();
                context.arc(0, 0, hitRadius, 0, Math.PI * 2);
                context.fillStrokeShape(shape);
              }}
              onClick={
                onPointClick
                  ? (e) => {
                      // For single-point regions, call onPointClick but don't stop propagation
                      // The onPointClick handler in KonvaVector will directly call handleClickWithDebouncing
                      // to trigger region selection
                      if (isSinglePointRegion && !e.evt.altKey && !e.evt.shiftKey && !e.evt.ctrlKey && !e.evt.metaKey) {
                        // Don't stop propagation - let onPointClick handle it and call onClick directly
                        onPointClick(e, index);
                        return;
                      }
 
                      // Stop propagation immediately to prevent the event from bubbling to VectorShape onClick
                      // This prevents the shape from being selected/unselected when clicking on points
                      e.evt.stopImmediatePropagation();
                      e.evt.stopPropagation();
                      e.evt.preventDefault();
                      e.cancelBubble = true;
                      onPointClick(e, index);
                    }
                  : undefined
              }
            />
          </>
        );
      })}
    </>
  );
};