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