11// /* global window */
22import React , { useEffect , useRef , useState } from "react" ;
33import { Wrapper , CanvasContainer , OutputBox , StyledSVG , CopyButton } from "./shapeBuilder.styles" ;
4- import { Button , Typography , Box , CopyIcon } from "@sistent/sistent" ;
4+ import { Button , Typography , Box , CopyIcon , Select , MenuItem , Slider , FormControl } from "@sistent/sistent" ;
55import { SVG , extend as SVGextend } from "@svgdotjs/svg.js" ;
66import draw from "@svgdotjs/svg.draw.js" ;
77
88SVGextend ( SVG . Polygon , draw ) ;
99
10+ const SCALE_PRESETS = [ 0.25 , 0.5 , 0.75 , 1 , 1.25 , 1.5 , 2 , 3 ] ;
11+ const MIN_SCALE = 0.1 ;
12+ const MAX_SCALE = 3 ;
13+
1014const ShapeBuilder = ( ) => {
1115 const boardRef = useRef ( null ) ;
1216 const polyRef = useRef ( null ) ;
1317 const keyHandlersRef = useRef ( { } ) ;
18+ const basePointsRef = useRef ( null ) ;
1419 const [ result , setResult ] = useState ( "" ) ;
1520 const [ error , setError ] = useState ( null ) ;
1621 const [ showCopied , setShowCopied ] = useState ( false ) ;
22+ const [ scale , setScale ] = useState ( 1 ) ;
23+ const [ currentPreset , setCurrentPreset ] = useState ( 1 ) ;
1724
1825 const handleCopyToClipboard = async ( ) => {
1926 if ( ! result . trim ( ) ) return ;
@@ -54,23 +61,55 @@ const ShapeBuilder = () => {
5461 }
5562 } ;
5663
57- const handleMaximize = ( ) => {
64+ const applyScale = ( newScale ) => {
5865 const poly = polyRef . current ;
5966 if ( ! poly ) return ;
6067
6168 const points = getPlottedPoints ( poly ) ;
62- if ( ! points ) return ;
63- const xs = points . map ( p => p [ 0 ] ) ;
64- const ys = points . map ( p => p [ 1 ] ) ;
69+ if ( ! points || points . length === 0 ) return ;
70+
71+ if ( ! basePointsRef . current ) {
72+ basePointsRef . current = points ;
73+ }
74+
75+ const basePoints = basePointsRef . current ;
76+
77+ const xs = basePoints . map ( p => p [ 0 ] ) ;
78+ const ys = basePoints . map ( p => p [ 1 ] ) ;
79+ const centerX = ( Math . max ( ...xs ) + Math . min ( ...xs ) ) / 2 ;
80+ const centerY = ( Math . max ( ...ys ) + Math . min ( ...ys ) ) / 2 ;
6581
66- const width = Math . abs ( Math . max ( ...xs ) - Math . min ( ...xs ) ) ;
67- const height = Math . abs ( Math . max ( ...ys ) - Math . min ( ...ys ) ) ;
82+ const scaledPoints = basePoints . map ( ( [ x , y ] ) => {
83+ const dx = x - centerX ;
84+ const dy = y - centerY ;
85+ return [ centerX + dx * newScale , centerY + dy * newScale ] ;
86+ } ) ;
6887
69- poly . size ( width > height ? 520 : undefined , height >= width ? 520 : undefined ) ;
70- poly . move ( 0 , 0 ) ;
88+ poly . plot ( scaledPoints ) ;
7189 showCytoArray ( ) ;
7290 } ;
7391
92+ const handleScaleChange = ( newScale ) => {
93+ const clampedScale = Math . max ( MIN_SCALE , Math . min ( MAX_SCALE , newScale ) ) ;
94+ setScale ( clampedScale ) ;
95+
96+ const matchingPreset = SCALE_PRESETS . find ( p => Math . abs ( p - clampedScale ) < 0.01 ) ;
97+ setCurrentPreset ( matchingPreset || clampedScale ) ;
98+
99+ applyScale ( clampedScale ) ;
100+ } ;
101+
102+ const handlePresetChange = ( event ) => {
103+ const newPreset = event . target . value ;
104+ setCurrentPreset ( newPreset ) ;
105+ setScale ( newPreset ) ;
106+ applyScale ( newPreset ) ;
107+ } ;
108+
109+ const handleSliderChange = ( event , newValue ) => {
110+ handleScaleChange ( newValue ) ;
111+ } ;
112+
74113 const handleKeyDown = ( e ) => {
75114 const poly = polyRef . current ;
76115 if ( ! poly ) return ;
@@ -144,7 +183,10 @@ const ShapeBuilder = () => {
144183 poly . remove ( ) ;
145184 detachKeyListeners ( ) ;
146185 polyRef . current = null ;
186+ basePointsRef . current = null ;
147187 setResult ( "" ) ;
188+ setScale ( 1 ) ;
189+ setCurrentPreset ( 1 ) ;
148190 initializeDrawing ( ) ;
149191 } ;
150192
@@ -154,6 +196,10 @@ const ShapeBuilder = () => {
154196
155197 poly . draw ( "done" ) ;
156198 poly . fill ( "#00B39F" ) ;
199+ const points = getPlottedPoints ( poly ) ;
200+ if ( points && points . length > 0 ) {
201+ basePointsRef . current = points ;
202+ }
157203 showCytoArray ( ) ;
158204 } ;
159205
@@ -201,10 +247,52 @@ const ShapeBuilder = () => {
201247 ) }
202248 </ CanvasContainer >
203249
204- < Box sx = { { display : "flex" , justifyContent : "center" , gap : 2 , mt : 3 , mb : 3 , flexWrap : "wrap" } } >
250+ < Box sx = { { display : "flex" , justifyContent : "center" , alignItems : "center" , gap : 2 , mt : 3 , mb : 3 , flexWrap : "wrap" } } >
205251 < Button variant = "contained" onClick = { clearShape } > Clear</ Button >
206252 < Button variant = "contained" onClick = { closeShape } > Close Shape</ Button >
207- < Button variant = "contained" onClick = { handleMaximize } > Maximize</ Button >
253+
254+ < Box sx = { { display : "flex" , alignItems : "center" , gap : 1.5 , ml : 2 } } >
255+ < FormControl size = "small" sx = { { minWidth : 80 } } >
256+ < Select
257+ id = "scale-preset-select"
258+ value = { currentPreset }
259+ onChange = { handlePresetChange }
260+ displayEmpty
261+ aria-label = "Scale preset"
262+ sx = { {
263+ color : '#fff' ,
264+ '& .MuiSelect-icon' : {
265+ color : '#fff'
266+ }
267+ } }
268+ >
269+ { SCALE_PRESETS . map ( ( preset ) => (
270+ < MenuItem key = { preset } value = { preset } >
271+ { preset } ×
272+ </ MenuItem >
273+ ) ) }
274+ </ Select >
275+ </ FormControl >
276+
277+ < Box sx = { { width : 150 , display : "flex" , alignItems : "center" , gap : 1 } } >
278+ < Slider
279+ value = { scale }
280+ onChange = { handleSliderChange }
281+ min = { MIN_SCALE }
282+ max = { MAX_SCALE }
283+ step = { 0.01 }
284+ valueLabelDisplay = "auto"
285+ valueLabelFormat = { ( value ) => `${ value . toFixed ( 2 ) } ×` }
286+ marks = { SCALE_PRESETS . map ( value => ( { value, label : "" } ) ) }
287+ aria-label = "Scale slider"
288+ sx = { { flexGrow : 1 } }
289+ />
290+ </ Box >
291+
292+ < Typography variant = "body2" sx = { { minWidth : "50px" , fontWeight : 500 } } >
293+ { scale . toFixed ( 2 ) } ×
294+ </ Typography >
295+ </ Box >
208296 </ Box >
209297
210298 < OutputBox >
0 commit comments