import { Coords2d } from 'lang/std/sketch' import { BoxGeometry, BufferGeometry, CatmullRomCurve3, ConeGeometry, CurvePath, EllipseCurve, ExtrudeGeometry, Group, LineCurve3, Mesh, MeshBasicMaterial, NormalBufferAttributes, Shape, SphereGeometry, Vector2, Vector3, } from 'three' import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js' import { PathToNode, SketchGroup, getTangentialArcToInfo } from 'lang/wasm' import { PROFILE_START, STRAIGHT_SEGMENT, STRAIGHT_SEGMENT_BODY, STRAIGHT_SEGMENT_DASH, TANGENTIAL_ARC_TO_SEGMENT, TANGENTIAL_ARC_TO_SEGMENT_BODY, TANGENTIAL_ARC_TO__SEGMENT_DASH, } from './sceneEntities' import { getTangentPointFromPreviousArc } from 'lib/utils2d' import { ARROWHEAD } from './sceneInfra' export function profileStart({ from, id, pathToNode, scale = 1, }: { from: Coords2d id: string pathToNode: PathToNode scale?: number }) { const group = new Group() const geometry = new BoxGeometry(0.8, 0.8, 0.8) 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 } export function straightSegment({ from, to, id, pathToNode, isDraftSegment, scale = 1, callExpName, }: { from: Coords2d to: Coords2d id: string pathToNode: PathToNode isDraftSegment?: boolean scale?: number callExpName: string }): Group { const group = new Group() const shape = new Shape() shape.moveTo(0, -0.08 * scale) shape.lineTo(0, 0.08 * scale) // The width of the line 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, }) } const baseColor = callExpName === 'close' ? 0x444444 : 0xffffff const body = new MeshBasicMaterial({ color: baseColor }) 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, callExpName, baseColor, } group.name = STRAIGHT_SEGMENT 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) group.add(mesh) if (callExpName !== 'close') group.add(arrowGroup) return group } function createArrowhead(scale = 1): Group { const arrowMaterial = new MeshBasicMaterial({ color: 0xffffff }) const arrowheadMesh = new Mesh(new ConeGeometry(0.31, 1.5, 12), arrowMaterial) arrowheadMesh.position.set(0, -0.6, 0) const sphereMesh = new Mesh(new SphereGeometry(0.27, 12, 12), arrowMaterial) 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 } export function tangentialArcToSegment({ prevSegment, from, to, id, pathToNode, isDraftSegment, scale = 1, }: { prevSegment: SketchGroup['value'][number] from: Coords2d to: Coords2d id: string pathToNode: PathToNode isDraftSegment?: boolean scale?: number }): Group { const group = new Group() const previousPoint = prevSegment?.type === 'TangentialArcTo' ? getTangentPointFromPreviousArc( prevSegment.center, prevSegment.ccw, prevSegment.to ) : prevSegment.from const { center, radius, startAngle, endAngle, ccw } = getTangentialArcToInfo({ arcStartPoint: from, arcEndPoint: to, tanPreviousPoint: previousPoint, obtuse: true, }) 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, } group.name = TANGENTIAL_ARC_TO_SEGMENT 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) ) group.add(mesh, arrowGroup) 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 { const dashSize = 1.2 * scale const gapSize = 1.2 * scale 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() shape.moveTo(0, -0.08 * scale) shape.lineTo(0, 0.08 * scale) // The width of the line if (!isDashed) { const points = arcStart.getPoints(50) const path = new CurvePath() 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() const totalDashes = length / (dashSize + gapSize) // rounding makes the dashes jittery since the new dash is suddenly appears instead of growing into place 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) const endVec = curve.getPoint(Math.min(0.5, t + dashSize / length)) const midVec = curve.getPoint(Math.min(0.5, t + dashSize / length / 2)) const dashCurve = new CurvePath() 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 const remainingArcLength = length - dashesAtEachEnd * 2 * (dashSize + gapSize) 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() 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 { const dashSize = 1.2 * scale const gapSize = 1.2 * scale // todo: gabSize is not respected 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 }