@@ -48,6 +48,7 @@ function clearSketchSelection() {
4848 this . selectedSketchEntities . clear ( ) ;
4949 this . hoveredSketchEntityId = null ;
5050 this . sketchLinePreview = null ;
51+ this . clearSketchMarquee ( ) ;
5152 this . updateSketchInteractionVisuals ( ) ;
5253}
5354
@@ -63,6 +64,10 @@ function handleSketchKeyDown(event) {
6364 }
6465
6566 if ( event . code === 'Escape' ) {
67+ const hadMarquee = ! ! this . sketchMarquee ;
68+ if ( hadMarquee ) {
69+ this . clearSketchMarquee ( ) ;
70+ }
6671 const hadLine = ! ! this . sketchLineStart ;
6772 if ( hadLine ) {
6873 this . cancelSketchLine ( ) ;
@@ -71,7 +76,7 @@ function handleSketchKeyDown(event) {
7176 this . setSketchTool ( 'select' ) ;
7277 return true ;
7378 }
74- return hadLine ;
79+ return hadLine || hadMarquee ;
7580 }
7681
7782 if ( event . code === 'KeyV' ) {
@@ -237,11 +242,47 @@ function handleSketchHover(event, intersections) {
237242 return true ;
238243}
239244
245+ function handleSketchPointerMove ( event ) {
246+ const feature = this . getEditingSketchFeature ( ) ;
247+ if ( ! feature || this . getSketchTool ( ) !== 'select' ) {
248+ return false ;
249+ }
250+ if ( ! this . sketchPointerDown ) {
251+ return false ;
252+ }
253+ if ( ! ( event ?. buttons & 1 ) ) {
254+ return false ;
255+ }
256+ if ( this . sketchDrag ) {
257+ return false ;
258+ }
259+ const offsetMag = this . pointerDistance ( event , this . sketchPointerDown ) ;
260+ if ( offsetMag < SKETCH_DRAG_START_PX ) {
261+ return false ;
262+ }
263+ const downId = this . sketchPointerDown . hitId || this . hoveredSketchEntityId || null ;
264+ if ( downId && downId !== SKETCH_VIRTUAL_ORIGIN_ID ) {
265+ return false ;
266+ }
267+ if ( ! this . sketchMarquee ) {
268+ this . startSketchMarquee ( feature , this . sketchPointerDown , event ) ;
269+ } else {
270+ this . updateSketchMarquee ( event ) ;
271+ }
272+ this . hoveredSketchEntityId = null ;
273+ this . updateSketchInteractionVisuals ( ) ;
274+ return true ;
275+ }
276+
240277function handleSketchMouseUp ( event , intersections ) {
241278 const feature = this . getEditingSketchFeature ( ) ;
242279 if ( ! feature ) {
243280 return false ;
244281 }
282+ if ( this . sketchMarquee ) {
283+ this . finishSketchMarquee ( feature ) ;
284+ return true ;
285+ }
245286
246287 const pointerDown = this . sketchPointerDown ;
247288 const dist = pointerDown ? this . pointerDistance ( event , pointerDown ) : 0 ;
@@ -329,6 +370,10 @@ function handleSketchDrag(delta, offset, isDone) {
329370 }
330371
331372 if ( isDone ) {
373+ if ( this . sketchMarquee ) {
374+ this . finishSketchMarquee ( feature ) ;
375+ return true ;
376+ }
332377 if ( ! this . sketchDrag ) {
333378 return false ;
334379 }
@@ -356,7 +401,10 @@ function handleSketchDrag(delta, offset, isDone) {
356401 }
357402 const downId = this . sketchPointerDown . hitId || this . hoveredSketchEntityId || null ;
358403 if ( ! downId || downId === SKETCH_VIRTUAL_ORIGIN_ID ) {
359- return false ;
404+ this . startSketchMarquee ( feature , this . sketchPointerDown , event ) ;
405+ this . hoveredSketchEntityId = null ;
406+ this . updateSketchInteractionVisuals ( ) ;
407+ return true ;
360408 }
361409 const activeIds = this . selectedSketchEntities . has ( downId )
362410 ? new Set ( this . selectedSketchEntities )
@@ -378,6 +426,11 @@ function handleSketchDrag(delta, offset, isDone) {
378426 this . updateSketchInteractionVisuals ( ) ;
379427 }
380428
429+ if ( this . sketchMarquee ) {
430+ this . updateSketchMarquee ( event ) ;
431+ return true ;
432+ }
433+
381434 const local = this . projectEventToSketchLocal ( event , feature ) ;
382435 if ( ! local ) {
383436 return true ;
@@ -397,6 +450,195 @@ function handleSketchDrag(delta, offset, isDone) {
397450 return true ;
398451}
399452
453+ function startSketchMarquee ( feature , pointerDown , event ) {
454+ const start = this . viewportPointFromClient ( pointerDown ?. clientX , pointerDown ?. clientY ) ;
455+ const end = this . getEventViewportXY ( event ) ;
456+ if ( ! start || ! end ) {
457+ return ;
458+ }
459+ this . sketchMarquee = {
460+ featureId : feature ?. id || null ,
461+ startX : start . x ,
462+ startY : start . y ,
463+ endX : end . x ,
464+ endY : end . y ,
465+ mode : end . x >= start . x ? 'window' : 'cross'
466+ } ;
467+ this . updateSketchMarqueeVisual ( ) ;
468+ }
469+
470+ function updateSketchMarquee ( event ) {
471+ if ( ! this . sketchMarquee ) {
472+ return ;
473+ }
474+ const end = this . getEventViewportXY ( event ) ;
475+ if ( ! end ) {
476+ return ;
477+ }
478+ this . sketchMarquee . endX = end . x ;
479+ this . sketchMarquee . endY = end . y ;
480+ this . sketchMarquee . mode = end . x >= this . sketchMarquee . startX ? 'window' : 'cross' ;
481+ this . updateSketchMarqueeVisual ( ) ;
482+ }
483+
484+ function finishSketchMarquee ( feature ) {
485+ if ( ! this . sketchMarquee ) {
486+ return ;
487+ }
488+ const marquee = this . sketchMarquee ;
489+ this . clearSketchMarquee ( ) ;
490+ const selectIds = this . selectSketchEntitiesInMarquee ( feature , marquee ) ;
491+ this . selectedSketchEntities = new Set ( selectIds ) ;
492+ this . hoveredSketchEntityId = null ;
493+ this . updateSketchInteractionVisuals ( ) ;
494+ }
495+
496+ function clearSketchMarquee ( ) {
497+ this . sketchMarquee = null ;
498+ if ( this . sketchMarqueeEl ?. parentElement ) {
499+ this . sketchMarqueeEl . parentElement . removeChild ( this . sketchMarqueeEl ) ;
500+ }
501+ this . sketchMarqueeEl = null ;
502+ }
503+
504+ function updateSketchMarqueeVisual ( ) {
505+ const marquee = this . sketchMarquee ;
506+ if ( ! marquee ) {
507+ this . clearSketchMarquee ( ) ;
508+ return ;
509+ }
510+ const { container } = space . internals ( ) ;
511+ if ( ! container ) {
512+ return ;
513+ }
514+ if ( ! this . sketchMarqueeEl ) {
515+ const el = document . createElement ( 'div' ) ;
516+ el . className = 'sketch-marquee sketch-marquee-window' ;
517+ container . appendChild ( el ) ;
518+ this . sketchMarqueeEl = el ;
519+ }
520+ const left = Math . min ( marquee . startX , marquee . endX ) ;
521+ const top = Math . min ( marquee . startY , marquee . endY ) ;
522+ const width = Math . abs ( marquee . endX - marquee . startX ) ;
523+ const height = Math . abs ( marquee . endY - marquee . startY ) ;
524+ this . sketchMarqueeEl . className = `sketch-marquee ${ marquee . mode === 'cross' ? 'sketch-marquee-cross' : 'sketch-marquee-window' } ` ;
525+ this . sketchMarqueeEl . style . left = `${ left } px` ;
526+ this . sketchMarqueeEl . style . top = `${ top } px` ;
527+ this . sketchMarqueeEl . style . width = `${ width } px` ;
528+ this . sketchMarqueeEl . style . height = `${ height } px` ;
529+ }
530+
531+ function viewportPointFromClient ( clientX , clientY ) {
532+ const { container } = space . internals ( ) ;
533+ if ( ! container ) {
534+ return null ;
535+ }
536+ const rect = container . getBoundingClientRect ( ) ;
537+ return {
538+ x : ( clientX || 0 ) - rect . left ,
539+ y : ( clientY || 0 ) - rect . top
540+ } ;
541+ }
542+
543+ function selectSketchEntitiesInMarquee ( feature , marquee ) {
544+ const entities = Array . isArray ( feature ?. entities ) ? feature . entities : [ ] ;
545+ const basis = this . getSketchBasis ( feature ) ;
546+ if ( ! basis ) {
547+ return [ ] ;
548+ }
549+ const minX = Math . min ( marquee . startX , marquee . endX ) ;
550+ const maxX = Math . max ( marquee . startX , marquee . endX ) ;
551+ const minY = Math . min ( marquee . startY , marquee . endY ) ;
552+ const maxY = Math . max ( marquee . startY , marquee . endY ) ;
553+ const rect = { minX, maxX, minY, maxY } ;
554+ const isWindow = marquee . mode !== 'cross' ;
555+
556+ const out = [ ] ;
557+ const pointById = new Map ( ) ;
558+ for ( const entity of entities ) {
559+ if ( entity ?. type === 'point' && entity . id ) {
560+ pointById . set ( entity . id , entity ) ;
561+ }
562+ }
563+ for ( const entity of entities ) {
564+ if ( ! entity ?. id ) continue ;
565+ if ( entity . type === 'point' ) {
566+ const p = this . projectSketchLocalToScreen ( { x : entity . x || 0 , y : entity . y || 0 } , basis ) ;
567+ if ( ! p ) continue ;
568+ if ( this . isPointInRect ( p . x , p . y , rect ) ) {
569+ out . push ( entity . id ) ;
570+ }
571+ continue ;
572+ }
573+ if ( entity . type === 'line' ) {
574+ const [ a , b ] = this . getLineEndpoints ( entity , pointById ) ;
575+ if ( ! a || ! b ) continue ;
576+ const pa = this . projectSketchLocalToScreen ( { x : a . x || 0 , y : a . y || 0 } , basis ) ;
577+ const pb = this . projectSketchLocalToScreen ( { x : b . x || 0 , y : b . y || 0 } , basis ) ;
578+ if ( ! pa || ! pb ) continue ;
579+ const hit = isWindow
580+ ? ( this . isPointInRect ( pa . x , pa . y , rect ) && this . isPointInRect ( pb . x , pb . y , rect ) )
581+ : this . segmentTouchesRect ( pa , pb , rect ) ;
582+ if ( hit ) {
583+ out . push ( entity . id ) ;
584+ }
585+ }
586+ }
587+ return out ;
588+ }
589+
590+ function projectSketchLocalToScreen ( local , basis ) {
591+ const world = this . sketchLocalToWorld ( local , basis ) ;
592+ const proj = api . overlay . project3Dto2D ( world ) ;
593+ if ( ! proj ?. visible ) {
594+ return null ;
595+ }
596+ return { x : proj . x , y : proj . y } ;
597+ }
598+
599+ function isPointInRect ( x , y , rect ) {
600+ return x >= rect . minX && x <= rect . maxX && y >= rect . minY && y <= rect . maxY ;
601+ }
602+
603+ function segmentTouchesRect ( a , b , rect ) {
604+ if ( this . isPointInRect ( a . x , a . y , rect ) || this . isPointInRect ( b . x , b . y , rect ) ) {
605+ return true ;
606+ }
607+ const edges = [
608+ [ { x : rect . minX , y : rect . minY } , { x : rect . maxX , y : rect . minY } ] ,
609+ [ { x : rect . maxX , y : rect . minY } , { x : rect . maxX , y : rect . maxY } ] ,
610+ [ { x : rect . maxX , y : rect . maxY } , { x : rect . minX , y : rect . maxY } ] ,
611+ [ { x : rect . minX , y : rect . maxY } , { x : rect . minX , y : rect . minY } ]
612+ ] ;
613+ for ( const [ c , d ] of edges ) {
614+ if ( this . segmentsIntersect ( a , b , c , d ) ) {
615+ return true ;
616+ }
617+ }
618+ return false ;
619+ }
620+
621+ function segmentsIntersect ( a , b , c , d ) {
622+ const orient = ( p , q , r ) => ( q . x - p . x ) * ( r . y - p . y ) - ( q . y - p . y ) * ( r . x - p . x ) ;
623+ const onSeg = ( p , q , r ) =>
624+ Math . min ( p . x , r . x ) <= q . x && q . x <= Math . max ( p . x , r . x ) &&
625+ Math . min ( p . y , r . y ) <= q . y && q . y <= Math . max ( p . y , r . y ) ;
626+
627+ const o1 = orient ( a , b , c ) ;
628+ const o2 = orient ( a , b , d ) ;
629+ const o3 = orient ( c , d , a ) ;
630+ const o4 = orient ( c , d , b ) ;
631+
632+ if ( ( o1 > 0 ) !== ( o2 > 0 ) && ( o3 > 0 ) !== ( o4 > 0 ) ) {
633+ return true ;
634+ }
635+ if ( Math . abs ( o1 ) < 1e-9 && onSeg ( a , c , b ) ) return true ;
636+ if ( Math . abs ( o2 ) < 1e-9 && onSeg ( a , d , b ) ) return true ;
637+ if ( Math . abs ( o3 ) < 1e-9 && onSeg ( c , a , d ) ) return true ;
638+ if ( Math . abs ( o4 ) < 1e-9 && onSeg ( c , b , d ) ) return true ;
639+ return false ;
640+ }
641+
400642function collectSelectedCoordinateRefs ( feature ) {
401643 return this . collectCoordinateRefsFromIds ( feature , this . selectedSketchEntities ) ;
402644}
@@ -796,6 +1038,7 @@ export {
7961038 handleSketchKeyDown ,
7971039 handleSketchPointerDown ,
7981040 handleSketchHover ,
1041+ handleSketchPointerMove ,
7991042 handleSketchMouseUp ,
8001043 handleSketchDrag ,
8011044 toggleSelectedConstruction ,
@@ -811,6 +1054,17 @@ export {
8111054 getSketchBasis ,
8121055 sketchLocalToWorld ,
8131056 projectEventToSketchLocal ,
1057+ viewportPointFromClient ,
1058+ startSketchMarquee ,
1059+ updateSketchMarquee ,
1060+ finishSketchMarquee ,
1061+ clearSketchMarquee ,
1062+ updateSketchMarqueeVisual ,
1063+ selectSketchEntitiesInMarquee ,
1064+ projectSketchLocalToScreen ,
1065+ isPointInRect ,
1066+ segmentTouchesRect ,
1067+ segmentsIntersect ,
8141068 collectSelectedCoordinateRefs ,
8151069 collectCoordinateRefsFromIds ,
8161070 createSketchPoint ,
0 commit comments