import { useRef, useEffect, useState, useMemo, Fragment } from 'react' import { useModelingContext } from 'hooks/useModelingContext' import { cameraMouseDragGuards } from 'lib/cameraControls' import { ARROWHEAD, DEBUG_SHOW_BOTH_SCENES } from './sceneInfra' import { ReactCameraProperties } from './CameraControls' import { throttle, toSync } from 'lib/utils' import { sceneInfra, kclManager, codeManager, editorManager, sceneEntitiesManager, engineCommandManager, rustContext, } from 'lib/singletons' import { EXTRA_SEGMENT_HANDLE, PROFILE_START, getParentGroup, } from './sceneEntities' import { SegmentOverlay, SketchDetails } from 'machines/modelingMachine' import { findUsesOfTagInPipe, getNodeFromPath } from 'lang/queryAst' import { CallExpression, CallExpressionKw, PathToNode, Program, Expr, parse, recast, defaultSourceRange, resultIsOk, topLevelRange, } from 'lang/wasm' import { CustomIcon, CustomIconName } from 'components/CustomIcon' import { ConstrainInfo } from 'lang/std/stdTypes' import { getConstraintInfo, getConstraintInfoKw } from 'lang/std/sketch' import { Dialog, Popover, Transition } from '@headlessui/react' import toast from 'react-hot-toast' import { InstanceProps, create } from 'react-modal-promise' import { executeAst } from 'lang/langHelpers' import { deleteSegmentFromPipeExpression, removeSingleConstraintInfo, } from 'lang/modifyAst' import { ActionButton } from 'components/ActionButton' import { err, reportRejection, trap } from 'lib/trap' import { Node } from '@rust/kcl-lib/bindings/Node' import { commandBarActor } from 'machines/commandBarMachine' import { useSettings } from 'machines/appMachine' function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } { const [isCamMoving, setIsCamMoving] = useState(false) const [isTween, setIsTween] = useState(false) const { state } = useModelingContext() useEffect(() => { sceneInfra.camControls.setIsCamMovingCallback((isMoving, isTween) => { setIsCamMoving(isMoving) setIsTween(isTween) }) }, []) if (DEBUG_SHOW_BOTH_SCENES || !isCamMoving) return { hideClient: false, hideServer: false } let hideServer = state.matches('Sketch') if (isTween) { hideServer = false } return { hideClient: !hideServer, hideServer } } export const ClientSideScene = ({ cameraControls, }: { cameraControls: ReturnType< typeof useSettings >['modeling']['mouseControls']['current'] }) => { const canvasRef = useRef(null) const { state, send, context } = useModelingContext() const { hideClient, hideServer } = useShouldHideScene() // Listen for changes to the camera controls setting // and update the client-side scene's controls accordingly. useEffect(() => { sceneInfra.camControls.interactionGuards = cameraMouseDragGuards[cameraControls] }, [cameraControls]) useEffect(() => { sceneInfra.updateOtherSelectionColors( state?.context?.selectionRanges?.otherSelections || [] ) }, [state?.context?.selectionRanges?.otherSelections]) useEffect(() => { if (!canvasRef.current) return const canvas = canvasRef.current canvas.appendChild(sceneInfra.renderer.domElement) canvas.appendChild(sceneInfra.labelRenderer.domElement) sceneInfra.animate() canvas.addEventListener( 'mousemove', toSync(sceneInfra.onMouseMove, reportRejection), false ) canvas.addEventListener('mousedown', sceneInfra.onMouseDown, false) canvas.addEventListener( 'mouseup', toSync(sceneInfra.onMouseUp, reportRejection), false ) sceneInfra.setSend(send) engineCommandManager.modelingSend = send return () => { canvas?.removeEventListener( 'mousemove', toSync(sceneInfra.onMouseMove, reportRejection) ) canvas?.removeEventListener('mousedown', sceneInfra.onMouseDown) canvas?.removeEventListener( 'mouseup', toSync(sceneInfra.onMouseUp, reportRejection) ) sceneEntitiesManager.tearDownSketch({ removeAxis: true }) } }, []) let cursor = 'default' if (state.matches('Sketch')) { if ( context.mouseState.type === 'isHovering' && getParentGroup(context.mouseState.on, [ ARROWHEAD, EXTRA_SEGMENT_HANDLE, PROFILE_START, ]) ) { cursor = 'move' } else if (context.mouseState.type === 'isDragging') { cursor = 'grabbing' } else if ( state.matches({ Sketch: 'Line tool' }) || state.matches({ Sketch: 'Tangential arc to' }) || state.matches({ Sketch: 'Rectangle tool' }) || state.matches({ Sketch: 'Circle tool' }) || state.matches({ Sketch: 'Circle three point tool' }) || state.matches({ Sketch: 'Arc three point tool' }) ) { cursor = 'crosshair' } else { cursor = 'default' } } return ( <>
) } const Overlays = () => { const { context } = useModelingContext() if (context.mouseState.type === 'isDragging') return null // Set a large zIndex, the overlay for hover dropdown menu on line segments needs to render // over the length labels on the line segments return (
{Object.entries(context.segmentOverlays) .flatMap((a) => a[1].map((b) => ({ pathToNodeString: a[0], overlay: b })) ) .filter((a) => a.overlay.visible) .map(({ pathToNodeString, overlay }, index) => { return ( ) })}
) } const Overlay = ({ overlay, overlayIndex, pathToNodeString, }: { overlay: SegmentOverlay overlayIndex: number pathToNodeString: string }) => { const { context, send, state } = useModelingContext() let xAlignment = overlay.angle < 0 ? '0%' : '-100%' let yAlignment = overlay.angle < -90 || overlay.angle >= 90 ? '0%' : '-100%' // It's possible for the pathToNode to request a newer AST node // than what's available in the AST at the moment of query. // It eventually settles on being updated. const _node1 = getNodeFromPath>( kclManager.ast, overlay.pathToNode, ['CallExpression', 'CallExpressionKw'] ) // For that reason, to prevent console noise, we do not use err here. if (_node1 instanceof Error) { console.warn('ast older than pathToNode, not fatal, eventually settles', '') return } const callExpression = _node1.node const constraints = callExpression.type === 'CallExpression' ? getConstraintInfo( callExpression, codeManager.code, overlay.pathToNode, overlay.filterValue ) : getConstraintInfoKw( callExpression, codeManager.code, overlay.pathToNode, overlay.filterValue ) const offset = 20 // px // We could put a boolean in settings that const offsetAngle = 90 const xOffset = Math.cos(((overlay.angle + offsetAngle) * Math.PI) / 180) * offset const yOffset = Math.sin(((overlay.angle + offsetAngle) * Math.PI) / 180) * offset const shouldShow = overlay.visible && typeof context?.segmentHoverMap?.[pathToNodeString] === 'number' && !( state.matches({ Sketch: 'Line tool' }) || state.matches({ Sketch: 'Tangential arc to' }) || state.matches({ Sketch: 'Rectangle tool' }) ) return (
{shouldShow && (
send({ type: 'Set mouse state', data: { type: 'isHovering', on: overlay.group, }, }) } onMouseLeave={() => send({ type: 'Set mouse state', data: { type: 'idle' }, }) } > {constraints && constraints.map((constraintInfo, i) => ( window.innerHeight / 2 ? 'top' : 'bottom' } /> ))} {/* delete circle is complicated by the fact it's the only segment in the pipe expression. Maybe it should delete the entire pipeExpression, however this will likely change soon when we implement multi-profile so we'll leave it for now issue: https://github.com/KittyCAD/modeling-app/issues/3910 */} {callExpression?.callee?.name !== 'circle' && callExpression?.callee?.name !== 'circleThreePoint' && ( window.innerHeight / 2 ? 'top' : 'bottom' } pathToNode={overlay.pathToNode} stdLibFnName={constraints[0]?.stdLibFnName} /> )}
)}
) } type ConfirmModalProps = InstanceProps & { text: string } export const ConfirmModal = ({ isOpen, onResolve, onReject, text, }: ConfirmModalProps) => { return ( onResolve(false)} >
{text}
onResolve(true)} > Continue and unconstrain onReject(false)} > Cancel
) } export const confirmModal = create( ConfirmModal ) export async function deleteSegment({ pathToNode, sketchDetails, }: { pathToNode: PathToNode sketchDetails: SketchDetails | null }) { let modifiedAst: Node | Error = kclManager.ast const dependentRanges = findUsesOfTagInPipe(modifiedAst, pathToNode) const shouldContinueSegDelete = dependentRanges.length ? await confirmModal({ text: `At least ${dependentRanges.length} segment rely on the segment you're deleting.\nDo you want to continue and unconstrain these segments?`, isOpen: true, }) : true if (!shouldContinueSegDelete) return modifiedAst = deleteSegmentFromPipeExpression( dependentRanges, modifiedAst, kclManager.variables, codeManager.code, pathToNode ) if (err(modifiedAst)) return Promise.reject(modifiedAst) const newCode = recast(modifiedAst) const pResult = parse(newCode) if (err(pResult) || !resultIsOk(pResult)) return Promise.reject(pResult) modifiedAst = pResult.program const testExecute = await executeAst({ ast: modifiedAst, engineCommandManager: engineCommandManager, isMock: true, rustContext, usePrevMemory: false, }) if (testExecute.errors.length) { toast.error('Segment tag used outside of current Sketch. Could not delete.') return } if (!sketchDetails) return await sceneEntitiesManager.updateAstAndRejigSketch( pathToNode, sketchDetails.sketchNodePaths, sketchDetails.planeNodePath, modifiedAst, sketchDetails.zAxis, sketchDetails.yAxis, sketchDetails.origin ) // Now 'Set sketchDetails' is called with the modified pathToNode } const SegmentMenu = ({ verticalPosition, pathToNode, stdLibFnName, }: { verticalPosition: 'top' | 'bottom' pathToNode: PathToNode stdLibFnName: string }) => { const { send } = useModelingContext() const dependentSourceRanges = findUsesOfTagInPipe(kclManager.ast, pathToNode) return ( {({ open }) => ( <> {stdLibFnName !== 'arcTo' && ( )} )} ) } const ConstraintSymbol = ({ constrainInfo: { type: _type, isConstrained, value, pathToNode, argPosition }, verticalPosition, }: { constrainInfo: ConstrainInfo verticalPosition: 'top' | 'bottom' }) => { const { context } = useModelingContext() const varNameMap: { [key in ConstrainInfo['type']]: { varName: string displayName: string iconName: CustomIconName implicitConstraintDesc?: string } } = { xRelative: { varName: 'xRel', displayName: 'X Relative', iconName: 'xRelative', }, xAbsolute: { varName: 'xAbs', displayName: 'X Absolute', iconName: 'xAbsolute', }, yRelative: { varName: 'yRel', displayName: 'Y Relative', iconName: 'yRelative', }, yAbsolute: { varName: 'yAbs', displayName: 'Y Absolute', iconName: 'yAbsolute', }, angle: { varName: 'angle', displayName: 'Angle', iconName: 'angle', }, length: { varName: 'len', displayName: 'Length', iconName: 'dimension', }, intersectionOffset: { varName: 'perpDist', displayName: 'Intersection Offset', iconName: 'intersection-offset', }, radius: { varName: 'radius', displayName: 'Radius', iconName: 'dimension', }, // implicit constraints vertical: { varName: '', displayName: '', iconName: 'vertical', implicitConstraintDesc: 'vertically', }, horizontal: { varName: '', displayName: '', iconName: 'horizontal', implicitConstraintDesc: 'horizontally', }, tangentialWithPrevious: { varName: '', displayName: '', iconName: 'tangent', implicitConstraintDesc: 'tangential to previous segment', }, // we don't render this one intersectionTag: { varName: '', displayName: '', iconName: 'dimension', }, } const varName = varNameMap?.[_type]?.varName || 'var' const name: CustomIconName = varNameMap[_type].iconName const displayName = varNameMap[_type]?.displayName const implicitDesc = varNameMap[_type]?.implicitConstraintDesc const _node = useMemo( () => getNodeFromPath(kclManager.ast, pathToNode), [kclManager.ast, pathToNode] ) if (err(_node)) return const node = _node.node const range = node ? topLevelRange(node.start, node.end) : defaultSourceRange() if (_type === 'intersectionTag') return null return (
{implicitDesc ? (
                {value}
              
{' '} is implicitly constrained {implicitDesc}
) : ( <>
{isConstrained ? 'Constrained' : 'Unconstrained'} {displayName}
Set to
                  {value}
                
{isConstrained ? 'Click to unconstrain with raw number' : 'Click to constrain with variable'}
)}
) } const throttled = throttle((a: ReactCameraProperties) => { if (a.type === 'perspective' && a.fov) { sceneInfra.camControls.dollyZoom(a.fov).catch(reportRejection) } }, 1000 / 15) export const CamDebugSettings = () => { const [camSettings, setCamSettings] = useState( sceneInfra.camControls.reactCameraProperties ) const [fov, setFov] = useState(12) useEffect(() => { sceneInfra.camControls.setReactCameraPropertiesCallback(setCamSettings) }, [sceneInfra]) useEffect(() => { if (camSettings.type === 'perspective' && camSettings.fov) { setFov(camSettings.fov) } }, [(camSettings as any)?.fov]) return (

cam settings

perspective cam commandBarActor.send({ type: 'Find and select command', data: { groupId: 'settings', name: 'modeling.cameraProjection', }, }) } />
{camSettings.type === 'perspective' && ( { setFov(parseFloat(e.target.value)) throttled({ ...camSettings, fov: parseFloat(e.target.value), }) }} className="w-full cursor-pointer pointer-events-auto" /> )} {camSettings.type === 'perspective' && (
fov { sceneInfra.camControls.setCam({ ...camSettings, fov: parseFloat(e.target.value), }) }} />
)} {camSettings.type === 'orthographic' && ( <>
fov { sceneInfra.camControls.setCam({ ...camSettings, zoom: parseFloat(e.target.value), }) }} />
)}
Position
  • x: { sceneInfra.camControls.setCam({ ...camSettings, position: [ parseFloat(e.target.value), camSettings.position[1], camSettings.position[2], ], }) }} />
  • y: { sceneInfra.camControls.setCam({ ...camSettings, position: [ camSettings.position[0], parseFloat(e.target.value), camSettings.position[2], ], }) }} />
  • z: { sceneInfra.camControls.setCam({ ...camSettings, position: [ camSettings.position[0], camSettings.position[1], parseFloat(e.target.value), ], }) }} />
target
  • x: { sceneInfra.camControls.setCam({ ...camSettings, target: [ parseFloat(e.target.value), camSettings.target[1], camSettings.target[2], ], }) }} />
  • y: { sceneInfra.camControls.setCam({ ...camSettings, target: [ camSettings.target[0], parseFloat(e.target.value), camSettings.target[2], ], }) }} />
  • z: { sceneInfra.camControls.setCam({ ...camSettings, target: [ camSettings.target[0], camSettings.target[1], parseFloat(e.target.value), ], }) }} />
) }