import { useRef, useEffect, useState, useMemo, Fragment } from 'react' import { useModelingContext } from 'hooks/useModelingContext' import { cameraMouseDragGuards } from 'lib/cameraControls' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { ARROWHEAD, DEBUG_SHOW_BOTH_SCENES } from './sceneInfra' import { ReactCameraProperties } from './CameraControls' import { throttle } from 'lib/utils' import { sceneInfra, kclManager, codeManager, editorManager, sceneEntitiesManager, engineCommandManager, } 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, PathToNode, Program, SourceRange, Value, parse, recast, } from 'lang/wasm' import { CustomIcon, CustomIconName } from 'components/CustomIcon' import { ConstrainInfo } from 'lang/std/stdTypes' import { getConstraintInfo } from 'lang/std/sketch' import { Dialog, Popover, Transition } from '@headlessui/react' import { LineInputsType } from 'lang/std/sketchcombos' import toast from 'react-hot-toast' import { InstanceProps, create } from 'react-modal-promise' import { executeAst } from 'useStore' import { deleteSegmentFromPipeExpression, makeRemoveSingleConstraintInput, removeSingleConstraintInfo, } from 'lang/modifyAst' import { ActionButton } from 'components/ActionButton' 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 useSettingsAuthContext >['settings']['context']['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) sceneInfra.animate() canvas.addEventListener('mousemove', sceneInfra.onMouseMove, false) canvas.addEventListener('mousedown', sceneInfra.onMouseDown, false) canvas.addEventListener('mouseup', sceneInfra.onMouseUp, false) sceneInfra.setSend(send) return () => { canvas?.removeEventListener('mousemove', sceneInfra.onMouseMove) canvas?.removeEventListener('mousedown', sceneInfra.onMouseDown) canvas?.removeEventListener('mouseup', sceneInfra.onMouseUp) } }, []) 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') ) { cursor = 'crosshair' } else { cursor = 'default' } } return ( <>
) } const Overlays = () => { const { context } = useModelingContext() if (context.mouseState.type === 'isDragging') return null return (
{Object.entries(context.segmentOverlays) .filter((a) => a[1].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%' const callExpression = getNodeFromPath( kclManager.ast, overlay.pathToNode, 'CallExpression' ).node const constraints = getConstraintInfo( callExpression, codeManager.code, overlay.pathToNode ) 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' } /> ))} 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: Program = 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.programMemory, codeManager.code, pathToNode ) const newCode = recast(modifiedAst) modifiedAst = parse(newCode) const testExecute = await executeAst({ ast: modifiedAst, useFakeExecutor: true, engineCommandManager: engineCommandManager, }) if (testExecute.errors.length) { toast.error('Segment tag used outside of current Sketch. Could not delete.') return } if (!sketchDetails) return sceneEntitiesManager.updateAstAndRejigSketch( sketchDetails.sketchPathToNode, modifiedAst, sketchDetails.zAxis, sketchDetails.yAxis, sketchDetails.origin ) } const SegmentMenu = ({ verticalPosition, pathToNode, stdLibFnName, }: { verticalPosition: 'top' | 'bottom' pathToNode: PathToNode stdLibFnName: string }) => { const { send } = useModelingContext() const dependentSourceRanges = findUsesOfTagInPipe(kclManager.ast, pathToNode) return ( {({ open }) => ( <> )} ) } const ConstraintSymbol = ({ constrainInfo: { type: _type, isConstrained, value, pathToNode, argPosition }, verticalPosition, }: { constrainInfo: ConstrainInfo verticalPosition: 'top' | 'bottom' }) => { const { context, send } = 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', }, // 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 = _type in varNameMap ? varNameMap[_type as LineInputsType].varName : 'var' const name: CustomIconName = varNameMap[_type as LineInputsType].iconName const displayName = varNameMap[_type as LineInputsType]?.displayName const implicitDesc = varNameMap[_type as LineInputsType]?.implicitConstraintDesc const node = useMemo( () => getNodeFromPath(kclManager.ast, pathToNode).node, [kclManager.ast, pathToNode] ) const range: SourceRange = node ? [node.start, node.end] : [0, 0] 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) } }, 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 { if (camSettings.type === 'perspective') { sceneInfra.camControls.useOrthographicCamera() } else { sceneInfra.camControls.usePerspectiveCamera() } }} />
{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), ], }) }} />
) }