2024-02-11 12:59:00 +11:00
import { Coords2d } from 'lang/std/sketch'
import {
2024-03-02 08:48:30 +11:00
BoxGeometry ,
2024-02-11 12:59:00 +11:00
BufferGeometry ,
CatmullRomCurve3 ,
ConeGeometry ,
CurvePath ,
EllipseCurve ,
ExtrudeGeometry ,
Group ,
LineCurve3 ,
2025-01-16 11:10:36 -05:00
LineBasicMaterial ,
LineDashedMaterial ,
Line ,
2024-02-11 12:59:00 +11:00
Mesh ,
MeshBasicMaterial ,
NormalBufferAttributes ,
2024-04-03 13:22:56 +11:00
Points ,
PointsMaterial ,
2024-02-11 12:59:00 +11:00
Shape ,
SphereGeometry ,
2024-04-03 13:22:56 +11:00
Texture ,
2024-02-11 12:59:00 +11:00
Vector2 ,
Vector3 ,
} from 'three'
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
2024-07-08 16:41:00 -04:00
import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer'
2024-09-27 15:44:44 -07:00
import { PathToNode , Sketch , getTangentialArcToInfo } from 'lang/wasm'
2024-02-11 12:59:00 +11:00
import {
2024-09-23 22:42:51 +10:00
CIRCLE_CENTER_HANDLE ,
CIRCLE_SEGMENT ,
CIRCLE_SEGMENT_BODY ,
CIRCLE_SEGMENT_DASH ,
2025-01-31 21:26:19 +11:00
CIRCLE_THREE_POINT_HANDLE1 ,
CIRCLE_THREE_POINT_HANDLE2 ,
CIRCLE_THREE_POINT_HANDLE3 ,
CIRCLE_THREE_POINT_SEGMENT ,
CIRCLE_THREE_POINT_SEGMENT_BODY ,
CIRCLE_THREE_POINT_SEGMENT_DASH ,
2024-04-03 13:22:56 +11:00
EXTRA_SEGMENT_HANDLE ,
EXTRA_SEGMENT_OFFSET_PX ,
2024-09-13 21:14:14 +10:00
HIDE_HOVER_SEGMENT_LENGTH ,
2024-04-04 11:07:51 +11:00
HIDE_SEGMENT_LENGTH ,
2024-03-02 08:48:30 +11:00
PROFILE_START ,
2024-04-03 13:22:56 +11:00
SEGMENT_WIDTH_PX ,
2024-02-11 12:59:00 +11:00
STRAIGHT_SEGMENT ,
STRAIGHT_SEGMENT_BODY ,
STRAIGHT_SEGMENT_DASH ,
TANGENTIAL_ARC_TO_SEGMENT ,
TANGENTIAL_ARC_TO_SEGMENT_BODY ,
TANGENTIAL_ARC_TO__SEGMENT_DASH ,
2024-09-13 21:14:14 +10:00
getParentGroup ,
2024-02-14 08:03:20 +11:00
} from './sceneEntities'
2024-02-11 12:59:00 +11:00
import { getTangentPointFromPreviousArc } from 'lib/utils2d'
2024-07-08 16:41:00 -04:00
import {
ARROWHEAD ,
2024-10-31 07:04:38 -07:00
DRAFT_POINT ,
2024-09-13 21:14:14 +10:00
SceneInfra ,
2024-07-08 16:41:00 -04:00
SEGMENT_LENGTH_LABEL ,
SEGMENT_LENGTH_LABEL_OFFSET_PX ,
SEGMENT_LENGTH_LABEL_TEXT ,
} from './sceneInfra'
2024-05-09 08:38:42 -04:00
import { Themes , getThemeColorForThreeJs } from 'lib/theme'
2024-09-13 21:14:14 +10:00
import { normaliseAngle , roundOff } from 'lib/utils'
2025-02-03 17:53:08 +11:00
import {
SegmentOverlay ,
SegmentOverlayPayload ,
SegmentOverlays ,
} from 'machines/modelingMachine'
2024-09-13 21:14:14 +10:00
import { SegmentInputs } from 'lang/std/stdTypes'
import { err } from 'lib/trap'
2025-02-07 12:59:19 +11:00
import { sceneInfra } from 'lib/singletons'
2024-12-17 15:12:18 -05:00
import { Selections } from 'lib/selections'
2025-01-31 21:26:19 +11:00
import { calculate_circle_from_3_points } from 'wasm-lib/pkg/wasm_lib'
2025-01-23 10:25:21 -05:00
import { commandBarActor } from 'machines/commandBarMachine'
2024-02-11 12:59:00 +11:00
2024-09-13 21:14:14 +10:00
interface CreateSegmentArgs {
input : SegmentInputs
2024-10-23 12:42:54 -05:00
prevSegment : Sketch [ 'paths' ] [ number ]
2024-03-02 08:48:30 +11:00
id : string
pathToNode : PathToNode
2024-09-13 21:14:14 +10:00
isDraftSegment? : boolean
2024-03-02 08:48:30 +11:00
scale? : number
2024-09-13 21:14:14 +10:00
callExpName : string
texture : Texture
2024-05-09 08:38:42 -04:00
theme : Themes
2024-06-22 04:49:31 -04:00
isSelected? : boolean
2024-09-13 21:14:14 +10:00
sceneInfra : SceneInfra
2024-12-17 15:12:18 -05:00
selection? : Selections
2024-09-13 21:14:14 +10:00
}
2024-03-02 08:48:30 +11:00
2024-09-13 21:14:14 +10:00
interface UpdateSegmentArgs {
input : SegmentInputs
2024-10-23 12:42:54 -05:00
prevSegment : Sketch [ 'paths' ] [ number ]
2024-09-13 21:14:14 +10:00
group : Group
sceneInfra : SceneInfra
scale? : number
}
2024-03-02 08:48:30 +11:00
2024-09-13 21:14:14 +10:00
interface CreateSegmentResult {
group : Group
updateOverlaysCallback : ( ) = > SegmentOverlayPayload | null
}
2024-03-02 08:48:30 +11:00
2024-09-13 21:14:14 +10:00
export interface SegmentUtils {
/ * *
* the init is responsible for adding all of the correct entities to the group with important details like ` mesh.name = ... `
* as these act like handles later
*
* It ' s * * Not * * responsible for doing all calculations to size and position the entities as this would be duplicated in the update function
* Which should instead be called at the end of the init function
* /
init : ( args : CreateSegmentArgs ) = > CreateSegmentResult | Error
/ * *
* The update function is responsible for updating the group with the correct size and position of the entities
* It should be called at the end of the init function and return a callback that can be used to update the overlay
*
* It returns a callback for updating the overlays , this is so the overlays do not have to update at the same pace threeJs does
* This is useful for performance reasons
* /
update : (
args : UpdateSegmentArgs
) = > CreateSegmentResult [ 'updateOverlaysCallback' ] | Error
}
class StraightSegment implements SegmentUtils {
init : SegmentUtils [ 'init' ] = ( {
input ,
2024-03-02 08:48:30 +11:00
id ,
pathToNode ,
2024-09-13 21:14:14 +10:00
isDraftSegment ,
scale = 1 ,
callExpName ,
texture ,
theme ,
isSelected = false ,
sceneInfra ,
prevSegment ,
2024-12-17 15:12:18 -05:00
selection ,
2024-09-13 21:14:14 +10:00
} ) = > {
if ( input . type !== 'straight-segment' )
return new Error ( 'Invalid segment type' )
const { from , to } = input
const baseColor =
callExpName === 'close' ? 0x444444 : getThemeColorForThreeJs ( theme )
const color = isSelected ? 0x0000ff : baseColor
const meshType = isDraftSegment
? STRAIGHT_SEGMENT_DASH
: STRAIGHT_SEGMENT_BODY
const segmentGroup = new Group ( )
const shape = new Shape ( )
const line = new LineCurve3 (
new Vector3 ( from [ 0 ] , from [ 1 ] , 0 ) ,
new Vector3 ( to [ 0 ] , to [ 1 ] , 0 )
)
const geometry = new ExtrudeGeometry ( shape , {
steps : 2 ,
bevelEnabled : false ,
extrudePath : line ,
} )
const body = new MeshBasicMaterial ( { color } )
const mesh = new Mesh ( geometry , body )
mesh . userData . type = meshType
mesh . name = meshType
segmentGroup . name = STRAIGHT_SEGMENT
segmentGroup . userData = {
type : STRAIGHT_SEGMENT ,
2024-10-25 15:34:53 -04:00
draft : isDraftSegment ,
2024-09-13 21:14:14 +10:00
id ,
from ,
to ,
pathToNode ,
isSelected ,
callExpName ,
baseColor ,
2024-12-17 15:12:18 -05:00
selection ,
2024-09-13 21:14:14 +10:00
}
// All segment types get an extra segment handle,
// Which is a little plus sign that appears at the origin of the segment
// and can be dragged to insert a new segment
const extraSegmentGroup = createExtraSegmentHandle ( scale , texture , theme )
// Segment decorators that only apply to non-close segments
if ( callExpName !== 'close' ) {
// an arrowhead that appears at the end of the segment
const arrowGroup = createArrowhead ( scale , theme , color )
// A length indicator that appears at the midpoint of the segment
const lengthIndicatorGroup = createLengthIndicator ( {
from ,
to ,
scale ,
} )
segmentGroup . add ( arrowGroup )
segmentGroup . add ( lengthIndicatorGroup )
}
segmentGroup . add ( mesh , extraSegmentGroup )
let updateOverlaysCallback = this . update ( {
prevSegment ,
input ,
group : segmentGroup ,
scale ,
sceneInfra ,
} )
if ( err ( updateOverlaysCallback ) ) return updateOverlaysCallback
return {
group : segmentGroup ,
updateOverlaysCallback ,
}
}
update : SegmentUtils [ 'update' ] = ( {
input ,
group ,
scale = 1 ,
sceneInfra ,
} ) = > {
if ( input . type !== 'straight-segment' )
return new Error ( 'Invalid segment type' )
const { from , to } = input
group . userData . from = from
group . userData . to = to
const shape = new Shape ( )
shape . moveTo ( 0 , ( - SEGMENT_WIDTH_PX / 2 ) * scale ) // The width of the line in px (2.4px in this case)
shape . lineTo ( 0 , ( SEGMENT_WIDTH_PX / 2 ) * scale )
const arrowGroup = group . getObjectByName ( ARROWHEAD ) as Group
const labelGroup = group . getObjectByName ( SEGMENT_LENGTH_LABEL ) as Group
const length = Math . sqrt (
Math . pow ( to [ 0 ] - from [ 0 ] , 2 ) + Math . pow ( to [ 1 ] - from [ 1 ] , 2 )
)
const pxLength = length / scale
const shouldHideIdle = pxLength < HIDE_SEGMENT_LENGTH
const shouldHideHover = pxLength < HIDE_HOVER_SEGMENT_LENGTH
const hoveredParent =
sceneInfra . hoveredObject &&
getParentGroup ( sceneInfra . hoveredObject , [ STRAIGHT_SEGMENT ] )
let isHandlesVisible = ! shouldHideIdle
if ( hoveredParent && hoveredParent ? . uuid === group ? . uuid ) {
isHandlesVisible = ! shouldHideHover
}
if ( arrowGroup ) {
arrowGroup . position . set ( to [ 0 ] , to [ 1 ] , 0 )
const dir = new Vector3 ( )
. subVectors (
new Vector3 ( to [ 0 ] , to [ 1 ] , 0 ) ,
new Vector3 ( from [ 0 ] , from [ 1 ] , 0 )
)
. normalize ( )
arrowGroup . quaternion . setFromUnitVectors ( new Vector3 ( 0 , 1 , 0 ) , dir )
arrowGroup . scale . set ( scale , scale , scale )
arrowGroup . visible = isHandlesVisible
}
const extraSegmentGroup = group . getObjectByName ( EXTRA_SEGMENT_HANDLE )
if ( extraSegmentGroup ) {
const offsetFromBase = new Vector2 ( to [ 0 ] - from [ 0 ] , to [ 1 ] - from [ 1 ] )
. normalize ( )
. multiplyScalar ( EXTRA_SEGMENT_OFFSET_PX * scale )
extraSegmentGroup . position . set (
from [ 0 ] + offsetFromBase . x ,
from [ 1 ] + offsetFromBase . y ,
0
)
extraSegmentGroup . scale . set ( scale , scale , scale )
extraSegmentGroup . visible = isHandlesVisible
}
if ( labelGroup ) {
const labelWrapper = labelGroup . getObjectByName (
SEGMENT_LENGTH_LABEL_TEXT
) as CSS2DObject
const labelWrapperElem = labelWrapper . element as HTMLDivElement
const label = labelWrapperElem . children [ 0 ] as HTMLParagraphElement
label . innerText = ` ${ roundOff ( length ) } `
label . classList . add ( SEGMENT_LENGTH_LABEL_TEXT )
const slope = ( to [ 1 ] - from [ 1 ] ) / ( to [ 0 ] - from [ 0 ] )
let slopeAngle = ( ( Math . atan ( slope ) * 180 ) / Math . PI ) * - 1
label . style . setProperty ( '--degree' , ` ${ slopeAngle } deg ` )
label . style . setProperty ( '--x' , ` 0px ` )
label . style . setProperty ( '--y' , ` 0px ` )
labelWrapper . position . set ( ( from [ 0 ] + to [ 0 ] ) / 2 , ( from [ 1 ] + to [ 1 ] ) / 2 , 0 )
labelGroup . visible = isHandlesVisible
}
const straightSegmentBody = group . children . find (
( child ) = > child . userData . type === STRAIGHT_SEGMENT_BODY
) as Mesh
if ( straightSegmentBody ) {
const line = new LineCurve3 (
new Vector3 ( from [ 0 ] , from [ 1 ] , 0 ) ,
new Vector3 ( to [ 0 ] , to [ 1 ] , 0 )
)
straightSegmentBody . geometry = new ExtrudeGeometry ( shape , {
steps : 2 ,
bevelEnabled : false ,
extrudePath : line ,
} )
}
const straightSegmentBodyDashed = group . children . find (
( child ) = > child . userData . type === STRAIGHT_SEGMENT_DASH
) as Mesh
if ( straightSegmentBodyDashed ) {
straightSegmentBodyDashed . geometry = dashedStraight (
from ,
to ,
shape ,
scale
)
}
return ( ) = >
sceneInfra . updateOverlayDetails ( {
2025-02-03 17:53:08 +11:00
handle : arrowGroup ,
2024-09-13 21:14:14 +10:00
group ,
isHandlesVisible ,
from ,
to ,
2025-02-03 17:53:08 +11:00
hasThreeDotMenu : true ,
2024-09-13 21:14:14 +10:00
} )
}
}
class TangentialArcToSegment implements SegmentUtils {
init : SegmentUtils [ 'init' ] = ( {
prevSegment ,
input ,
id ,
pathToNode ,
isDraftSegment ,
scale = 1 ,
texture ,
theme ,
2024-06-22 04:49:31 -04:00
isSelected ,
2024-09-13 21:14:14 +10:00
sceneInfra ,
} ) = > {
if ( input . type !== 'straight-segment' )
return new Error ( 'Invalid segment type' )
const { from , to } = input
const meshName = isDraftSegment
? TANGENTIAL_ARC_TO__SEGMENT_DASH
: TANGENTIAL_ARC_TO_SEGMENT_BODY
const group = new Group ( )
const geometry = createArcGeometry ( {
center : [ 0 , 0 ] ,
radius : 1 ,
startAngle : 0 ,
endAngle : 1 ,
ccw : true ,
isDashed : isDraftSegment ,
scale ,
} )
const baseColor = getThemeColorForThreeJs ( theme )
const color = isSelected ? 0x0000ff : baseColor
const body = new MeshBasicMaterial ( { color } )
const mesh = new Mesh ( geometry , body )
const arrowGroup = createArrowhead ( scale , theme , color )
const extraSegmentGroup = createExtraSegmentHandle ( scale , texture , theme )
group . name = TANGENTIAL_ARC_TO_SEGMENT
mesh . userData . type = meshName
mesh . name = meshName
group . userData = {
type : TANGENTIAL_ARC_TO_SEGMENT ,
2024-10-25 15:34:53 -04:00
draft : isDraftSegment ,
2024-09-13 21:14:14 +10:00
id ,
from ,
to ,
prevSegment ,
pathToNode ,
isSelected ,
baseColor ,
}
group . add ( mesh , arrowGroup , extraSegmentGroup )
const updateOverlaysCallback = this . update ( {
prevSegment ,
input ,
group ,
scale ,
sceneInfra ,
} )
if ( err ( updateOverlaysCallback ) ) return updateOverlaysCallback
return {
group ,
updateOverlaysCallback ,
}
}
update : SegmentUtils [ 'update' ] = ( {
prevSegment ,
input ,
group ,
scale = 1 ,
sceneInfra ,
} ) = > {
if ( input . type !== 'straight-segment' )
return new Error ( 'Invalid segment type' )
const { from , to } = input
group . userData . from = from
group . userData . to = to
group . userData . prevSegment = prevSegment
const arrowGroup = group . getObjectByName ( ARROWHEAD ) as Group
const extraSegmentGroup = group . getObjectByName ( EXTRA_SEGMENT_HANDLE )
const previousPoint =
prevSegment ? . type === 'TangentialArcTo'
? getTangentPointFromPreviousArc (
prevSegment . center ,
prevSegment . ccw ,
prevSegment . to
)
: prevSegment . from
const arcInfo = getTangentialArcToInfo ( {
arcStartPoint : from ,
arcEndPoint : to ,
tanPreviousPoint : previousPoint ,
obtuse : true ,
} )
const pxLength = arcInfo . arcLength / scale
const shouldHideIdle = pxLength < HIDE_SEGMENT_LENGTH
const shouldHideHover = pxLength < HIDE_HOVER_SEGMENT_LENGTH
const hoveredParent =
sceneInfra ? . hoveredObject &&
getParentGroup ( sceneInfra . hoveredObject , [ TANGENTIAL_ARC_TO_SEGMENT ] )
let isHandlesVisible = ! shouldHideIdle
if ( hoveredParent && hoveredParent ? . uuid === group ? . uuid ) {
isHandlesVisible = ! shouldHideHover
}
if ( arrowGroup ) {
arrowGroup . position . set ( to [ 0 ] , to [ 1 ] , 0 )
const arrowheadAngle =
arcInfo . endAngle + ( Math . PI / 2 ) * ( arcInfo . ccw ? 1 : - 1 )
arrowGroup . quaternion . setFromUnitVectors (
new Vector3 ( 0 , 1 , 0 ) ,
new Vector3 ( Math . cos ( arrowheadAngle ) , Math . sin ( arrowheadAngle ) , 0 )
)
arrowGroup . scale . set ( scale , scale , scale )
arrowGroup . visible = isHandlesVisible
}
if ( extraSegmentGroup ) {
const circumferenceInPx = ( 2 * Math . PI * arcInfo . radius ) / scale
const extraSegmentAngleDelta =
( EXTRA_SEGMENT_OFFSET_PX / circumferenceInPx ) * Math . PI * 2
const extraSegmentAngle =
arcInfo . startAngle + ( arcInfo . ccw ? 1 : - 1 ) * extraSegmentAngleDelta
const extraSegmentOffset = new Vector2 (
Math . cos ( extraSegmentAngle ) * arcInfo . radius ,
Math . sin ( extraSegmentAngle ) * arcInfo . radius
)
extraSegmentGroup . position . set (
arcInfo . center [ 0 ] + extraSegmentOffset . x ,
arcInfo . center [ 1 ] + extraSegmentOffset . y ,
0
)
extraSegmentGroup . scale . set ( scale , scale , scale )
extraSegmentGroup . visible = isHandlesVisible
}
const tangentialArcToSegmentBody = group . children . find (
( child ) = > child . userData . type === TANGENTIAL_ARC_TO_SEGMENT_BODY
) as Mesh
if ( tangentialArcToSegmentBody ) {
const newGeo = createArcGeometry ( { . . . arcInfo , scale } )
tangentialArcToSegmentBody . geometry = newGeo
}
const tangentialArcToSegmentBodyDashed = group . getObjectByName (
TANGENTIAL_ARC_TO__SEGMENT_DASH
)
if ( tangentialArcToSegmentBodyDashed instanceof Mesh ) {
tangentialArcToSegmentBodyDashed . geometry = createArcGeometry ( {
. . . arcInfo ,
isDashed : true ,
scale ,
} )
}
const angle = normaliseAngle (
( arcInfo . endAngle * 180 ) / Math . PI + ( arcInfo . ccw ? 90 : - 90 )
)
return ( ) = >
sceneInfra . updateOverlayDetails ( {
2025-02-03 17:53:08 +11:00
handle : arrowGroup ,
2024-09-13 21:14:14 +10:00
group ,
isHandlesVisible ,
from ,
to ,
angle ,
2025-02-03 17:53:08 +11:00
hasThreeDotMenu : true ,
2024-09-13 21:14:14 +10:00
} )
2024-03-02 08:48:30 +11:00
}
}
2024-09-23 22:42:51 +10:00
class CircleSegment implements SegmentUtils {
init : SegmentUtils [ 'init' ] = ( {
prevSegment ,
input ,
id ,
pathToNode ,
isDraftSegment ,
scale = 1 ,
theme ,
isSelected ,
sceneInfra ,
} ) = > {
if ( input . type !== 'arc-segment' ) {
return new Error ( 'Invalid segment type' )
}
const { from , center , radius } = input
const baseColor = getThemeColorForThreeJs ( theme )
const color = isSelected ? 0x0000ff : baseColor
const group = new Group ( )
const geometry = createArcGeometry ( {
center ,
radius ,
startAngle : 0 ,
endAngle : Math.PI * 2 ,
ccw : true ,
isDashed : isDraftSegment ,
scale ,
} )
const mat = new MeshBasicMaterial ( { color } )
const arcMesh = new Mesh ( geometry , mat )
const meshType = isDraftSegment ? CIRCLE_SEGMENT_DASH : CIRCLE_SEGMENT_BODY
const arrowGroup = createArrowhead ( scale , theme , color )
const circleCenterGroup = createCircleCenterHandle ( scale , theme , color )
2024-10-25 17:42:27 -04:00
// A radius indicator that appears from the center to the perimeter
const radiusIndicatorGroup = createLengthIndicator ( {
from : center ,
to : [ center [ 0 ] + radius , center [ 1 ] ] ,
scale ,
} )
2024-09-23 22:42:51 +10:00
arcMesh . userData . type = meshType
arcMesh . name = meshType
group . userData = {
type : CIRCLE_SEGMENT ,
2024-10-25 15:34:53 -04:00
draft : isDraftSegment ,
2024-09-23 22:42:51 +10:00
id ,
from ,
radius ,
center ,
ccw : true ,
prevSegment ,
pathToNode ,
isSelected ,
baseColor ,
}
group . name = CIRCLE_SEGMENT
2024-10-25 17:42:27 -04:00
group . add ( arcMesh , arrowGroup , circleCenterGroup , radiusIndicatorGroup )
2024-09-23 22:42:51 +10:00
const updateOverlaysCallback = this . update ( {
prevSegment ,
input ,
group ,
scale ,
sceneInfra ,
} )
if ( err ( updateOverlaysCallback ) ) return updateOverlaysCallback
return {
group ,
updateOverlaysCallback ,
}
}
update : SegmentUtils [ 'update' ] = ( {
prevSegment ,
input ,
group ,
scale = 1 ,
sceneInfra ,
} ) = > {
if ( input . type !== 'arc-segment' ) {
return new Error ( 'Invalid segment type' )
}
const { from , center , radius } = input
group . userData . from = from
// group.userData.to = to
group . userData . center = center
group . userData . radius = radius
group . userData . prevSegment = prevSegment
const arrowGroup = group . getObjectByName ( ARROWHEAD ) as Group
2024-10-25 17:42:27 -04:00
const radiusLengthIndicator = group . getObjectByName (
SEGMENT_LENGTH_LABEL
) as Group
2024-09-23 22:42:51 +10:00
const circleCenterHandle = group . getObjectByName (
CIRCLE_CENTER_HANDLE
) as Group
const pxLength = ( 2 * radius * Math . PI ) / scale
const shouldHideIdle = pxLength < HIDE_SEGMENT_LENGTH
const shouldHideHover = pxLength < HIDE_HOVER_SEGMENT_LENGTH
const hoveredParent =
sceneInfra . hoveredObject &&
getParentGroup ( sceneInfra . hoveredObject , [ CIRCLE_SEGMENT ] )
let isHandlesVisible = ! shouldHideIdle
if ( hoveredParent && hoveredParent ? . uuid === group ? . uuid ) {
isHandlesVisible = ! shouldHideHover
}
if ( arrowGroup ) {
2024-10-25 17:42:27 -04:00
// The arrowhead is placed at the perimeter of the circle,
// pointing up and to the right
const arrowPoint = {
x : center [ 0 ] + Math . cos ( Math . PI / 4 ) * radius ,
y : center [ 1 ] + Math . sin ( Math . PI / 4 ) * radius ,
}
arrowGroup . position . set ( arrowPoint . x , arrowPoint . y , 0 )
2024-09-23 22:42:51 +10:00
const arrowheadAngle = Math . PI / 4
arrowGroup . quaternion . setFromUnitVectors (
new Vector3 ( 0 , 1 , 0 ) ,
new Vector3 ( Math . cos ( arrowheadAngle ) , Math . sin ( arrowheadAngle ) , 0 )
)
arrowGroup . scale . set ( scale , scale , scale )
arrowGroup . visible = isHandlesVisible
2024-10-25 17:42:27 -04:00
}
if ( radiusLengthIndicator ) {
// The radius indicator is placed at the midpoint of the radius,
// at a 45 degree CCW angle from the positive X-axis
const indicatorPoint = {
x : center [ 0 ] + ( Math . cos ( Math . PI / 4 ) * radius ) / 2 ,
y : center [ 1 ] + ( Math . sin ( Math . PI / 4 ) * radius ) / 2 ,
}
const labelWrapper = radiusLengthIndicator . getObjectByName (
SEGMENT_LENGTH_LABEL_TEXT
) as CSS2DObject
const labelWrapperElem = labelWrapper . element as HTMLDivElement
const label = labelWrapperElem . children [ 0 ] as HTMLParagraphElement
label . innerText = ` ${ roundOff ( radius ) } `
label . classList . add ( SEGMENT_LENGTH_LABEL_TEXT )
const isPlaneBackFace = center [ 0 ] > indicatorPoint . x
label . style . setProperty (
'--degree' ,
` ${ isPlaneBackFace ? '45' : '-45' } deg `
)
label . style . setProperty ( '--x' , ` 0px ` )
label . style . setProperty ( '--y' , ` 0px ` )
labelWrapper . position . set ( indicatorPoint . x , indicatorPoint . y , 0 )
radiusLengthIndicator . visible = isHandlesVisible
2024-09-23 22:42:51 +10:00
}
if ( circleCenterHandle ) {
circleCenterHandle . position . set ( center [ 0 ] , center [ 1 ] , 0 )
circleCenterHandle . scale . set ( scale , scale , scale )
circleCenterHandle . visible = isHandlesVisible
}
const circleSegmentBody = group . children . find (
( child ) = > child . userData . type === CIRCLE_SEGMENT_BODY
) as Mesh
if ( circleSegmentBody ) {
const newGeo = createArcGeometry ( {
radius ,
center ,
startAngle : 0 ,
endAngle : Math.PI * 2 ,
ccw : true ,
scale ,
} )
circleSegmentBody . geometry = newGeo
}
const circleSegmentBodyDashed = group . getObjectByName ( CIRCLE_SEGMENT_DASH )
if ( circleSegmentBodyDashed instanceof Mesh ) {
// consider throttling the whole updateTangentialArcToSegment
// if there are more perf considerations going forward
circleSegmentBodyDashed . geometry = createArcGeometry ( {
center ,
radius ,
ccw : true ,
// make the start end where the handle is
startAngle : Math.PI * 0.25 ,
endAngle : Math.PI * 2.25 ,
isDashed : true ,
scale ,
} )
}
return ( ) = >
sceneInfra . updateOverlayDetails ( {
2025-02-03 17:53:08 +11:00
handle : arrowGroup ,
2024-09-23 22:42:51 +10:00
group ,
isHandlesVisible ,
from : from ,
to : [ center [ 0 ] , center [ 1 ] ] ,
angle : Math.PI / 4 ,
2025-02-03 17:53:08 +11:00
hasThreeDotMenu : true ,
2024-09-23 22:42:51 +10:00
} )
}
}
2025-01-31 21:26:19 +11:00
class CircleThreePointSegment implements SegmentUtils {
init : SegmentUtils [ 'init' ] = ( {
input ,
id ,
pathToNode ,
isDraftSegment ,
scale = 1 ,
theme ,
isSelected = false ,
sceneInfra ,
prevSegment ,
} ) = > {
if ( input . type !== 'circle-three-point-segment' ) {
return new Error ( 'Invalid segment type' )
}
const { p1 , p2 , p3 } = input
const { center_x , center_y , radius } = calculate_circle_from_3_points (
p1 [ 0 ] ,
p1 [ 1 ] ,
p2 [ 0 ] ,
p2 [ 1 ] ,
p3 [ 0 ] ,
p3 [ 1 ]
)
const center : [ number , number ] = [ center_x , center_y ]
const baseColor = getThemeColorForThreeJs ( theme )
const color = isSelected ? 0x0000ff : baseColor
const group = new Group ( )
const geometry = createArcGeometry ( {
center ,
radius ,
startAngle : 0 ,
endAngle : Math.PI * 2 ,
ccw : true ,
isDashed : isDraftSegment ,
scale ,
} )
const mat = new MeshBasicMaterial ( { color } )
const arcMesh = new Mesh ( geometry , mat )
const meshType = isDraftSegment
? CIRCLE_THREE_POINT_SEGMENT_DASH
: CIRCLE_THREE_POINT_SEGMENT_BODY
const handle1 = createCircleThreePointHandle (
scale ,
theme ,
CIRCLE_THREE_POINT_HANDLE1 ,
color
)
const handle2 = createCircleThreePointHandle (
scale ,
theme ,
CIRCLE_THREE_POINT_HANDLE2 ,
color
)
const handle3 = createCircleThreePointHandle (
scale ,
theme ,
CIRCLE_THREE_POINT_HANDLE3 ,
color
)
arcMesh . userData . type = meshType
arcMesh . name = meshType
group . userData = {
type : CIRCLE_THREE_POINT_SEGMENT ,
draft : isDraftSegment ,
id ,
p1 ,
p2 ,
p3 ,
ccw : true ,
prevSegment ,
pathToNode ,
isSelected ,
baseColor ,
}
group . name = CIRCLE_THREE_POINT_SEGMENT
group . add ( arcMesh , handle1 , handle2 , handle3 )
const updateOverlaysCallback = this . update ( {
prevSegment ,
input ,
group ,
scale ,
sceneInfra ,
} )
if ( err ( updateOverlaysCallback ) ) return updateOverlaysCallback
return {
group ,
updateOverlaysCallback ,
}
}
update : SegmentUtils [ 'update' ] = ( {
input ,
group ,
scale = 1 ,
sceneInfra ,
} ) = > {
if ( input . type !== 'circle-three-point-segment' ) {
return new Error ( 'Invalid segment type' )
}
const { p1 , p2 , p3 } = input
group . userData . p1 = p1
group . userData . p2 = p2
group . userData . p3 = p3
const { center_x , center_y , radius } = calculate_circle_from_3_points (
p1 [ 0 ] ,
p1 [ 1 ] ,
p2 [ 0 ] ,
p2 [ 1 ] ,
p3 [ 0 ] ,
p3 [ 1 ]
)
const center : [ number , number ] = [ center_x , center_y ]
const points = [ p1 , p2 , p3 ]
const handles = [
CIRCLE_THREE_POINT_HANDLE1 ,
CIRCLE_THREE_POINT_HANDLE2 ,
CIRCLE_THREE_POINT_HANDLE3 ,
] . map ( ( handle ) = > group . getObjectByName ( handle ) as Group )
handles . forEach ( ( handle , i ) = > {
const point = points [ i ]
if ( handle && point ) {
handle . position . set ( point [ 0 ] , point [ 1 ] , 0 )
handle . scale . set ( scale , scale , scale )
handle . visible = true
}
} )
const pxLength = ( 2 * radius * Math . PI ) / scale
const shouldHideIdle = pxLength < HIDE_SEGMENT_LENGTH
const shouldHideHover = pxLength < HIDE_HOVER_SEGMENT_LENGTH
const hoveredParent =
sceneInfra . hoveredObject &&
getParentGroup ( sceneInfra . hoveredObject , [ CIRCLE_SEGMENT ] )
let isHandlesVisible = ! shouldHideIdle
if ( hoveredParent && hoveredParent ? . uuid === group ? . uuid ) {
isHandlesVisible = ! shouldHideHover
}
const circleSegmentBody = group . children . find (
( child ) = > child . userData . type === CIRCLE_THREE_POINT_SEGMENT_BODY
) as Mesh
if ( circleSegmentBody ) {
const newGeo = createArcGeometry ( {
radius ,
center ,
startAngle : 0 ,
endAngle : Math.PI * 2 ,
ccw : true ,
scale ,
} )
circleSegmentBody . geometry = newGeo
}
const circleSegmentBodyDashed = group . getObjectByName (
CIRCLE_THREE_POINT_SEGMENT_DASH
)
if ( circleSegmentBodyDashed instanceof Mesh ) {
// consider throttling the whole updateTangentialArcToSegment
// if there are more perf considerations going forward
circleSegmentBodyDashed . geometry = createArcGeometry ( {
center ,
radius ,
ccw : true ,
// make the start end where the handle is
startAngle : Math.PI * 0.25 ,
endAngle : Math.PI * 2.25 ,
isDashed : true ,
scale ,
} )
}
2025-02-03 17:53:08 +11:00
return ( ) = > {
const overlays : SegmentOverlays = { }
const points = [ p1 , p2 , p3 ]
const overlayDetails = handles . map ( ( handle , index ) = > {
const currentPoint = points [ index ]
const angle = Math . atan2 (
currentPoint [ 1 ] - center [ 1 ] ,
currentPoint [ 0 ] - center [ 0 ]
)
return sceneInfra . updateOverlayDetails ( {
handle ,
group ,
isHandlesVisible ,
from : [ 0 , 0 ] ,
to : [ center [ 0 ] , center [ 1 ] ] ,
angle : angle ,
hasThreeDotMenu : index === 0 ,
} )
} )
const segmentOverlays : SegmentOverlay [ ] = [ ]
overlayDetails . forEach ( ( payload , index ) = > {
if ( payload ? . type === 'set-one' ) {
overlays [ payload . pathToNodeString ] = payload . seg
segmentOverlays . push ( {
. . . payload . seg [ 0 ] ,
filterValue : index === 0 ? 'p1' : index === 1 ? 'p2' : 'p3' ,
} )
}
2025-01-31 21:26:19 +11:00
} )
2025-02-03 17:53:08 +11:00
const segmentOverlayPayload : SegmentOverlayPayload = {
type : 'set-one' ,
pathToNodeString :
overlayDetails [ 0 ] ? . type === 'set-one'
? overlayDetails [ 0 ] . pathToNodeString
: '' ,
seg : segmentOverlays ,
}
return segmentOverlayPayload
}
2025-01-31 21:26:19 +11:00
}
}
2024-09-13 21:14:14 +10:00
export function createProfileStartHandle ( {
2024-02-11 12:59:00 +11:00
from ,
2024-10-31 07:04:38 -07:00
isDraft = false ,
2024-02-11 12:59:00 +11:00
scale = 1 ,
2024-05-09 08:38:42 -04:00
theme ,
2024-09-13 21:14:14 +10:00
isSelected ,
2024-12-18 07:08:41 +11:00
size = 12 ,
2024-10-31 07:04:38 -07:00
. . . rest
2024-02-11 12:59:00 +11:00
} : {
from : Coords2d
scale? : number
2024-05-09 08:38:42 -04:00
theme : Themes
2024-06-22 04:49:31 -04:00
isSelected? : boolean
2024-12-18 07:08:41 +11:00
size? : number
2024-10-31 07:04:38 -07:00
} & (
| { isDraft : true }
| { isDraft : false ; id : string ; pathToNode : PathToNode }
) ) {
2024-09-13 21:14:14 +10:00
const group = new Group ( )
2024-02-11 12:59:00 +11:00
2024-12-18 07:08:41 +11:00
const geometry = new BoxGeometry ( size , size , size ) // in pixels scaled later
2024-09-13 21:14:14 +10:00
const baseColor = getThemeColorForThreeJs ( theme )
2024-06-22 04:49:31 -04:00
const color = isSelected ? 0x0000ff : baseColor
const body = new MeshBasicMaterial ( { color } )
2024-02-11 12:59:00 +11:00
const mesh = new Mesh ( geometry , body )
2024-09-13 21:14:14 +10:00
group . add ( mesh )
group . userData = {
type : PROFILE_START ,
2024-02-11 12:59:00 +11:00
from ,
2024-06-22 04:49:31 -04:00
isSelected ,
2024-03-04 14:18:08 +11:00
baseColor ,
2024-10-31 07:04:38 -07:00
. . . rest ,
2024-02-11 12:59:00 +11:00
}
2024-10-31 07:04:38 -07:00
group . name = isDraft ? DRAFT_POINT : PROFILE_START
2024-09-13 21:14:14 +10:00
group . position . set ( from [ 0 ] , from [ 1 ] , 0 )
group . scale . set ( scale , scale , scale )
return group
2024-02-11 12:59:00 +11:00
}
2024-06-22 04:49:31 -04:00
function createArrowhead ( scale = 1 , theme : Themes , color? : number ) : Group {
const baseColor = getThemeColorForThreeJs ( theme )
2024-05-09 08:38:42 -04:00
const arrowMaterial = new MeshBasicMaterial ( {
2024-06-22 04:49:31 -04:00
color : color || baseColor ,
2024-05-09 08:38:42 -04:00
} )
2024-04-03 13:22:56 +11:00
// specify the size of the geometry in pixels (i.e. cone height = 20px, cone radius = 4.5px)
// we'll scale the group to the correct size later to match these sizes in screen space
const arrowheadMesh = new Mesh ( new ConeGeometry ( 4.5 , 20 , 12 ) , arrowMaterial )
arrowheadMesh . position . set ( 0 , - 9 , 0 )
const sphereMesh = new Mesh ( new SphereGeometry ( 4 , 12 , 12 ) , arrowMaterial )
2024-02-11 12:59:00 +11:00
const arrowGroup = new Group ( )
arrowGroup . userData . type = ARROWHEAD
arrowGroup . name = ARROWHEAD
arrowGroup . add ( arrowheadMesh , sphereMesh )
arrowGroup . lookAt ( new Vector3 ( 0 , 1 , 0 ) )
arrowGroup . scale . set ( scale , scale , scale )
return arrowGroup
}
2024-09-23 22:42:51 +10:00
function createCircleCenterHandle (
scale = 1 ,
theme : Themes ,
color? : number
) : Group {
const circleCenterGroup = new Group ( )
const geometry = new BoxGeometry ( 12 , 12 , 12 ) // in pixels scaled later
const baseColor = getThemeColorForThreeJs ( theme )
const body = new MeshBasicMaterial ( { color } )
const mesh = new Mesh ( geometry , body )
circleCenterGroup . add ( mesh )
circleCenterGroup . userData = {
type : CIRCLE_CENTER_HANDLE ,
baseColor ,
}
circleCenterGroup . name = CIRCLE_CENTER_HANDLE
circleCenterGroup . scale . set ( scale , scale , scale )
return circleCenterGroup
}
2025-01-31 21:26:19 +11:00
function createCircleThreePointHandle (
scale = 1 ,
theme : Themes ,
2025-02-12 19:23:30 +11:00
name : ` circle-three-point-handle ${ '1' | '2' | '3' } ` ,
2025-01-31 21:26:19 +11:00
color? : number
) : Group {
const circleCenterGroup = new Group ( )
const geometry = new BoxGeometry ( 12 , 12 , 12 ) // in pixels scaled later
const baseColor = getThemeColorForThreeJs ( theme )
const body = new MeshBasicMaterial ( { color } )
const mesh = new Mesh ( geometry , body )
circleCenterGroup . add ( mesh )
circleCenterGroup . userData = {
type : name ,
baseColor ,
}
circleCenterGroup . name = name
circleCenterGroup . scale . set ( scale , scale , scale )
return circleCenterGroup
}
2024-02-11 12:59:00 +11:00
2024-05-09 08:38:42 -04:00
function createExtraSegmentHandle (
scale : number ,
texture : Texture ,
theme : Themes
) : Group {
2024-04-03 13:22:56 +11:00
const particleMaterial = new PointsMaterial ( {
size : 12 , // in pixels
map : texture ,
transparent : true ,
opacity : 0 ,
depthTest : false ,
} )
const mat = new MeshBasicMaterial ( {
transparent : true ,
2024-05-09 08:38:42 -04:00
color : getThemeColorForThreeJs ( theme ) ,
2024-04-03 13:22:56 +11:00
opacity : 0 ,
} )
const particleGeometry = new BufferGeometry ( ) . setFromPoints ( [
new Vector3 ( 0 , 0 , 0 ) ,
] )
const sphereMesh = new Mesh ( new SphereGeometry ( 6 , 12 , 12 ) , mat ) // sphere radius in pixels
const particle = new Points ( particleGeometry , particleMaterial )
particle . userData . ignoreColorChange = true
particle . userData . type = EXTRA_SEGMENT_HANDLE
const extraSegmentGroup = new Group ( )
extraSegmentGroup . userData . type = EXTRA_SEGMENT_HANDLE
extraSegmentGroup . name = EXTRA_SEGMENT_HANDLE
extraSegmentGroup . add ( sphereMesh )
extraSegmentGroup . add ( particle )
extraSegmentGroup . scale . set ( scale , scale , scale )
return extraSegmentGroup
}
2024-07-08 16:41:00 -04:00
/ * *
* Creates a group containing a CSS2DObject with the length of the segment
* /
function createLengthIndicator ( {
from ,
to ,
scale ,
2024-09-13 21:14:14 +10:00
length = 0.1 ,
2024-07-08 16:41:00 -04:00
} : {
from : Coords2d
to : Coords2d
scale : number
2024-09-13 21:14:14 +10:00
length? : number
2024-07-08 16:41:00 -04:00
} ) {
const lengthIndicatorGroup = new Group ( )
lengthIndicatorGroup . name = SEGMENT_LENGTH_LABEL
// Make the elements
const lengthIndicatorText = document . createElement ( 'p' )
lengthIndicatorText . classList . add ( SEGMENT_LENGTH_LABEL_TEXT )
lengthIndicatorText . innerText = roundOff ( length ) . toString ( )
const lengthIndicatorWrapper = document . createElement ( 'div' )
2024-12-17 15:12:18 -05:00
// Double click workflow
lengthIndicatorWrapper . ondblclick = ( ) = > {
const selection = lengthIndicatorGroup . parent ? . userData . selection
if ( ! selection ) {
console . error ( 'Unable to dimension segment when clicking the label.' )
return
}
sceneInfra . modelingSend ( {
type : 'Set selection' ,
data : {
selectionType : 'singleCodeCursor' ,
selection : selection.graphSelections [ 0 ] ,
} ,
} )
// Command Bar
2025-01-23 10:25:21 -05:00
commandBarActor . send ( {
2024-12-17 15:12:18 -05:00
type : 'Find and select command' ,
data : {
name : 'Constrain length' ,
groupId : 'modeling' ,
argDefaultValues : {
selection ,
} ,
} ,
} )
}
2024-07-08 16:41:00 -04:00
// Style the elements
lengthIndicatorWrapper . style . position = 'absolute'
2024-12-17 15:12:18 -05:00
lengthIndicatorWrapper . style . pointerEvents = 'auto'
2024-07-08 16:41:00 -04:00
lengthIndicatorWrapper . appendChild ( lengthIndicatorText )
const cssObject = new CSS2DObject ( lengthIndicatorWrapper )
cssObject . name = SEGMENT_LENGTH_LABEL_TEXT
// Position the elements based on the line's heading
const offsetFromMidpoint = new Vector2 ( to [ 0 ] - from [ 0 ] , to [ 1 ] - from [ 1 ] )
. normalize ( )
. rotateAround ( new Vector2 ( 0 , 0 ) , - Math . PI / 2 )
. multiplyScalar ( SEGMENT_LENGTH_LABEL_OFFSET_PX * scale )
lengthIndicatorText . style . setProperty ( '--x' , ` ${ offsetFromMidpoint . x } px ` )
lengthIndicatorText . style . setProperty ( '--y' , ` ${ offsetFromMidpoint . y } px ` )
lengthIndicatorGroup . add ( cssObject )
return lengthIndicatorGroup
}
2024-02-11 12:59:00 +11:00
export function createArcGeometry ( {
center ,
radius ,
startAngle ,
endAngle ,
ccw ,
isDashed = false ,
scale = 1 ,
} : {
center : Coords2d
radius : number
startAngle : number
endAngle : number
ccw : boolean
isDashed? : boolean
scale? : number
} ) : BufferGeometry {
2024-04-03 13:22:56 +11:00
const dashSizePx = 18 * scale
const gapSizePx = 18 * scale
2024-02-11 12:59:00 +11:00
const arcStart = new EllipseCurve (
center [ 0 ] ,
center [ 1 ] ,
radius ,
radius ,
startAngle ,
endAngle ,
! ccw ,
0
)
const arcEnd = new EllipseCurve (
center [ 0 ] ,
center [ 1 ] ,
radius ,
radius ,
endAngle ,
startAngle ,
ccw ,
0
)
const shape = new Shape ( )
2024-04-03 13:22:56 +11:00
shape . moveTo ( 0 , ( - SEGMENT_WIDTH_PX / 2 ) * scale )
shape . lineTo ( 0 , ( SEGMENT_WIDTH_PX / 2 ) * scale ) // The width of the line
2024-02-11 12:59:00 +11:00
if ( ! isDashed ) {
const points = arcStart . getPoints ( 50 )
const path = new CurvePath < Vector3 > ( )
path . add ( new CatmullRomCurve3 ( points . map ( ( p ) = > new Vector3 ( p . x , p . y , 0 ) ) ) )
return new ExtrudeGeometry ( shape , {
steps : 100 ,
bevelEnabled : false ,
extrudePath : path ,
} )
}
const length = arcStart . getLength ( )
2024-04-03 13:22:56 +11:00
const totalDashes = length / ( dashSizePx + gapSizePx ) // rounding makes the dashes jittery since the new dash is suddenly appears instead of growing into place
2024-02-11 12:59:00 +11:00
const dashesAtEachEnd = Math . min ( 100 , totalDashes / 2 ) // Assuming we want 50 dashes total, 25 at each end
const dashGeometries = [ ]
// Function to create a dash at a specific t value (0 to 1 along the curve)
const createDashAt = ( t : number , curve : EllipseCurve ) = > {
const startVec = curve . getPoint ( t )
2024-04-03 13:22:56 +11:00
const endVec = curve . getPoint ( Math . min ( 0.5 , t + dashSizePx / length ) )
const midVec = curve . getPoint ( Math . min ( 0.5 , t + dashSizePx / length / 2 ) )
2024-02-11 12:59:00 +11:00
const dashCurve = new CurvePath < Vector3 > ( )
dashCurve . add (
new CatmullRomCurve3 ( [
new Vector3 ( startVec . x , startVec . y , 0 ) ,
new Vector3 ( midVec . x , midVec . y , 0 ) ,
new Vector3 ( endVec . x , endVec . y , 0 ) ,
] )
)
return new ExtrudeGeometry ( shape , {
steps : 3 ,
bevelEnabled : false ,
extrudePath : dashCurve ,
} )
}
// Create dashes at the start of the arc
for ( let i = 0 ; i < dashesAtEachEnd ; i ++ ) {
const t = i / totalDashes
dashGeometries . push ( createDashAt ( t , arcStart ) )
dashGeometries . push ( createDashAt ( t , arcEnd ) )
}
// fill in the remaining arc
2024-04-03 13:22:56 +11:00
const remainingArcLength =
length - dashesAtEachEnd * 2 * ( dashSizePx + gapSizePx )
2024-02-11 12:59:00 +11:00
if ( remainingArcLength > 0 ) {
const remainingArcStartT = dashesAtEachEnd / totalDashes
const remainingArcEndT = 1 - remainingArcStartT
const centerVec = new Vector2 ( center [ 0 ] , center [ 1 ] )
const remainingArcStartVec = arcStart . getPoint ( remainingArcStartT )
const remainingArcEndVec = arcStart . getPoint ( remainingArcEndT )
const remainingArcCurve = new EllipseCurve (
arcStart . aX ,
arcStart . aY ,
arcStart . xRadius ,
arcStart . yRadius ,
new Vector2 ( ) . subVectors ( centerVec , remainingArcStartVec ) . angle ( ) +
Math . PI ,
new Vector2 ( ) . subVectors ( centerVec , remainingArcEndVec ) . angle ( ) + Math . PI ,
! ccw
)
const remainingArcPoints = remainingArcCurve . getPoints ( 50 )
const remainingArcPath = new CurvePath < Vector3 > ( )
remainingArcPath . add (
new CatmullRomCurve3 (
remainingArcPoints . map ( ( p ) = > new Vector3 ( p . x , p . y , 0 ) )
)
)
const remainingArcGeometry = new ExtrudeGeometry ( shape , {
steps : 50 ,
bevelEnabled : false ,
extrudePath : remainingArcPath ,
} )
dashGeometries . push ( remainingArcGeometry )
}
const geo = dashGeometries . length
? mergeGeometries ( dashGeometries )
: new BufferGeometry ( )
geo . userData . type = 'dashed'
return geo
}
2025-01-16 11:10:36 -05:00
// (lee) The above is much more complex than necessary.
// I've derived the new code from:
// https://threejs.org/docs/#api/en/extras/curves/EllipseCurve
// I'm not sure why it wasn't done like this in the first place?
// I don't touch the code above because it may break something else.
export function createCircleGeometry ( {
center ,
radius ,
color ,
isDashed = false ,
scale = 1 ,
} : {
center : Coords2d
radius : number
color : number
isDashed? : boolean
scale? : number
} ) : Line {
const circle = new EllipseCurve (
center [ 0 ] ,
center [ 1 ] ,
radius ,
radius ,
0 ,
Math . PI * 2 ,
true ,
scale
)
const points = circle . getPoints ( 75 ) // just enough points to not see edges.
const geometry = new BufferGeometry ( ) . setFromPoints ( points )
const material = ! isDashed
? new LineBasicMaterial ( { color } )
: new LineDashedMaterial ( {
color ,
scale ,
dashSize : 6 ,
gapSize : 6 ,
} )
const line = new Line ( geometry , material )
line . computeLineDistances ( )
return line
}
2024-02-11 12:59:00 +11:00
export function dashedStraight (
from : Coords2d ,
to : Coords2d ,
shape : Shape ,
scale = 1
) : BufferGeometry < NormalBufferAttributes > {
2024-04-03 13:22:56 +11:00
const dashSize = 18 * scale
const gapSize = 18 * scale // TODO: gapSize is not respected
2024-02-11 12:59:00 +11:00
const dashLine = new LineCurve3 (
new Vector3 ( from [ 0 ] , from [ 1 ] , 0 ) ,
new Vector3 ( to [ 0 ] , to [ 1 ] , 0 )
)
const length = dashLine . getLength ( )
const numberOfPoints = ( length / ( dashSize + gapSize ) ) * 2
const startOfLine = new Vector3 ( from [ 0 ] , from [ 1 ] , 0 )
const endOfLine = new Vector3 ( to [ 0 ] , to [ 1 ] , 0 )
const dashGeometries = [ ]
const dashComponent = ( xOrY : number , pointIndex : number ) = >
( ( to [ xOrY ] - from [ xOrY ] ) / numberOfPoints ) * pointIndex + from [ xOrY ]
for ( let i = 0 ; i < numberOfPoints ; i += 2 ) {
const dashStart = new Vector3 ( dashComponent ( 0 , i ) , dashComponent ( 1 , i ) , 0 )
let dashEnd = new Vector3 (
dashComponent ( 0 , i + 1 ) ,
dashComponent ( 1 , i + 1 ) ,
0
)
if ( startOfLine . distanceTo ( dashEnd ) > startOfLine . distanceTo ( endOfLine ) )
dashEnd = endOfLine
if ( dashEnd ) {
const dashCurve = new LineCurve3 ( dashStart , dashEnd )
const dashGeometry = new ExtrudeGeometry ( shape , {
steps : 1 ,
bevelEnabled : false ,
extrudePath : dashCurve ,
} )
dashGeometries . push ( dashGeometry )
}
}
const geo = dashGeometries . length
? mergeGeometries ( dashGeometries )
: new BufferGeometry ( )
geo . userData . type = 'dashed'
return geo
}
2024-09-13 21:14:14 +10:00
export const segmentUtils = {
straight : new StraightSegment ( ) ,
tangentialArcTo : new TangentialArcToSegment ( ) ,
2024-09-23 22:42:51 +10:00
circle : new CircleSegment ( ) ,
2025-01-31 21:26:19 +11:00
circleThreePoint : new CircleThreePointSegment ( ) ,
2024-09-13 21:14:14 +10:00
} as const