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 ,
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'
import { PathToNode , SketchGroup , getTangentialArcToInfo } from 'lang/wasm'
import {
2024-04-03 13:22:56 +11:00
EXTRA_SEGMENT_HANDLE ,
EXTRA_SEGMENT_OFFSET_PX ,
MIN_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-02-14 08:03:20 +11:00
} from './sceneEntities'
2024-02-11 12:59:00 +11:00
import { getTangentPointFromPreviousArc } from 'lib/utils2d'
2024-02-14 08:03:20 +11:00
import { ARROWHEAD } from './sceneInfra'
2024-02-11 12:59:00 +11:00
2024-03-02 08:48:30 +11:00
export function profileStart ( {
from ,
id ,
pathToNode ,
scale = 1 ,
} : {
from : Coords2d
id : string
pathToNode : PathToNode
scale? : number
} ) {
const group = new Group ( )
2024-04-03 13:22:56 +11:00
const geometry = new BoxGeometry ( 12 , 12 , 12 ) // in pixels scaled later
2024-03-02 08:48:30 +11:00
const body = new MeshBasicMaterial ( { color : 0xffffff } )
const mesh = new Mesh ( geometry , body )
group . add ( mesh )
group . userData = {
type : PROFILE_START ,
id ,
from ,
pathToNode ,
isSelected : false ,
}
group . name = PROFILE_START
group . position . set ( from [ 0 ] , from [ 1 ] , 0 )
group . scale . set ( scale , scale , scale )
return group
}
2024-02-11 12:59:00 +11:00
export function straightSegment ( {
from ,
to ,
id ,
pathToNode ,
isDraftSegment ,
scale = 1 ,
2024-03-04 14:18:08 +11:00
callExpName ,
2024-04-03 13:22:56 +11:00
texture ,
2024-02-11 12:59:00 +11:00
} : {
from : Coords2d
to : Coords2d
id : string
pathToNode : PathToNode
isDraftSegment? : boolean
scale? : number
2024-03-04 14:18:08 +11:00
callExpName : string
2024-04-03 13:22:56 +11:00
texture : Texture
2024-02-11 12:59:00 +11:00
} ) : Group {
const group = new Group ( )
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 )
2024-02-11 12:59:00 +11:00
let geometry
if ( isDraftSegment ) {
geometry = dashedStraight ( from , to , shape , scale )
} else {
const line = new LineCurve3 (
new Vector3 ( from [ 0 ] , from [ 1 ] , 0 ) ,
new Vector3 ( to [ 0 ] , to [ 1 ] , 0 )
)
geometry = new ExtrudeGeometry ( shape , {
steps : 2 ,
bevelEnabled : false ,
extrudePath : line ,
} )
}
2024-03-04 14:18:08 +11:00
const baseColor = callExpName === 'close' ? 0x444444 : 0xffffff
const body = new MeshBasicMaterial ( { color : baseColor } )
2024-02-11 12:59:00 +11:00
const mesh = new Mesh ( geometry , body )
mesh . userData . type = isDraftSegment
? STRAIGHT_SEGMENT_DASH
: STRAIGHT_SEGMENT_BODY
mesh . name = STRAIGHT_SEGMENT_BODY
group . userData = {
type : STRAIGHT_SEGMENT ,
id ,
from ,
to ,
pathToNode ,
isSelected : false ,
2024-03-04 14:18:08 +11:00
callExpName ,
baseColor ,
2024-02-11 12:59:00 +11:00
}
2024-03-02 19:00:24 +11:00
group . name = STRAIGHT_SEGMENT
2024-02-11 12:59:00 +11:00
2024-04-03 13:22:56 +11:00
const length = Math . sqrt (
Math . pow ( to [ 0 ] - from [ 0 ] , 2 ) + Math . pow ( to [ 1 ] - from [ 1 ] , 2 )
)
2024-02-11 12:59:00 +11:00
const arrowGroup = createArrowhead ( scale )
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 )
2024-04-03 13:22:56 +11:00
const pxLength = length / scale
const shouldHide = pxLength < MIN_SEGMENT_LENGTH
arrowGroup . visible = ! shouldHide
2024-02-11 12:59:00 +11:00
2024-03-04 14:18:08 +11:00
group . add ( mesh )
if ( callExpName !== 'close' ) group . add ( arrowGroup )
2024-02-11 12:59:00 +11:00
2024-04-03 13:22:56 +11:00
const extraSegmentGroup = createExtraSegmentHandle ( scale , texture )
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 . visible = ! shouldHide
group . add ( extraSegmentGroup )
2024-02-11 12:59:00 +11:00
return group
}
function createArrowhead ( scale = 1 ) : Group {
const arrowMaterial = new MeshBasicMaterial ( { color : 0xffffff } )
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-04-03 13:22:56 +11:00
function createExtraSegmentHandle ( scale : number , texture : Texture ) : Group {
const particleMaterial = new PointsMaterial ( {
size : 12 , // in pixels
map : texture ,
transparent : true ,
opacity : 0 ,
depthTest : false ,
} )
const mat = new MeshBasicMaterial ( {
transparent : true ,
color : 0xffffff ,
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-02-11 12:59:00 +11:00
export function tangentialArcToSegment ( {
prevSegment ,
from ,
to ,
id ,
pathToNode ,
isDraftSegment ,
scale = 1 ,
2024-04-03 13:22:56 +11:00
texture ,
2024-02-11 12:59:00 +11:00
} : {
prevSegment : SketchGroup [ 'value' ] [ number ]
from : Coords2d
to : Coords2d
id : string
pathToNode : PathToNode
isDraftSegment? : boolean
scale? : number
2024-04-03 13:22:56 +11:00
texture : Texture
2024-02-11 12:59:00 +11:00
} ) : Group {
const group = new Group ( )
const previousPoint =
prevSegment ? . type === 'TangentialArcTo'
? getTangentPointFromPreviousArc (
prevSegment . center ,
prevSegment . ccw ,
prevSegment . to
)
: prevSegment . from
2024-04-03 13:22:56 +11:00
const { center , radius , startAngle , endAngle , ccw , arcLength } =
getTangentialArcToInfo ( {
arcStartPoint : from ,
arcEndPoint : to ,
tanPreviousPoint : previousPoint ,
obtuse : true ,
} )
2024-02-11 12:59:00 +11:00
const geometry = createArcGeometry ( {
center ,
radius ,
startAngle ,
endAngle ,
ccw ,
isDashed : isDraftSegment ,
scale ,
} )
const body = new MeshBasicMaterial ( { color : 0xffffff } )
const mesh = new Mesh ( geometry , body )
mesh . userData . type = isDraftSegment
? TANGENTIAL_ARC_TO__SEGMENT_DASH
: TANGENTIAL_ARC_TO_SEGMENT_BODY
group . userData = {
type : TANGENTIAL_ARC_TO_SEGMENT ,
id ,
from ,
to ,
prevSegment ,
pathToNode ,
isSelected : false ,
}
2024-03-02 19:00:24 +11:00
group . name = TANGENTIAL_ARC_TO_SEGMENT
2024-02-11 12:59:00 +11:00
const arrowGroup = createArrowhead ( scale )
arrowGroup . position . set ( to [ 0 ] , to [ 1 ] , 0 )
const arrowheadAngle = endAngle + ( Math . PI / 2 ) * ( ccw ? 1 : - 1 )
arrowGroup . quaternion . setFromUnitVectors (
new Vector3 ( 0 , 1 , 0 ) ,
new Vector3 ( Math . cos ( arrowheadAngle ) , Math . sin ( arrowheadAngle ) , 0 )
)
2024-04-03 13:22:56 +11:00
const pxLength = arcLength / scale
const shouldHide = pxLength < MIN_SEGMENT_LENGTH
arrowGroup . visible = ! shouldHide
const extraSegmentGroup = createExtraSegmentHandle ( scale , texture )
const circumferenceInPx = ( 2 * Math . PI * radius ) / scale
const extraSegmentAngleDelta =
( EXTRA_SEGMENT_OFFSET_PX / circumferenceInPx ) * Math . PI * 2
const extraSegmentAngle = startAngle + ( ccw ? 1 : - 1 ) * extraSegmentAngleDelta
const extraSegmentOffset = new Vector2 (
Math . cos ( extraSegmentAngle ) * radius ,
Math . sin ( extraSegmentAngle ) * radius
)
extraSegmentGroup . position . set (
center [ 0 ] + extraSegmentOffset . x ,
center [ 1 ] + extraSegmentOffset . y ,
0
)
extraSegmentGroup . visible = ! shouldHide
2024-02-11 12:59:00 +11:00
2024-04-03 13:22:56 +11:00
group . add ( mesh , arrowGroup , extraSegmentGroup )
2024-02-11 12:59:00 +11:00
return group
}
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
}
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
}