Files
modeling-app/src/clientSideScene/ClientSideSceneComp.tsx
Kurt Hutten 834f7133d8 Allow multiple profiles in the same sketch (#5196)
* Revert "Revert multi-profile (#4812)"

This reverts commit efe8089b08.

* fix poor 1000ms wait UX

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores)

* trigger CI

* Add Rust side artifacts for startSketchOn face or plane (#4834)

* Add Rust side artifacts for startSketchOn face or plane

* move ast digging

---------

Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>

* lint

* lint

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-macos-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-16-cores)

* trigger CI

* chore: disabled file watcher which prevents faster file write (#4835)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* partial fixes

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* Fix up all the tests

* Fix partial execution

* wip

* WIP

* wip

* rust changes to make three point confrom to same as others since we're not ready with name params yet

* most of the fix for 3 point circle

* get overlays working for circle three point

* fmt

* fix types

* cargo fmt

* add face codef ref for walls and caps

* fix sketch on face after updates to rust side artifact graph

* some things needed for multi-profile tests

* bad attempts at fixing rust

* more

* more

* fix rust

* more rust fixes

* overlay fix

* remove duplicate test

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* lint and typing

* maybe fix a unit test

* small thing

* fix circ dep

* fix unit test

* fix some tests

* fix sweep point-and-click test

* fix more tests and add a fix me

* fix more tests

* fix electron specific test

* tsc

* more test tweaks

* update docs

* commint snaps?

* is clippy happy now?

* clippy again

* test works now without me changing anything big-fixed-itself

* small bug

* make three point have cross hair to make it consistent with othe rtools

* fix up state diagram

* fmt

* add draft point for first click of three point circ

* 1 test for three point circle

* 2 test for three point circle

* clean up

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* remove bad doc comment

* remove test skip

* remove onboarding test changes

* Update src/lang/modifyAst.ts

Co-authored-by: Jonathan Tran <jonnytran@gmail.com>

* Update output from simulation tests

* Fix to use correct source ranges

This also reduces cloning.

* Change back to skipping face cap none and both

* Update output after changing back to skipping none and both

* Fix clippy warning

* fix profile start snap bug

* add path ids to cap

* fix going into edit sketch

* make other startSketchOn's work

* fix snapshot test

* explain function name

* Update src/lib/rectangleTool.ts

Co-authored-by: Frank Noirot <frank@zoo.dev>

* rename error

* remove file tree from diff

* Update src/clientSideScene/segments.ts

Co-authored-by: Frank Noirot <frank@zoo.dev>

* nit

* Prevent double write to KCL code on revolve

* Update output after adding cap-to-path graph edge

* Fix edit/select sketch-on-cap via feature tree

* clean up for face codeRef

* fix changing tools part way through circle/rect tools

* fix delete of circle profile

* fix close profiles

* fix closing profile bug (tangentArcTo being ignored)

* remove stale comment

* Delete paths associated with sketch when the sketch plane is deleted

* Add support for deleting sketches on caps (not walls)

* get delet working for walls

* make delet of extrusions work for multi profile

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Delete the sketch statement too on the cap and wall cases

* Don't write to file in `split-sketch-pipe-if-needed` unless necessary

* Don't wait for file write to complete within `updateEditorWithAstAndWriteToFile`
It is already debounced internally. If we await it, we will have to wait for a debounced timeout

* docs

* fix circ dep

* tsc

* fix selection enter sketch weirdness

* test fixes

* comment out and fixme for delete related tests

* add skip wins

* try and get last test to pass

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
Co-authored-by: Kevin Nadro <nadr0@users.noreply.github.com>
Co-authored-by: Pierre Jacquier <pierre@zoo.dev>
Co-authored-by: 49lf <ircsurfer33@gmail.com>
Co-authored-by: Frank Noirot <frank@zoo.dev>
Co-authored-by: Frank Noirot <frankjohnson1993@gmail.com>
2025-02-14 08:57:04 -05:00

995 lines
30 KiB
TypeScript

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, toSync } 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,
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 'wasm-lib/kcl/bindings/Node'
import { commandBarActor } from 'machines/commandBarMachine'
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<HTMLDivElement>(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' })
) {
cursor = 'crosshair'
} else {
cursor = 'default'
}
}
return (
<>
<div
ref={canvasRef}
style={{ cursor: cursor }}
data-testid="client-side-scene"
className={`absolute inset-0 h-full w-full transition-all duration-300 ${
hideClient ? 'opacity-0' : 'opacity-100'
} ${hideServer ? 'bg-chalkboard-10 dark:bg-chalkboard-100' : ''} ${
!hideClient && !hideServer && state.matches('Sketch')
? 'bg-chalkboard-10/80 dark:bg-chalkboard-100/80'
: ''
}`}
></div>
<Overlays />
</>
)
}
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 (
<div
className="absolute inset-0 pointer-events-none"
style={{ zIndex: '99999999' }}
>
{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 (
<Overlay
overlay={overlay}
key={pathToNodeString + String(index)}
pathToNodeString={pathToNodeString}
overlayIndex={index}
/>
)
})}
</div>
)
}
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<Node<CallExpression | CallExpressionKw>>(
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 (
<div className={`absolute w-0 h-0`}>
<div
data-testid="segment-overlay"
data-path-to-node={pathToNodeString}
data-overlay-index={overlayIndex}
data-overlay-visible={shouldShow}
data-overlay-angle={overlay.angle}
className="pointer-events-auto absolute w-0 h-0"
style={{
transform: `translate3d(${overlay.windowCoords[0]}px, ${overlay.windowCoords[1]}px, 0)`,
}}
></div>
{shouldShow && (
<div
data-overlay-toolbar-index={overlayIndex}
className={`px-0 pointer-events-auto absolute flex gap-1`}
style={{
transform: `translate3d(calc(${
overlay.windowCoords[0] + xOffset
}px + ${xAlignment}), calc(${
overlay.windowCoords[1] - yOffset
}px + ${yAlignment}), 0)`,
}}
onMouseEnter={() =>
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) => (
<ConstraintSymbol
constrainInfo={constraintInfo}
key={i}
verticalPosition={
overlay.windowCoords[1] > 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' && (
<SegmentMenu
verticalPosition={
overlay.windowCoords[1] > window.innerHeight / 2
? 'top'
: 'bottom'
}
pathToNode={overlay.pathToNode}
stdLibFnName={constraints[0]?.stdLibFnName}
/>
)}
</div>
)}
</div>
)
}
type ConfirmModalProps = InstanceProps<boolean, boolean> & { text: string }
export const ConfirmModal = ({
isOpen,
onResolve,
onReject,
text,
}: ConfirmModalProps) => {
return (
<Transition appear show={isOpen} as={Fragment}>
<Dialog
as="div"
className="relative z-10"
onClose={() => onResolve(false)}
>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-black bg-opacity-25" />
</Transition.Child>
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="rounded relative mx-auto px-4 py-8 bg-chalkboard-10 dark:bg-chalkboard-100 border dark:border-chalkboard-70 max-w-xl w-full shadow-lg">
<div>{text}</div>
<div className="mt-8 flex justify-between">
<ActionButton
Element="button"
onClick={() => onResolve(true)}
>
Continue and unconstrain
</ActionButton>
<ActionButton
Element="button"
onClick={() => onReject(false)}
>
Cancel
</ActionButton>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition>
)
}
export const confirmModal = create<ConfirmModalProps, boolean, boolean>(
ConfirmModal
)
export async function deleteSegment({
pathToNode,
sketchDetails,
}: {
pathToNode: PathToNode
sketchDetails: SketchDetails | null
}) {
let modifiedAst: Node<Program> | 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,
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 (
<Popover className="relative">
{({ open }) => (
<>
<Popover.Button
data-testid="overlay-menu"
data-stdlib-fn-name={stdLibFnName}
className="bg-chalkboard-10 dark:bg-chalkboard-100 border !border-transparent hover:!border-chalkboard-40 dark:hover:!border-chalkboard-70 ui-open:!border-chalkboard-40 dark:ui-open:!border-chalkboard-70 h-[26px] w-[26px] rounded-sm p-0 m-0"
>
<CustomIcon name={'three-dots'} />
</Popover.Button>
<Popover.Panel
as="menu"
className={`absolute ${
verticalPosition === 'top' ? 'bottom-full' : 'top-full'
} z-10 w-36 flex flex-col gap-1 divide-y divide-chalkboard-20 dark:divide-chalkboard-70 align-stretch px-0 py-1 bg-chalkboard-10 dark:bg-chalkboard-100 rounded-sm shadow-lg border border-solid border-chalkboard-20/50 dark:border-chalkboard-80/50`}
>
<button
className="!border-transparent rounded-sm text-left p-1 text-nowrap"
onClick={() => {
send({ type: 'Constrain remove constraints', data: pathToNode })
}}
>
Remove constraints
</button>
<button
className="!border-transparent rounded-sm text-left p-1 text-nowrap"
title={
dependentSourceRanges.length > 0
? `At least ${dependentSourceRanges.length} segment rely on this segment's tag.`
: ''
}
onClick={() => {
send({ type: 'Delete segment', data: pathToNode })
}}
>
Delete Segment
</button>
</Popover.Panel>
</>
)}
</Popover>
)
}
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<Expr>(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 (
<div className="relative group">
<button
data-testid="constraint-symbol"
data-is-implicit-constraint={implicitDesc ? 'true' : 'false'}
data-constraint-type={_type}
data-is-constrained={isConstrained ? 'true' : 'false'}
className={`${
implicitDesc
? 'bg-chalkboard-10 dark:bg-chalkboard-100 border-transparent border-0 rounded'
: isConstrained
? 'bg-chalkboard-10 dark:bg-chalkboard-90 dark:hover:bg-chalkboard-80 border-chalkboard-40 dark:border-chalkboard-70 rounded-sm'
: 'bg-primary/30 dark:bg-primary text-primary dark:text-chalkboard-10 dark:border-transparent group-hover:bg-primary/40 group-hover:border-primary/50 group-hover:brightness-125'
} h-[26px] w-[26px] rounded-sm relative m-0 p-0`}
onMouseEnter={() => {
editorManager.setHighlightRange([range])
}}
onMouseLeave={() => {
editorManager.setHighlightRange([defaultSourceRange()])
}}
// disabled={isConstrained || !convertToVarEnabled}
// disabled={implicitDesc} TODO why does this change styles that are hard to override?
onClick={toSync(async () => {
if (!isConstrained) {
commandBarActor.send({
type: 'Find and select command',
data: {
name: 'Constrain with named value',
groupId: 'modeling',
argDefaultValues: {
currentValue: {
pathToNode,
variableName: varName,
valueText: value,
},
},
},
})
} else if (isConstrained) {
try {
const pResult = parse(recast(kclManager.ast))
if (trap(pResult) || !resultIsOk(pResult))
return Promise.reject(pResult)
const _node1 = getNodeFromPath<CallExpression | CallExpressionKw>(
pResult.program!,
pathToNode,
['CallExpression', 'CallExpressionKw'],
true
)
if (trap(_node1)) return Promise.reject(_node1)
const shallowPath = _node1.shallowPath
if (!context.sketchDetails || !argPosition) return
const transform = removeSingleConstraintInfo(
shallowPath,
argPosition,
kclManager.ast,
kclManager.variables
)
if (!transform) return
const { modifiedAst } = transform
await kclManager.updateAst(modifiedAst, true)
// Code editor will be updated in the modelingMachine.
const newCode = recast(modifiedAst)
if (err(newCode)) return
await codeManager.updateCodeEditor(newCode)
} catch (e) {
console.log('error', e)
}
toast.success('Constraint removed')
}
}, reportRejection)}
>
<CustomIcon name={name} />
</button>
<div
className={`absolute ${
verticalPosition === 'top'
? 'top-0 -translate-y-full'
: 'bottom-0 translate-y-full'
} group-hover:block hidden w-[2px] h-2 translate-x-[12px] bg-white/40`}
></div>
<div
className={`absolute ${
verticalPosition === 'top' ? 'top-0' : 'bottom-0'
} group-hover:block hidden`}
style={{
transform: `translate3d(calc(-50% + 13px), ${
verticalPosition === 'top' ? '-100%' : '100%'
}, 0)`,
}}
>
<div
className="bg-chalkboard-10 dark:bg-chalkboard-90 p-2 px-3 rounded-sm border border-solid border-chalkboard-20 dark:border-chalkboard-80 shadow-sm"
data-testid="constraint-symbol-popover"
>
{implicitDesc ? (
<div className="min-w-48">
<pre className="inline-block">
<code className="text-primary">{value}</code>
</pre>{' '}
<span>is implicitly constrained {implicitDesc}</span>
</div>
) : (
<>
<div className="flex mb-1">
<span className="text-nowrap">
<span className="font-bold">
{isConstrained ? 'Constrained' : 'Unconstrained'}
</span>
<span className="text-white/80 text-sm pl-2">
{displayName}
</span>
</span>
</div>
<div className="flex mb-1">
<span className="pr-2 whitespace-nowrap">Set to</span>
<pre>
<code className="text-primary">{value}</code>
</pre>
</div>
<div className="text-sm text-chalkboard-70 dark:text-chalkboard-40 text-nowrap">
{isConstrained
? 'Click to unconstrain with raw number'
: 'Click to constrain with variable'}
</div>
</>
)}
</div>
</div>
</div>
)
}
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<ReactCameraProperties>(
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 (
<div>
<h3>cam settings</h3>
perspective cam
<input
type="checkbox"
checked={camSettings.type === 'perspective'}
onChange={() =>
commandBarActor.send({
type: 'Find and select command',
data: {
groupId: 'settings',
name: 'modeling.cameraProjection',
},
})
}
/>
<div>
<button
onClick={() => {
sceneInfra.camControls.resetCameraPosition().catch(reportRejection)
}}
>
Reset Camera Position
</button>
</div>
{camSettings.type === 'perspective' && (
<input
type="range"
min="4"
max="90"
step={0.5}
value={fov}
onChange={(e) => {
setFov(parseFloat(e.target.value))
throttled({
...camSettings,
fov: parseFloat(e.target.value),
})
}}
className="w-full cursor-pointer pointer-events-auto"
/>
)}
{camSettings.type === 'perspective' && (
<div>
<span>fov</span>
<input
type="number"
value={camSettings.fov}
className="text-black w-16"
onChange={(e) => {
sceneInfra.camControls.setCam({
...camSettings,
fov: parseFloat(e.target.value),
})
}}
/>
</div>
)}
{camSettings.type === 'orthographic' && (
<>
<div>
<span>fov</span>
<input
type="number"
value={camSettings.zoom}
className="text-black w-16"
onChange={(e) => {
sceneInfra.camControls.setCam({
...camSettings,
zoom: parseFloat(e.target.value),
})
}}
/>
</div>
</>
)}
<div>
Position
<ul className="flex">
<li>
<span className="pl-2 pr-1">x:</span>
<input
type="number"
step={5}
data-testid="cam-x-position"
value={camSettings.position[0]}
className="text-black w-16"
onChange={(e) => {
sceneInfra.camControls.setCam({
...camSettings,
position: [
parseFloat(e.target.value),
camSettings.position[1],
camSettings.position[2],
],
})
}}
/>
</li>
<li>
<span className="pl-2 pr-1">y:</span>
<input
type="number"
step={5}
data-testid="cam-y-position"
value={camSettings.position[1]}
className="text-black w-16"
onChange={(e) => {
sceneInfra.camControls.setCam({
...camSettings,
position: [
camSettings.position[0],
parseFloat(e.target.value),
camSettings.position[2],
],
})
}}
/>
</li>
<li>
<span className="pl-2 pr-1">z:</span>
<input
type="number"
step={5}
data-testid="cam-z-position"
value={camSettings.position[2]}
className="text-black w-16"
onChange={(e) => {
sceneInfra.camControls.setCam({
...camSettings,
position: [
camSettings.position[0],
camSettings.position[1],
parseFloat(e.target.value),
],
})
}}
/>
</li>
</ul>
</div>
<div>
target
<ul className="flex">
<li>
<span className="pl-2 pr-1">x:</span>
<input
type="number"
step={5}
data-testid="cam-x-target"
value={camSettings.target[0]}
className="text-black w-16"
onChange={(e) => {
sceneInfra.camControls.setCam({
...camSettings,
target: [
parseFloat(e.target.value),
camSettings.target[1],
camSettings.target[2],
],
})
}}
/>
</li>
<li>
<span className="pl-2 pr-1">y:</span>
<input
type="number"
step={5}
data-testid="cam-y-target"
value={camSettings.target[1]}
className="text-black w-16"
onChange={(e) => {
sceneInfra.camControls.setCam({
...camSettings,
target: [
camSettings.target[0],
parseFloat(e.target.value),
camSettings.target[2],
],
})
}}
/>
</li>
<li>
<span className="pl-2 pr-1">z:</span>
<input
type="number"
step={5}
data-testid="cam-z-target"
value={camSettings.target[2]}
className="text-black w-16"
onChange={(e) => {
sceneInfra.camControls.setCam({
...camSettings,
target: [
camSettings.target[0],
camSettings.target[1],
parseFloat(e.target.value),
],
})
}}
/>
</li>
</ul>
</div>
</div>
)
}