Symbols overlay (#2033)
* start of overlay work
* add new icons
* add constraint symbols
* add three dots
* add primary colours
* refactor how we get constraint info for overlays
* refactor how we get constraint info for overlays
* get symbols working for tangential arc too
* extra data on constraint info
* add initial delete
* fix types and circular dep issue after rebase
* fix quirk with horz vert line overlays
* fix setup and tear down of overlays
* remove overlays that are too small
* throttle overlay updates and prove tests selecting html instead of hardcoded px coords
* initial show overaly on segment hover
* remove overlays when tool is equipped
* dounce overlay updates
* tsc
* make higlighting robust to small changes in source ranges
* replace with variable for unconstrained values, and improve styles for popover
* background tweak
* make overlays unconstrain inputs
* fix small regression
* write query for finding related tag references
* make delete segment safe
* typo
* un used imports
* test deleteSegmentFromPipeExpression
* add getConstraintInfo test
* test removeSingleConstraintInfo
* more tests
* tsc
* add tests for overlay buttons
* rename tests
* fmt
* better naming structure
* more reliablity
* more test tweaks
* fix selection test
* add delete segments with overlays tests
* dependant tag tests for segment delet
* typo
* test clean up
* fix some perf issus
* clean up
* clean up
* make things a little more dry
* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)
* trigger ci
* Make constraint hover popovers readable on light mode
* Touch up the new variable dialog
* Little touch-up to three-dot menu style
* fix highlight issue
* fmt
* use optional chain
* Revert "A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)"
This reverts commit be3d61e4a3
.
* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)
* disable var panel in sketch mode
* fix overlay tests after mergi in main
* test tweak
* try fix ubuntu
* fmt
* more test tweaks
* tweak
* tweaks
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Frank Noirot <frank@kittycad.io>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
Binary file not shown.
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
@ -130,6 +130,11 @@ export async function getUtils(page: Page) {
|
||||
},
|
||||
waitForCmdReceive: (commandType: string) =>
|
||||
waitForCmdReceive(page, commandType),
|
||||
getBoundingBox: async (locator: string) =>
|
||||
page
|
||||
.locator(locator)
|
||||
.boundingBox()
|
||||
.then((box) => ({ x: box?.x || 0, y: box?.y || 0 })),
|
||||
doAndWaitForCmd: async (
|
||||
fn: () => Promise<void>,
|
||||
commandType: string,
|
||||
|
@ -15,10 +15,12 @@ import { ActionButtonDropdown } from 'components/ActionButtonDropdown'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import Tooltip from 'components/Tooltip'
|
||||
|
||||
export const Toolbar = () => {
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
export function Toolbar({
|
||||
className = '',
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLElement>) {
|
||||
const { state, send, context } = useModelingContext()
|
||||
const toolbarButtonsRef = useRef<HTMLUListElement>(null)
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
const iconClassName =
|
||||
'group-disabled:text-chalkboard-50 group-enabled:group-hover:!text-primary dark:group-enabled:group-hover:!text-inherit group-pressed:!text-chalkboard-10 group-ui-open:!text-chalkboard-10 dark:group-ui-open:!text-chalkboard-10'
|
||||
const bgClassName =
|
||||
@ -34,6 +36,9 @@ export const Toolbar = () => {
|
||||
context.selectionRanges
|
||||
)
|
||||
}, [engineCommandManager.artifactMap, context.selectionRanges])
|
||||
|
||||
const toolbarButtonsRef = useRef<HTMLUListElement>(null)
|
||||
|
||||
const { overallState } = useNetworkStatus()
|
||||
const { isExecuting } = useKclContext()
|
||||
const { isStreamReady } = useStore((s) => ({
|
||||
@ -100,12 +105,45 @@ export const Toolbar = () => {
|
||||
|
||||
span.scrollLeft = span.scrollLeft += ev.deltaY
|
||||
}
|
||||
const nextEvents = useMemo(() => state.nextEvents, [state.nextEvents])
|
||||
const splitMenuItems = useMemo(
|
||||
() =>
|
||||
nextEvents
|
||||
.filter(
|
||||
(eventName) =>
|
||||
eventName.includes('Make segment') ||
|
||||
eventName.includes('Constrain')
|
||||
)
|
||||
.sort((a, b) => {
|
||||
const aisEnabled = nextEvents
|
||||
.filter((event) => state.can(event as any))
|
||||
.includes(a)
|
||||
const bIsEnabled = nextEvents
|
||||
.filter((event) => state.can(event as any))
|
||||
.includes(b)
|
||||
if (aisEnabled && !bIsEnabled) {
|
||||
return -1
|
||||
}
|
||||
if (!aisEnabled && bIsEnabled) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
.map((eventName) => ({
|
||||
label: eventName
|
||||
.replace('Make segment ', '')
|
||||
.replace('Constrain ', ''),
|
||||
onClick: () => send(eventName),
|
||||
disabled:
|
||||
!nextEvents
|
||||
.filter((event) => state.can(event as any))
|
||||
.includes(eventName) || disableAllButtons,
|
||||
})),
|
||||
|
||||
function ToolbarButtons({
|
||||
className = '',
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLElement>) {
|
||||
[JSON.stringify(nextEvents), state]
|
||||
)
|
||||
return (
|
||||
<menu className="max-w-full whitespace-nowrap rounded px-1.5 py-0.5 backdrop-blur-sm bg-chalkboard-10/80 dark:bg-chalkboard-110/70 relative">
|
||||
<ul
|
||||
{...props}
|
||||
ref={toolbarButtonsRef}
|
||||
@ -113,7 +151,7 @@ export const Toolbar = () => {
|
||||
className={'m-0 py-1 rounded-l-sm flex gap-2 items-center ' + className}
|
||||
style={{ scrollbarWidth: 'thin' }}
|
||||
>
|
||||
{state.nextEvents.includes('Enter sketch') && (
|
||||
{nextEvents.includes('Enter sketch') && (
|
||||
<li className="contents">
|
||||
<ActionButton
|
||||
className={buttonClassName}
|
||||
@ -139,7 +177,7 @@ export const Toolbar = () => {
|
||||
</ActionButton>
|
||||
</li>
|
||||
)}
|
||||
{state.nextEvents.includes('Enter sketch') && pathId && (
|
||||
{nextEvents.includes('Enter sketch') && pathId && (
|
||||
<li className="contents">
|
||||
<ActionButton
|
||||
className={buttonClassName}
|
||||
@ -163,7 +201,7 @@ export const Toolbar = () => {
|
||||
</ActionButton>
|
||||
</li>
|
||||
)}
|
||||
{state.nextEvents.includes('Cancel') && !state.matches('idle') && (
|
||||
{nextEvents.includes('Cancel') && !state.matches('idle') && (
|
||||
<li className="contents">
|
||||
<ActionButton
|
||||
className={buttonClassName}
|
||||
@ -286,43 +324,13 @@ export const Toolbar = () => {
|
||||
</>
|
||||
)}
|
||||
{state.matches('Sketch.SketchIdle') &&
|
||||
state.nextEvents.filter(
|
||||
nextEvents.filter(
|
||||
(eventName) =>
|
||||
eventName.includes('Make segment') ||
|
||||
eventName.includes('Constrain')
|
||||
).length > 0 && (
|
||||
<ActionButtonDropdown
|
||||
splitMenuItems={state.nextEvents
|
||||
.filter(
|
||||
(eventName) =>
|
||||
eventName.includes('Make segment') ||
|
||||
eventName.includes('Constrain')
|
||||
)
|
||||
.sort((a, b) => {
|
||||
const aisEnabled = state.nextEvents
|
||||
.filter((event) => state.can(event as any))
|
||||
.includes(a)
|
||||
const bIsEnabled = state.nextEvents
|
||||
.filter((event) => state.can(event as any))
|
||||
.includes(b)
|
||||
if (aisEnabled && !bIsEnabled) {
|
||||
return -1
|
||||
}
|
||||
if (!aisEnabled && bIsEnabled) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
.map((eventName) => ({
|
||||
label: eventName
|
||||
.replace('Make segment ', '')
|
||||
.replace('Constrain ', ''),
|
||||
onClick: () => send(eventName),
|
||||
disabled:
|
||||
!state.nextEvents
|
||||
.filter((event) => state.can(event as any))
|
||||
.includes(eventName) || disableAllButtons,
|
||||
}))}
|
||||
splitMenuItems={splitMenuItems}
|
||||
className={buttonClassName}
|
||||
Element="button"
|
||||
iconStart={{
|
||||
@ -369,12 +377,6 @@ export const Toolbar = () => {
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<menu className="max-w-full whitespace-nowrap rounded px-1.5 py-0.5 backdrop-blur-sm bg-chalkboard-10/80 dark:bg-chalkboard-110/70 relative">
|
||||
<ToolbarButtons />
|
||||
</menu>
|
||||
)
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useRef, useEffect, useState } from 'react'
|
||||
import { useRef, useEffect, useState, useMemo, Fragment } from 'react'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
|
||||
import { cameraMouseDragGuards } from 'lib/cameraControls'
|
||||
@ -6,12 +6,44 @@ import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { ARROWHEAD, DEBUG_SHOW_BOTH_SCENES } from './sceneInfra'
|
||||
import { ReactCameraProperties } from './CameraControls'
|
||||
import { throttle } from 'lib/utils'
|
||||
import { sceneInfra } from 'lib/singletons'
|
||||
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)
|
||||
@ -100,6 +132,7 @@ export const ClientSideScene = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
ref={canvasRef}
|
||||
style={{ cursor: cursor }}
|
||||
@ -111,6 +144,519 @@ export const ClientSideScene = ({
|
||||
: ''
|
||||
}`}
|
||||
></div>
|
||||
<Overlays />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const Overlays = () => {
|
||||
const { context } = useModelingContext()
|
||||
if (context.mouseState.type === 'isDragging') return null
|
||||
return (
|
||||
<div className="absolute inset-0 pointer-events-none">
|
||||
{Object.entries(context.segmentOverlays)
|
||||
.filter((a) => a[1].visible)
|
||||
.map(([pathToNodeString, overlay], index) => {
|
||||
return (
|
||||
<Overlay
|
||||
overlay={overlay}
|
||||
key={pathToNodeString}
|
||||
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%'
|
||||
|
||||
const callExpression = getNodeFromPath<CallExpression>(
|
||||
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 (
|
||||
<div className={`absolute w-0 h-0`}>
|
||||
<div
|
||||
data-testid="segment-overlay"
|
||||
data-path-to-node={pathToNodeString}
|
||||
data-overlay-index={overlayIndex}
|
||||
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
|
||||
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'
|
||||
}
|
||||
/>
|
||||
))}
|
||||
<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: 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 (
|
||||
<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="hover:bg-white/80 bg-white/50 rounded p-1 text-nowrap">
|
||||
Remove segment constraints
|
||||
</button> */}
|
||||
<button
|
||||
className="!border-transparent rounded-sm text-left p-1 text-nowrap"
|
||||
// disabled={dependentSourceRanges.length > 0}
|
||||
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, 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<Value>(parse(recast(kclManager.ast)), pathToNode).node,
|
||||
[kclManager.ast, pathToNode]
|
||||
)
|
||||
const range: SourceRange = node ? [node.start, node.end] : [0, 0]
|
||||
|
||||
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([0, 0])
|
||||
}}
|
||||
// disabled={isConstrained || !convertToVarEnabled}
|
||||
// disabled={implicitDesc} TODO why does this change styles that are hard to override?
|
||||
onClick={async () => {
|
||||
if (!isConstrained) {
|
||||
send({
|
||||
type: 'Convert to variable',
|
||||
data: {
|
||||
pathToNode,
|
||||
variableName: varName,
|
||||
},
|
||||
})
|
||||
} else if (isConstrained) {
|
||||
try {
|
||||
const shallowPath = getNodeFromPath<CallExpression>(
|
||||
parse(recast(kclManager.ast)),
|
||||
pathToNode,
|
||||
'CallExpression',
|
||||
true
|
||||
).shallowPath
|
||||
const input = makeRemoveSingleConstraintInput(
|
||||
argPosition,
|
||||
shallowPath
|
||||
)
|
||||
if (!input || !context.sketchDetails) return
|
||||
const transform = removeSingleConstraintInfo(
|
||||
input,
|
||||
kclManager.ast,
|
||||
kclManager.programMemory
|
||||
)
|
||||
if (!transform) return
|
||||
const { modifiedAst } = transform
|
||||
kclManager.updateAst(modifiedAst, true)
|
||||
} catch (e) {
|
||||
console.log('error', e)
|
||||
}
|
||||
toast.success('Constraint removed')
|
||||
}
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -75,7 +75,7 @@ import {
|
||||
changeSketchArguments,
|
||||
updateStartProfileAtArgs,
|
||||
} from 'lang/std/sketch'
|
||||
import { roundOff, throttle } from 'lib/utils'
|
||||
import { normaliseAngle, roundOff, throttle } from 'lib/utils'
|
||||
import {
|
||||
createArrayExpression,
|
||||
createCallExpressionStdLib,
|
||||
@ -92,7 +92,7 @@ import { getTangentPointFromPreviousArc } from 'lib/utils2d'
|
||||
import { createGridHelper, orthoScale, perspScale } from './helpers'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import { uuidv4 } from 'lib/utils'
|
||||
import { SketchDetails } from 'machines/modelingMachine'
|
||||
import { SegmentOverlayPayload, SketchDetails } from 'machines/modelingMachine'
|
||||
import { EngineCommandManager } from 'lang/std/engineConnection'
|
||||
import {
|
||||
getRectangleCallExpressions,
|
||||
@ -139,8 +139,8 @@ export class SceneEntities {
|
||||
}
|
||||
onCamChange = () => {
|
||||
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
|
||||
|
||||
Object.values(this.activeSegments).forEach((segment) => {
|
||||
const callbacks: (() => SegmentOverlayPayload | null)[] = []
|
||||
Object.values(this.activeSegments).forEach((segment, index) => {
|
||||
const factor =
|
||||
(sceneInfra.camControls.camera instanceof OrthographicCamera
|
||||
? orthoFactor
|
||||
@ -151,12 +151,14 @@ export class SceneEntities {
|
||||
segment.userData.to &&
|
||||
segment.userData.type === STRAIGHT_SEGMENT
|
||||
) {
|
||||
callbacks.push(
|
||||
this.updateStraightSegment({
|
||||
from: segment.userData.from,
|
||||
to: segment.userData.to,
|
||||
group: segment,
|
||||
scale: factor,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
@ -165,6 +167,7 @@ export class SceneEntities {
|
||||
segment.userData.prevSegment &&
|
||||
segment.userData.type === TANGENTIAL_ARC_TO_SEGMENT
|
||||
) {
|
||||
callbacks.push(
|
||||
this.updateTangentialArcToSegment({
|
||||
prevSegment: segment.userData.prevSegment,
|
||||
from: segment.userData.from,
|
||||
@ -172,6 +175,7 @@ export class SceneEntities {
|
||||
group: segment,
|
||||
scale: factor,
|
||||
})
|
||||
)
|
||||
}
|
||||
if (segment.name === PROFILE_START) {
|
||||
segment.scale.set(factor, factor, factor)
|
||||
@ -187,6 +191,7 @@ export class SceneEntities {
|
||||
const y = this.axisGroup.getObjectByName(Y_AXIS)
|
||||
y?.scale.set(factor / sceneInfra._baseUnitMultiplier, 1, 1)
|
||||
}
|
||||
sceneInfra.overlayCallbacks(callbacks)
|
||||
}
|
||||
|
||||
createIntersectionPlane() {
|
||||
@ -366,7 +371,7 @@ export class SceneEntities {
|
||||
})
|
||||
group.add(_profileStart)
|
||||
this.activeSegments[JSON.stringify(segPathToNode)] = _profileStart
|
||||
|
||||
const callbacks: (() => SegmentOverlayPayload | null)[] = []
|
||||
sketchGroup.value.forEach((segment, index) => {
|
||||
let segPathToNode = getNodePathFromSourceRange(
|
||||
maybeModdedAst,
|
||||
@ -411,6 +416,15 @@ export class SceneEntities {
|
||||
texture: sceneInfra.extraSegmentTexture,
|
||||
theme: sceneInfra._theme,
|
||||
})
|
||||
callbacks.push(
|
||||
this.updateTangentialArcToSegment({
|
||||
prevSegment: sketchGroup.value[index - 1],
|
||||
from: segment.from,
|
||||
to: segment.to,
|
||||
group: seg,
|
||||
scale: factor,
|
||||
})
|
||||
)
|
||||
} else {
|
||||
seg = straightSegment({
|
||||
from: segment.from,
|
||||
@ -423,6 +437,14 @@ export class SceneEntities {
|
||||
texture: sceneInfra.extraSegmentTexture,
|
||||
theme: sceneInfra._theme,
|
||||
})
|
||||
callbacks.push(
|
||||
this.updateStraightSegment({
|
||||
from: segment.from,
|
||||
to: segment.to,
|
||||
group: seg,
|
||||
scale: factor,
|
||||
})
|
||||
)
|
||||
}
|
||||
seg.layers.set(SKETCH_LAYER)
|
||||
seg.traverse((child) => {
|
||||
@ -447,6 +469,7 @@ export class SceneEntities {
|
||||
this.intersectionPlane.position.set(...position)
|
||||
this.scene.add(group)
|
||||
sceneInfra.camControls.enableRotate = false
|
||||
sceneInfra.overlayCallbacks(callbacks)
|
||||
|
||||
return {
|
||||
truncatedAst,
|
||||
@ -1019,7 +1042,8 @@ export class SceneEntities {
|
||||
orthoFactor,
|
||||
sketchGroup
|
||||
)
|
||||
sgPaths.forEach((group, index) =>
|
||||
|
||||
const callBacks = sgPaths.map((group, index) =>
|
||||
this.updateSegment(
|
||||
group,
|
||||
index,
|
||||
@ -1029,6 +1053,7 @@ export class SceneEntities {
|
||||
sketchGroup
|
||||
)
|
||||
)
|
||||
sceneInfra.overlayCallbacks(callBacks)
|
||||
})()
|
||||
}
|
||||
|
||||
@ -1049,7 +1074,7 @@ export class SceneEntities {
|
||||
modifiedAst: Program,
|
||||
orthoFactor: number,
|
||||
sketchGroup: SketchGroup
|
||||
) => {
|
||||
): (() => SegmentOverlayPayload | null) => {
|
||||
const segPathToNode = getNodePathFromSourceRange(
|
||||
modifiedAst,
|
||||
segment.__geoMeta.sourceRange
|
||||
@ -1070,7 +1095,7 @@ export class SceneEntities {
|
||||
: perspScale(sceneInfra.camControls.camera, group)) /
|
||||
sceneInfra._baseUnitMultiplier
|
||||
if (type === TANGENTIAL_ARC_TO_SEGMENT) {
|
||||
this.updateTangentialArcToSegment({
|
||||
return this.updateTangentialArcToSegment({
|
||||
prevSegment: sgPaths[index - 1],
|
||||
from: segment.from,
|
||||
to: segment.to,
|
||||
@ -1078,7 +1103,7 @@ export class SceneEntities {
|
||||
scale: factor,
|
||||
})
|
||||
} else if (type === STRAIGHT_SEGMENT) {
|
||||
this.updateStraightSegment({
|
||||
return this.updateStraightSegment({
|
||||
from: segment.from,
|
||||
to: segment.to,
|
||||
group,
|
||||
@ -1088,6 +1113,7 @@ export class SceneEntities {
|
||||
group.position.set(segment.from[0], segment.from[1], 0)
|
||||
group.scale.set(factor, factor, factor)
|
||||
}
|
||||
return () => null
|
||||
}
|
||||
|
||||
updateTangentialArcToSegment({
|
||||
@ -1102,7 +1128,7 @@ export class SceneEntities {
|
||||
to: [number, number]
|
||||
group: Group
|
||||
scale?: number
|
||||
}) {
|
||||
}): () => SegmentOverlayPayload | null {
|
||||
group.userData.from = from
|
||||
group.userData.to = to
|
||||
group.userData.prevSegment = prevSegment
|
||||
@ -1190,6 +1216,18 @@ export class SceneEntities {
|
||||
scale,
|
||||
})
|
||||
}
|
||||
const angle = normaliseAngle(
|
||||
(arcInfo.endAngle * 180) / Math.PI + (arcInfo.ccw ? 90 : -90)
|
||||
)
|
||||
return () =>
|
||||
sceneInfra.updateOverlayDetails({
|
||||
arrowGroup,
|
||||
group,
|
||||
isHandlesVisible,
|
||||
from,
|
||||
to,
|
||||
angle,
|
||||
})
|
||||
}
|
||||
throttledUpdateDashedArcGeo = throttle(
|
||||
(
|
||||
@ -1210,7 +1248,7 @@ export class SceneEntities {
|
||||
to: [number, number]
|
||||
group: Group
|
||||
scale?: number
|
||||
}) {
|
||||
}): () => SegmentOverlayPayload | null {
|
||||
group.userData.from = from
|
||||
group.userData.to = to
|
||||
const shape = new Shape()
|
||||
@ -1287,6 +1325,14 @@ export class SceneEntities {
|
||||
scale
|
||||
)
|
||||
}
|
||||
return () =>
|
||||
sceneInfra.updateOverlayDetails({
|
||||
arrowGroup,
|
||||
group,
|
||||
isHandlesVisible,
|
||||
from,
|
||||
to,
|
||||
})
|
||||
}
|
||||
async animateAfterSketch() {
|
||||
// if (isReducedMotion()) {
|
||||
@ -1558,6 +1604,14 @@ export class SceneEntities {
|
||||
},
|
||||
}
|
||||
}
|
||||
resetOverlays() {
|
||||
sceneInfra.modelingSend({
|
||||
type: 'Set Segment Overlays',
|
||||
data: {
|
||||
type: 'clear',
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export type DefaultPlaneStr = 'XY' | 'XZ' | 'YZ' | '-XY' | '-XZ' | '-YZ'
|
||||
|
@ -21,14 +21,15 @@ import {
|
||||
TextureLoader,
|
||||
Texture,
|
||||
} from 'three'
|
||||
import { compareVec2Epsilon2 } from 'lang/std/sketch'
|
||||
import { Coords2d, compareVec2Epsilon2 } from 'lang/std/sketch'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import * as TWEEN from '@tweenjs/tween.js'
|
||||
import { Axis } from 'lib/selections'
|
||||
import { type BaseUnit } from 'lib/settings/settingsTypes'
|
||||
import { CameraControls } from './CameraControls'
|
||||
import { EngineCommandManager } from 'lang/std/engineConnection'
|
||||
import { MouseState } from 'machines/modelingMachine'
|
||||
import { MouseState, SegmentOverlayPayload } from 'machines/modelingMachine'
|
||||
import { getAngle, throttle } from 'lib/utils'
|
||||
import { Themes } from 'lib/theme'
|
||||
|
||||
type SendType = ReturnType<typeof useModelingContext>['send']
|
||||
@ -154,8 +155,88 @@ export class SceneInfra {
|
||||
}
|
||||
|
||||
modelingSend: SendType = (() => {}) as any
|
||||
throttledModelingSend: any = (() => {}) as any
|
||||
setSend(send: SendType) {
|
||||
this.modelingSend = send
|
||||
this.throttledModelingSend = throttle(send, 100)
|
||||
}
|
||||
overlayTimeout = 0
|
||||
callbacks: (() => SegmentOverlayPayload | null)[] = []
|
||||
_overlayCallbacks(callbacks: (() => SegmentOverlayPayload | null)[]) {
|
||||
const segmentOverlayPayload: SegmentOverlayPayload = {
|
||||
type: 'set-many',
|
||||
overlays: {},
|
||||
}
|
||||
callbacks.forEach((cb) => {
|
||||
const overlay = cb()
|
||||
if (overlay?.type === 'set-one') {
|
||||
segmentOverlayPayload.overlays[overlay.pathToNodeString] = overlay.seg
|
||||
}
|
||||
})
|
||||
this.modelingSend({
|
||||
type: 'Set Segment Overlays',
|
||||
data: segmentOverlayPayload,
|
||||
})
|
||||
}
|
||||
overlayCallbacks(
|
||||
callbacks: (() => SegmentOverlayPayload | null)[],
|
||||
instant = false
|
||||
) {
|
||||
if (instant) {
|
||||
this._overlayCallbacks(callbacks)
|
||||
return
|
||||
}
|
||||
this.callbacks = callbacks
|
||||
if (this.overlayTimeout) clearTimeout(this.overlayTimeout)
|
||||
this.overlayTimeout = setTimeout(() => {
|
||||
this._overlayCallbacks(this.callbacks)
|
||||
}, 100) as unknown as number
|
||||
}
|
||||
|
||||
overlayThrottleMap: { [pathToNodeString: string]: number } = {}
|
||||
updateOverlayDetails({
|
||||
arrowGroup,
|
||||
group,
|
||||
isHandlesVisible,
|
||||
from,
|
||||
to,
|
||||
angle,
|
||||
}: {
|
||||
arrowGroup: Group
|
||||
group: Group
|
||||
isHandlesVisible: boolean
|
||||
from: Coords2d
|
||||
to: Coords2d
|
||||
angle?: number
|
||||
}): SegmentOverlayPayload | null {
|
||||
if (group.userData.pathToNode && arrowGroup) {
|
||||
const vector = new Vector3(0, 0, 0)
|
||||
|
||||
// Get the position of the object3D in world space
|
||||
// console.log('arrowGroup', arrowGroup)
|
||||
arrowGroup.getWorldPosition(vector)
|
||||
|
||||
// Project that position to screen space
|
||||
vector.project(this.camControls.camera)
|
||||
|
||||
const _angle = typeof angle === 'number' ? angle : getAngle(from, to)
|
||||
|
||||
const x = (vector.x * 0.5 + 0.5) * window.innerWidth
|
||||
const y = (-vector.y * 0.5 + 0.5) * window.innerHeight
|
||||
const pathToNodeString = JSON.stringify(group.userData.pathToNode)
|
||||
return {
|
||||
type: 'set-one',
|
||||
pathToNodeString,
|
||||
seg: {
|
||||
windowCoords: [x, y],
|
||||
angle: _angle,
|
||||
group,
|
||||
pathToNode: group.userData.pathToNode,
|
||||
visible: isHandlesVisible,
|
||||
},
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
hoveredObject: null | any = null
|
||||
|
@ -214,10 +214,7 @@ export const CreateNewVariable = ({
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<label
|
||||
htmlFor="create-new-variable"
|
||||
className="block mt-3 font-mono text-gray-900"
|
||||
>
|
||||
<label htmlFor="create-new-variable" className="block mt-3 font-mono">
|
||||
Create new variable
|
||||
</label>
|
||||
<div className="mt-1 flex gap-2 items-center">
|
||||
@ -228,7 +225,7 @@ export const CreateNewVariable = ({
|
||||
onChange={(e) => {
|
||||
setShouldCreateVariable(e.target.checked)
|
||||
}}
|
||||
className="bg-white text-gray-900"
|
||||
className="bg-chalkboard-10 dark:bg-chalkboard-80"
|
||||
/>
|
||||
)}
|
||||
<input
|
||||
@ -249,7 +246,7 @@ export const CreateNewVariable = ({
|
||||
/>
|
||||
</div>
|
||||
{!isNewVariableNameUnique && (
|
||||
<div className="bg-pink-200 rounded px-2 py-0.5 text-xs">
|
||||
<div className="bg-pink-200 dark:bg-chalkboard-80 dark:text-pink-200 rounded px-2 py-0.5 text-xs">
|
||||
Sorry, that's not a unique variable name. Please try something else
|
||||
</div>
|
||||
)}
|
||||
|
@ -11,6 +11,16 @@ const CustomIconMap = {
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
angle: (
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M15 14L10 14H5.00001H4.0495L4.58799 13.2168L7.33799 9.21675L10.088 5.21674L10.912 5.78326L8.45436 9.35807C9.07972 9.78751 9.54479 10.2461 9.87084 10.8329C10.2065 11.437 10.3723 12.1375 10.4598 13H15V14ZM9.45406 13C9.37153 12.2592 9.23025 11.739 8.99671 11.3186C8.7674 10.9059 8.42873 10.5535 7.88782 10.1821L5.95053 13L9.45406 13Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
arrowDown: (
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
@ -227,6 +237,16 @@ const CustomIconMap = {
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
'intersection-offset': (
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M13.8189 4.34932L4.21895 13.9493L3.51184 13.2422L13.1118 3.64221L13.8189 4.34932ZM7.17419 15.6636L6.48848 16.3493L5.78137 15.6422L6.46709 14.9564L7.17419 15.6636ZM9.57419 13.2636L8.20276 14.635L7.49566 13.9279L8.86709 12.5564L9.57419 13.2636ZM12.0932 13.0433C12.3807 12.8662 12.6223 12.6217 12.796 12.3319L15.8739 15.4098L15.1668 16.1169L12.0932 13.0433ZM14.3742 8.46355L13.0028 9.83498L12.2957 9.12787L13.6671 7.75644L14.3742 8.46355ZM16.0885 6.74927L15.4028 7.43498L14.6957 6.72787L15.3814 6.04216L16.0885 6.74927ZM10.9933 12.754C11.8217 12.754 12.4933 12.0825 12.4933 11.254C12.4933 10.4256 11.8217 9.75404 10.9933 9.75404C10.1649 9.75404 9.49329 10.4256 9.49329 11.254C9.49329 12.0825 10.1649 12.754 10.9933 12.754Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
kcl: (
|
||||
<svg viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
@ -385,6 +405,16 @@ const CustomIconMap = {
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
tangent: (
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M12.73 2.73L9.23398 6.226C9.72614 6.46571 10.1178 6.87964 10.3288 7.38755C10.6321 7.31396 10.949 7.27497 11.275 7.27497C13.4841 7.27497 15.275 9.06583 15.275 11.275C15.275 13.4841 13.4841 15.275 11.275 15.275C9.06587 15.275 7.27501 13.4841 7.27501 11.275C7.27501 10.949 7.314 10.6321 7.38757 10.3288C6.87965 10.1178 6.46571 9.72614 6.226 9.23398L2.72998 12.73L3.43709 13.4371L6.32769 10.5465C6.29298 10.7843 6.27501 11.0275 6.27501 11.275C6.27501 14.0364 8.51358 16.275 11.275 16.275C14.0364 16.275 16.275 14.0364 16.275 11.275C16.275 8.51355 14.0364 6.27497 11.275 6.27497C11.0276 6.27497 10.7843 6.29294 10.5465 6.32765L13.4371 3.4371L12.73 2.73ZM8.26001 9.75C9.08844 9.75 9.76001 9.07843 9.76001 8.25C9.76001 7.42157 9.08844 6.75 8.26001 6.75C7.43158 6.75 6.76001 7.42157 6.76001 8.25C6.76001 9.07843 7.43158 9.75 8.26001 9.75Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
'three-dots': (
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
@ -413,6 +443,42 @@ const CustomIconMap = {
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
xAbsolute: (
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M4 16V4H5L5 16H4ZM8.75069 6.82599C8.97469 6.78866 9.20803 6.79799 9.45069 6.85399C9.91736 6.95666 10.2627 7.17132 10.4867 7.49799C10.524 7.55399 10.552 7.58199 10.5707 7.58199L10.6547 7.46999C10.8787 7.20866 11.1447 7.01732 11.4527 6.89599C11.8074 6.76532 12.162 6.77932 12.5167 6.93799C12.7874 7.07799 12.9787 7.26466 13.0907 7.49799C13.2774 7.87132 13.282 8.26332 13.1047 8.67399C13.03 8.83266 12.9367 8.95399 12.8247 9.03799C12.4887 9.28066 12.1714 9.32732 11.8727 9.17799C11.8167 9.14999 11.77 9.11732 11.7327 9.07999C11.5927 8.93999 11.5367 8.76266 11.5647 8.54799C11.6207 8.12799 11.8354 7.85266 12.2087 7.72199L12.3207 7.67999L12.2647 7.62399C12.0967 7.47466 11.8914 7.43266 11.6487 7.49799C11.63 7.49799 11.6114 7.50266 11.5927 7.51199C11.3127 7.61466 11.0887 7.88532 10.9207 8.32399C10.8274 8.55732 10.58 9.54199 10.1787 11.278C10.132 11.4367 10.1087 11.5347 10.1087 11.572C10.062 11.8613 10.0667 12.0573 10.1227 12.16C10.1974 12.3 10.314 12.398 10.4727 12.454C10.5194 12.4727 10.6174 12.482 10.7667 12.482C10.9067 12.482 11.0094 12.4727 11.0747 12.454C11.3174 12.3793 11.56 12.23 11.8027 12.006C12.092 11.7073 12.2834 11.3807 12.3767 11.026C12.4047 10.9327 12.442 10.8813 12.4887 10.872C12.526 10.8627 12.61 10.858 12.7407 10.858C12.918 10.858 13.0207 10.8673 13.0487 10.886C13.0674 10.9047 13.0767 10.9373 13.0767 10.984C13.0767 11.18 12.9554 11.474 12.7127 11.866C12.7034 11.894 12.6894 11.9173 12.6707 11.936C12.2507 12.58 11.6954 12.9767 11.0047 13.126C10.79 13.1633 10.5474 13.1633 10.2767 13.126C9.80069 13.0233 9.44603 12.8133 9.21269 12.496L9.12869 12.37L9.08669 12.412C8.72269 12.8787 8.31203 13.126 7.85469 13.154C7.32269 13.182 6.92603 12.986 6.66469 12.566C6.60869 12.4913 6.56669 12.4073 6.53869 12.314C6.47336 12.1087 6.45469 11.8893 6.48269 11.656C6.54803 11.2547 6.73469 10.9793 7.04269 10.83C7.34136 10.69 7.60736 10.6853 7.84069 10.816C8.05536 10.9187 8.15336 11.1053 8.13469 11.376C8.10669 11.7493 7.93403 12.02 7.61669 12.188C7.57003 12.216 7.50469 12.244 7.42069 12.272L7.36469 12.286L7.40669 12.328C7.53736 12.4307 7.68669 12.482 7.85469 12.482C7.97603 12.4913 8.10203 12.4587 8.23269 12.384C8.47536 12.216 8.65736 11.9593 8.77869 11.614L9.54869 8.53399C9.61403 8.19799 9.62336 7.96466 9.57669 7.83399C9.53003 7.68466 9.40869 7.57732 9.21269 7.51199C9.02603 7.45599 8.83469 7.45599 8.63869 7.51199C8.36803 7.58666 8.13003 7.72666 7.92469 7.93199C7.65403 8.21199 7.45803 8.52466 7.33669 8.86999C7.30869 8.98199 7.28069 9.05199 7.25269 9.07999C7.23403 9.09866 7.13603 9.10799 6.95869 9.10799H6.69269L6.65069 9.06599C6.62269 9.03799 6.60869 9.00066 6.60869 8.95399C6.63669 8.81399 6.69269 8.65532 6.77669 8.47799C6.86069 8.28199 6.96803 8.09532 7.09869 7.91799C7.24803 7.69399 7.45336 7.48399 7.71469 7.28799C7.72403 7.27866 7.73803 7.26932 7.75669 7.25999C8.06469 7.04532 8.39603 6.90066 8.75069 6.82599ZM15 4L15 16H16L16 4H15Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
xRelative: (
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M8.75069 6.82599C8.97469 6.78866 9.20803 6.79799 9.45069 6.85399C9.91736 6.95666 10.2627 7.17132 10.4867 7.49799C10.524 7.55399 10.552 7.58199 10.5707 7.58199L10.6547 7.46999C10.8787 7.20866 11.1447 7.01732 11.4527 6.89599C11.8074 6.76532 12.162 6.77932 12.5167 6.93799C12.7874 7.07799 12.9787 7.26466 13.0907 7.49799C13.2774 7.87132 13.282 8.26332 13.1047 8.67399C13.03 8.83266 12.9367 8.95399 12.8247 9.03799C12.4887 9.28066 12.1714 9.32732 11.8727 9.17799C11.8167 9.14999 11.77 9.11732 11.7327 9.07999C11.5927 8.93999 11.5367 8.76266 11.5647 8.54799C11.6207 8.12799 11.8354 7.85266 12.2087 7.72199L12.3207 7.67999L12.2647 7.62399C12.0967 7.47466 11.8914 7.43266 11.6487 7.49799C11.63 7.49799 11.6114 7.50266 11.5927 7.51199C11.3127 7.61466 11.0887 7.88532 10.9207 8.32399C10.8274 8.55732 10.58 9.54199 10.1787 11.278C10.132 11.4367 10.1087 11.5347 10.1087 11.572C10.062 11.8613 10.0667 12.0573 10.1227 12.16C10.1974 12.3 10.314 12.398 10.4727 12.454C10.5194 12.4727 10.6174 12.482 10.7667 12.482C10.9067 12.482 11.0094 12.4727 11.0747 12.454C11.3174 12.3793 11.56 12.23 11.8027 12.006C12.092 11.7073 12.2834 11.3807 12.3767 11.026C12.4047 10.9327 12.442 10.8813 12.4887 10.872C12.526 10.8627 12.61 10.858 12.7407 10.858C12.918 10.858 13.0207 10.8673 13.0487 10.886C13.0674 10.9047 13.0767 10.9373 13.0767 10.984C13.0767 11.18 12.9554 11.474 12.7127 11.866C12.7034 11.894 12.6894 11.9173 12.6707 11.936C12.2507 12.58 11.6954 12.9767 11.0047 13.126C10.79 13.1633 10.5474 13.1633 10.2767 13.126C9.80069 13.0233 9.44603 12.8133 9.21269 12.496L9.12869 12.37L9.08669 12.412C8.72269 12.8787 8.31203 13.126 7.85469 13.154C7.32269 13.182 6.92603 12.986 6.66469 12.566C6.60869 12.4913 6.56669 12.4073 6.53869 12.314C6.47336 12.1087 6.45469 11.8893 6.48269 11.656C6.54803 11.2547 6.73469 10.9793 7.04269 10.83C7.34136 10.69 7.60736 10.6853 7.84069 10.816C8.05536 10.9187 8.15336 11.1053 8.13469 11.376C8.10669 11.7493 7.93403 12.02 7.61669 12.188C7.57003 12.216 7.50469 12.244 7.42069 12.272L7.36469 12.286L7.40669 12.328C7.53736 12.4307 7.68669 12.482 7.85469 12.482C7.97603 12.4913 8.10203 12.4587 8.23269 12.384C8.47536 12.216 8.65736 11.9593 8.77869 11.614L9.54869 8.53399C9.61403 8.19799 9.62336 7.96466 9.57669 7.83399C9.53003 7.68466 9.40869 7.57732 9.21269 7.51199C9.02603 7.45599 8.83469 7.45599 8.63869 7.51199C8.36803 7.58666 8.13003 7.72666 7.92469 7.93199C7.65403 8.21199 7.45803 8.52466 7.33669 8.86999C7.30869 8.98199 7.28069 9.05199 7.25269 9.07999C7.23403 9.09866 7.13603 9.10799 6.95869 9.10799H6.69269L6.65069 9.06599C6.62269 9.03799 6.60869 9.00066 6.60869 8.95399C6.63669 8.81399 6.69269 8.65532 6.77669 8.47799C6.86069 8.28199 6.96803 8.09532 7.09869 7.91799C7.24803 7.69399 7.45336 7.48399 7.71469 7.28799C7.72403 7.27866 7.73803 7.26932 7.75669 7.25999C8.06469 7.04532 8.39603 6.90066 8.75069 6.82599Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
yAbsolute: (
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M4 16V4H5L5 16H4ZM7.92469 6.83999C8.11136 6.79332 8.33069 6.79799 8.58269 6.85399C9.05869 6.98466 9.36203 7.26932 9.49269 7.70799C9.53003 7.86666 9.53003 8.06732 9.49269 8.30999C9.47403 8.37532 9.40403 8.56666 9.28269 8.88399C8.94669 9.78932 8.72736 10.4847 8.62469 10.97C8.52203 11.5953 8.57803 12.0293 8.79269 12.272C8.95136 12.4587 9.17536 12.5287 9.46469 12.482C9.88469 12.426 10.2534 12.174 10.5707 11.726L10.6547 11.6L11.1587 9.54199C11.5134 8.15132 11.7047 7.43266 11.7327 7.38599C11.882 7.11532 12.106 6.97532 12.4047 6.96599C12.554 6.96599 12.68 7.00799 12.7827 7.09199C12.8294 7.11066 12.8714 7.16199 12.9087 7.24599C12.9554 7.33932 12.9647 7.45599 12.9367 7.59599C12.9087 7.73599 12.6847 8.65532 12.2647 10.354C11.742 12.3887 11.49 13.3733 11.5087 13.308C11.4154 13.6067 11.2754 13.882 11.0887 14.134C10.566 14.9273 9.87536 15.4593 9.01669 15.73C8.60603 15.87 8.20469 15.912 7.81269 15.856C7.11269 15.7533 6.67403 15.436 6.49669 14.904C6.45936 14.6987 6.46403 14.5073 6.51069 14.33C6.56669 14.0967 6.68336 13.91 6.86069 13.77C7.19669 13.5273 7.51403 13.4807 7.81269 13.63C7.92469 13.6953 8.00869 13.784 8.06469 13.896C8.16736 14.12 8.14869 14.3627 8.00869 14.624C7.91536 14.8013 7.78936 14.932 7.63069 15.016L7.54669 15.072L7.61669 15.1C7.98069 15.2587 8.36336 15.2587 8.76469 15.1C9.26869 14.8947 9.68869 14.442 10.0247 13.742C10.09 13.5927 10.1507 13.4433 10.2067 13.294C10.3187 12.9673 10.3654 12.7993 10.3467 12.79C10.3374 12.79 10.3047 12.8087 10.2487 12.846C10.1087 12.93 9.95936 13 9.80069 13.056C9.28736 13.2333 8.76003 13.2007 8.21869 12.958C8.08803 12.902 7.97136 12.832 7.86869 12.748C7.44869 12.412 7.26669 11.8987 7.32269 11.208C7.36003 10.7507 7.57936 9.98066 7.98069 8.89799C7.99003 8.86066 7.99936 8.82799 8.00869 8.79999C8.13936 8.47332 8.20936 8.27732 8.21869 8.21199C8.33069 7.89466 8.34469 7.67532 8.26069 7.55399C8.22336 7.49799 8.14869 7.46999 8.03669 7.46999C7.63536 7.50732 7.30403 7.84799 7.04269 8.49199C6.99603 8.60399 6.95403 8.72532 6.91669 8.85599C6.87003 9.00532 6.83269 9.08466 6.80469 9.09399C6.79536 9.10332 6.69736 9.10799 6.51069 9.10799H6.25869L6.21669 9.06599C6.17003 9.02866 6.17469 8.93066 6.23069 8.77199C6.40803 8.17466 6.67869 7.70332 7.04269 7.35799C7.30403 7.08732 7.59803 6.91466 7.92469 6.83999ZM15 4L15 16H16L16 4H15Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
yRelative: (
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M7.92463 6.83998C8.1113 6.79332 8.33063 6.79798 8.58263 6.85398C9.05863 6.98465 9.36197 7.26932 9.49263 7.70798C9.52997 7.86665 9.52997 8.06732 9.49263 8.30998C9.47397 8.37532 9.40397 8.56665 9.28263 8.88399C8.94663 9.78932 8.7273 10.4847 8.62463 10.97C8.52197 11.5953 8.57797 12.0293 8.79263 12.272C8.9513 12.4587 9.1753 12.5287 9.46463 12.482C9.88463 12.426 10.2533 12.174 10.5706 11.726L10.6546 11.6L11.1586 9.54198C11.5133 8.15132 11.7046 7.43265 11.7326 7.38598C11.882 7.11532 12.106 6.97532 12.4046 6.96598C12.554 6.96598 12.68 7.00798 12.7826 7.09198C12.8293 7.11065 12.8713 7.16198 12.9086 7.24598C12.9553 7.33932 12.9646 7.45598 12.9366 7.59598C12.9086 7.73598 12.6846 8.65532 12.2646 10.354C11.742 12.3887 11.49 13.3733 11.5086 13.308C11.4153 13.6067 11.2753 13.882 11.0886 14.134C10.566 14.9273 9.8753 15.4593 9.01663 15.73C8.60597 15.87 8.20463 15.912 7.81263 15.856C7.11263 15.7533 6.67397 15.436 6.49663 14.904C6.4593 14.6987 6.46397 14.5073 6.51063 14.33C6.56663 14.0967 6.6833 13.91 6.86063 13.77C7.19663 13.5273 7.51397 13.4807 7.81263 13.63C7.92463 13.6953 8.00863 13.784 8.06463 13.896C8.1673 14.12 8.14863 14.3627 8.00863 14.624C7.9153 14.8013 7.7893 14.932 7.63063 15.016L7.54663 15.072L7.61663 15.1C7.98063 15.2587 8.3633 15.2587 8.76463 15.1C9.26863 14.8947 9.68863 14.442 10.0246 13.742C10.09 13.5927 10.1506 13.4433 10.2066 13.294C10.3186 12.9673 10.3653 12.7993 10.3466 12.79C10.3373 12.79 10.3046 12.8087 10.2486 12.846C10.1086 12.93 9.9593 13 9.80063 13.056C9.2873 13.2333 8.75997 13.2007 8.21863 12.958C8.08797 12.902 7.9713 12.832 7.86863 12.748C7.44863 12.412 7.26663 11.8987 7.32263 11.208C7.35997 10.7507 7.5793 9.98065 7.98063 8.89798C7.98997 8.86065 7.9993 8.82798 8.00863 8.79998C8.1393 8.47332 8.2093 8.27732 8.21863 8.21198C8.33063 7.89465 8.34463 7.67532 8.26063 7.55398C8.2233 7.49798 8.14863 7.46998 8.03663 7.46998C7.6353 7.50732 7.30397 7.84798 7.04263 8.49198C6.99597 8.60398 6.95397 8.72532 6.91663 8.85598C6.86997 9.00532 6.83263 9.08465 6.80463 9.09398C6.7953 9.10332 6.6973 9.10798 6.51063 9.10798H6.25863L6.21663 9.06598C6.16997 9.02865 6.17463 8.93065 6.23063 8.77198C6.40797 8.17465 6.67863 7.70332 7.04263 7.35798C7.30397 7.08732 7.59797 6.91465 7.92463 6.83998Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
} as const
|
||||
|
||||
export type CustomIconName = keyof typeof CustomIconMap
|
||||
|
@ -14,9 +14,9 @@ export function LowerRightControls(props: React.PropsWithChildren) {
|
||||
'!text-chalkboard-70 hover:!text-chalkboard-80 dark:!text-chalkboard-40 dark:hover:!text-chalkboard-30'
|
||||
|
||||
return (
|
||||
<section className="fixed bottom-2 right-2 flex flex-col items-end gap-3">
|
||||
<section className="fixed bottom-2 right-2 flex flex-col items-end gap-3 pointer-events-none">
|
||||
{props.children}
|
||||
<menu className="flex items-center justify-end gap-3">
|
||||
<menu className="flex items-center justify-end gap-3 pointer-events-auto">
|
||||
<a
|
||||
href={`https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`}
|
||||
target="_blank"
|
||||
|
@ -18,6 +18,7 @@ import {
|
||||
engineCommandManager,
|
||||
codeManager,
|
||||
editorManager,
|
||||
sceneEntitiesManager,
|
||||
} from 'lib/singletons'
|
||||
import { applyConstraintHorzVertDistance } from './Toolbar/SetHorzVertDistance'
|
||||
import {
|
||||
@ -39,11 +40,24 @@ import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
|
||||
import useStateMachineCommands from 'hooks/useStateMachineCommands'
|
||||
import { modelingMachineConfig } from 'lib/commandBarConfigs/modelingCommandConfig'
|
||||
import {
|
||||
STRAIGHT_SEGMENT,
|
||||
TANGENTIAL_ARC_TO_SEGMENT,
|
||||
getParentGroup,
|
||||
getSketchOrientationDetails,
|
||||
getSketchQuaternion,
|
||||
} from 'clientSideScene/sceneEntities'
|
||||
import { sketchOnExtrudedFace, startSketchOnDefault } from 'lang/modifyAst'
|
||||
import { Program, VariableDeclaration, coreDump } from 'lang/wasm'
|
||||
import {
|
||||
moveValueIntoNewVariablePath,
|
||||
sketchOnExtrudedFace,
|
||||
startSketchOnDefault,
|
||||
} from 'lang/modifyAst'
|
||||
import {
|
||||
Program,
|
||||
VariableDeclaration,
|
||||
coreDump,
|
||||
parse,
|
||||
recast,
|
||||
} from 'lang/wasm'
|
||||
import {
|
||||
getNodeFromPath,
|
||||
getNodePathFromSourceRange,
|
||||
@ -57,6 +71,7 @@ import { EditorSelection } from '@uiw/react-codemirror'
|
||||
import { CoreDumpManager } from 'lib/coredump'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
|
||||
import { getVarNameModal } from 'hooks/useToolbarGuards'
|
||||
import useHotkeyWrapper from 'lib/hotkeyWrapper'
|
||||
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
@ -127,7 +142,82 @@ export const ModelingMachineProvider = ({
|
||||
},
|
||||
'Set mouse state': assign({
|
||||
mouseState: (_, event) => event.data,
|
||||
segmentHoverMap: ({ mouseState, segmentHoverMap }, event) => {
|
||||
if (event.data.type === 'isHovering') {
|
||||
const parent = getParentGroup(event.data.on, [
|
||||
STRAIGHT_SEGMENT,
|
||||
TANGENTIAL_ARC_TO_SEGMENT,
|
||||
])
|
||||
const pathToNode = parent?.userData?.pathToNode
|
||||
const pathToNodeString = JSON.stringify(pathToNode)
|
||||
if (!parent || !pathToNode) return {}
|
||||
if (segmentHoverMap[pathToNodeString] !== undefined)
|
||||
clearTimeout(segmentHoverMap[JSON.stringify(pathToNode)])
|
||||
return {
|
||||
...segmentHoverMap,
|
||||
[pathToNodeString]: 0,
|
||||
}
|
||||
} else if (
|
||||
event.data.type === 'idle' &&
|
||||
mouseState.type === 'isHovering'
|
||||
) {
|
||||
const mouseOnParent = getParentGroup(mouseState.on, [
|
||||
STRAIGHT_SEGMENT,
|
||||
TANGENTIAL_ARC_TO_SEGMENT,
|
||||
])
|
||||
if (!mouseOnParent || !mouseOnParent?.userData?.pathToNode)
|
||||
return segmentHoverMap
|
||||
const pathToNodeString = JSON.stringify(
|
||||
mouseOnParent?.userData?.pathToNode
|
||||
)
|
||||
const timeoutId = setTimeout(() => {
|
||||
sceneInfra.modelingSend({
|
||||
type: 'Set mouse state',
|
||||
data: {
|
||||
type: 'timeoutEnd',
|
||||
pathToNodeString,
|
||||
},
|
||||
})
|
||||
}, 800) as unknown as number
|
||||
return {
|
||||
...segmentHoverMap,
|
||||
[pathToNodeString]: timeoutId,
|
||||
}
|
||||
} else if (event.data.type === 'timeoutEnd') {
|
||||
const copy = { ...segmentHoverMap }
|
||||
delete copy[event.data.pathToNodeString]
|
||||
return copy
|
||||
}
|
||||
return {}
|
||||
},
|
||||
}),
|
||||
'Set Segment Overlays': assign({
|
||||
segmentOverlays: ({ segmentOverlays }, { data }) => {
|
||||
if (data.type === 'set-many') return data.overlays
|
||||
if (data.type === 'set-one')
|
||||
return {
|
||||
...segmentOverlays,
|
||||
[data.pathToNodeString]: data.seg,
|
||||
}
|
||||
if (data.type === 'delete-one') {
|
||||
const copy = { ...segmentOverlays }
|
||||
delete copy[data.pathToNodeString]
|
||||
return copy
|
||||
}
|
||||
// data.type === 'clear'
|
||||
return {}
|
||||
},
|
||||
}),
|
||||
'Set sketchDetails': assign(({ sketchDetails }, event) =>
|
||||
sketchDetails
|
||||
? {
|
||||
sketchDetails: {
|
||||
...sketchDetails,
|
||||
sketchPathToNode: event.data,
|
||||
},
|
||||
}
|
||||
: {}
|
||||
),
|
||||
'Set selection': assign(({ selectionRanges }, event) => {
|
||||
if (event.type !== 'Set selection') return {} // this was needed for ts after adding 'Set selection' action to on done modal events
|
||||
const setSelections = event.data
|
||||
@ -517,6 +607,27 @@ export const ModelingMachineProvider = ({
|
||||
),
|
||||
}
|
||||
},
|
||||
'Get convert to variable info': async ({ sketchDetails }, { data }) => {
|
||||
if (!sketchDetails) return []
|
||||
const { variableName } = await getVarNameModal({
|
||||
valueName: data.variableName || 'var',
|
||||
})
|
||||
const { modifiedAst: _modifiedAst, pathToReplacedNode } =
|
||||
moveValueIntoNewVariablePath(
|
||||
parse(recast(kclManager.ast)),
|
||||
kclManager.programMemory,
|
||||
data.pathToNode,
|
||||
variableName
|
||||
)
|
||||
await sceneEntitiesManager.updateAstAndRejigSketch(
|
||||
pathToReplacedNode || [],
|
||||
parse(recast(_modifiedAst)),
|
||||
sketchDetails.zAxis,
|
||||
sketchDetails.yAxis,
|
||||
sketchDetails.origin
|
||||
)
|
||||
return pathToReplacedNode || sketchDetails.sketchPathToNode
|
||||
},
|
||||
},
|
||||
devTools: true,
|
||||
}
|
||||
|
@ -48,7 +48,7 @@ export const KclEditorMenu = ({ children }: PropsWithChildren) => {
|
||||
{convertToVarEnabled && (
|
||||
<Menu.Item>
|
||||
<button
|
||||
onClick={handleConvertToVarClick}
|
||||
onClick={() => handleConvertToVarClick()}
|
||||
className={styles.button}
|
||||
>
|
||||
<span>Convert to Variable</span>
|
||||
|
@ -6,6 +6,7 @@ import { useResolvedTheme } from 'hooks/useResolvedTheme'
|
||||
import { ActionButton } from 'components/ActionButton'
|
||||
import toast from 'react-hot-toast'
|
||||
import Tooltip from 'components/Tooltip'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
|
||||
export const MemoryPaneMenu = () => {
|
||||
const { programMemory } = useKclContext()
|
||||
@ -44,6 +45,7 @@ export const MemoryPaneMenu = () => {
|
||||
export const MemoryPane = () => {
|
||||
const theme = useResolvedTheme()
|
||||
const { programMemory } = useKclContext()
|
||||
const { state } = useModelingContext()
|
||||
const ProcessedMemory = useMemo(
|
||||
() => processMemory(programMemory),
|
||||
[programMemory]
|
||||
@ -67,6 +69,12 @@ export const MemoryPane = () => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{state.matches('Sketch') && (
|
||||
<div
|
||||
className="absolute inset-0 dark:bg-chalkboard-90/80 bg-chalkboard-10/80 cursor-not-allowed"
|
||||
title="Variables won't update in sketch mode"
|
||||
></div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ import { Dialog, Transition } from '@headlessui/react'
|
||||
import { Fragment } from 'react'
|
||||
import { CreateNewVariable } from './AvailableVarsHelpers'
|
||||
import { ActionButton } from './ActionButton'
|
||||
import { faPlus } from '@fortawesome/free-solid-svg-icons'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import { type InstanceProps, create } from 'react-modal-promise'
|
||||
import { useCalculateKclExpression } from 'lib/useCalculateKclExpression'
|
||||
@ -74,17 +73,17 @@ export const SetVarNameModal = ({
|
||||
showCheckbox={false}
|
||||
/>
|
||||
<div className="mt-8 flex justify-between">
|
||||
<ActionButton Element="button" onClick={() => onReject(false)}>
|
||||
Cancel
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
type="submit"
|
||||
disabled={!isNewVariableNameUnique}
|
||||
iconStart={{ icon: faPlus }}
|
||||
iconStart={{ icon: 'plus' }}
|
||||
>
|
||||
Add variable
|
||||
</ActionButton>
|
||||
<ActionButton Element="button" onClick={() => onReject(false)}>
|
||||
Cancel
|
||||
</ActionButton>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog.Panel>
|
||||
|
@ -7,10 +7,13 @@ import { moveValueIntoNewVariable } from 'lang/modifyAst'
|
||||
import { isNodeSafeToReplace } from 'lang/queryAst'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useModelingContext } from './useModelingContext'
|
||||
import { PathToNode, SourceRange, parse, recast } from 'lang/wasm'
|
||||
import { useKclContext } from 'lang/KclProvider'
|
||||
|
||||
const getModalInfo = createSetVarNameModal(SetVarNameModal)
|
||||
export const getVarNameModal = createSetVarNameModal(SetVarNameModal)
|
||||
|
||||
export function useConvertToVariable() {
|
||||
export function useConvertToVariable(range?: SourceRange) {
|
||||
const { ast } = useKclContext()
|
||||
const { context } = useModelingContext()
|
||||
const [enable, setEnabled] = useState(false)
|
||||
|
||||
@ -20,31 +23,34 @@ export function useConvertToVariable() {
|
||||
|
||||
useEffect(() => {
|
||||
const { isSafe, value } = isNodeSafeToReplace(
|
||||
kclManager.ast,
|
||||
context.selectionRanges.codeBasedSelections?.[0]?.range || []
|
||||
parse(recast(ast)),
|
||||
range || context.selectionRanges.codeBasedSelections?.[0]?.range || []
|
||||
)
|
||||
const canReplace = isSafe && value.type !== 'Identifier'
|
||||
const isOnlyOneSelection =
|
||||
context.selectionRanges.codeBasedSelections.length === 1
|
||||
!!range || context.selectionRanges.codeBasedSelections.length === 1
|
||||
|
||||
const _enableHorz = canReplace && isOnlyOneSelection
|
||||
setEnabled(_enableHorz)
|
||||
setEnabled(canReplace && isOnlyOneSelection)
|
||||
}, [context.selectionRanges])
|
||||
|
||||
const handleClick = async () => {
|
||||
const handleClick = async (
|
||||
valueName?: string
|
||||
): Promise<PathToNode | undefined> => {
|
||||
try {
|
||||
const { variableName } = await getModalInfo({
|
||||
valueName: 'var',
|
||||
const { variableName } = await getVarNameModal({
|
||||
valueName: valueName || 'var',
|
||||
})
|
||||
|
||||
const { modifiedAst: _modifiedAst } = moveValueIntoNewVariable(
|
||||
kclManager.ast,
|
||||
const { modifiedAst: _modifiedAst, pathToReplacedNode } =
|
||||
moveValueIntoNewVariable(
|
||||
ast,
|
||||
kclManager.programMemory,
|
||||
context.selectionRanges.codeBasedSelections[0].range,
|
||||
range || context.selectionRanges.codeBasedSelections[0].range,
|
||||
variableName
|
||||
)
|
||||
|
||||
kclManager.updateAst(_modifiedAst, true)
|
||||
await kclManager.updateAst(_modifiedAst, true)
|
||||
return pathToReplacedNode
|
||||
} catch (e) {
|
||||
console.log('error', e)
|
||||
}
|
||||
|
@ -40,7 +40,10 @@ root.render(
|
||||
primary: 'oklch(89% 0.16 143.4deg)',
|
||||
secondary: 'oklch(48.62% 0.1654 142.5deg)',
|
||||
},
|
||||
duration: 1500,
|
||||
duration:
|
||||
window?.localStorage.getItem('playwright') === 'true'
|
||||
? 10 // speed up e2e tests
|
||||
: 1500,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
@ -13,9 +13,11 @@ import {
|
||||
giveSketchFnCallTag,
|
||||
moveValueIntoNewVariable,
|
||||
sketchOnExtrudedFace,
|
||||
deleteSegmentFromPipeExpression,
|
||||
removeSingleConstraintInfo,
|
||||
} from './modifyAst'
|
||||
import { enginelessExecutor } from '../lib/testHelpers'
|
||||
import { getNodePathFromSourceRange } from './queryAst'
|
||||
import { findUsesOfTagInPipe, getNodePathFromSourceRange } from './queryAst'
|
||||
|
||||
beforeAll(async () => {
|
||||
await initPromise
|
||||
@ -364,3 +366,231 @@ const part002 = startSketchOn(part001, 'seg01')`)
|
||||
const part002 = startSketchOn(part001, 'END')`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Testing deleteSegmentFromPipeExpression', () => {
|
||||
it('Should delete a segment withOUT any dependent segments', async () => {
|
||||
const code = `const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt([54.78, -95.91], %)
|
||||
|> line([306.21, 198.82], %)
|
||||
|> line([306.21, 198.85], %, 'a')
|
||||
|> line([306.21, 198.87], %)`
|
||||
const ast = parse(code)
|
||||
const programMemory = await enginelessExecutor(ast)
|
||||
const lineOfInterest = "line([306.21, 198.85], %, 'a')"
|
||||
const range: [number, number] = [
|
||||
code.indexOf(lineOfInterest),
|
||||
code.indexOf(lineOfInterest) + lineOfInterest.length,
|
||||
]
|
||||
const pathToNode = getNodePathFromSourceRange(ast, range)
|
||||
const modifiedAst = deleteSegmentFromPipeExpression(
|
||||
[],
|
||||
ast,
|
||||
programMemory,
|
||||
code,
|
||||
pathToNode
|
||||
)
|
||||
const newCode = recast(modifiedAst)
|
||||
expect(newCode).toBe(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt([54.78, -95.91], %)
|
||||
|> line([306.21, 198.82], %)
|
||||
|> line([306.21, 198.87], %)
|
||||
`)
|
||||
})
|
||||
describe('Should delete a segment WITH any dependent segments, unconstraining the dependent parts', () => {
|
||||
const makeCode = (
|
||||
line: string,
|
||||
replace1 = '',
|
||||
replace2 = ''
|
||||
) => `const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt([54.78, -95.91], %)
|
||||
|> line([306.21, 198.82], %, 'b')
|
||||
${!replace1 ? ` |> ${line}\n` : ''} |> angledLine([-65, ${
|
||||
!replace1 ? "segLen('a', %)" : replace1
|
||||
}], %)
|
||||
|> line([306.21, 198.87], %)
|
||||
|> angledLine([65, ${!replace2 ? "segAng('a', %)" : replace2}], %)
|
||||
|> line([-963.39, -154.67], %)
|
||||
`
|
||||
test.each([
|
||||
['line', "line([306.21, 198.85], %, 'a')", ['365.11', '33']],
|
||||
['lineTo', "lineTo([306.21, 198.85], %, 'a')", ['110.48', '119.73']],
|
||||
['yLine', "yLine(198.85, %, 'a')", ['198.85', '90']],
|
||||
['xLine', "xLine(198.85, %, 'a')", ['198.85', '0']],
|
||||
['yLineTo', "yLineTo(198.85, %, 'a')", ['95.94', '90']],
|
||||
['xLineTo', "xLineTo(198.85, %, 'a')", ['162.14', '180']],
|
||||
[
|
||||
'angledLine',
|
||||
"angledLine({ angle: 45.5, length: 198.85 }, %, 'a')",
|
||||
['198.85', '45.5'],
|
||||
],
|
||||
[
|
||||
'angledLineOfXLength',
|
||||
"angledLineOfXLength({ angle: 45.5, length: 198.85 }, %, 'a')",
|
||||
['283.7', '45.5'],
|
||||
],
|
||||
[
|
||||
'angledLineOfYLength',
|
||||
"angledLineOfYLength({ angle: 45.5, length: 198.85 }, %, 'a')",
|
||||
['278.79', '45.5'],
|
||||
],
|
||||
[
|
||||
'angledLineToX',
|
||||
"angledLineToX({ angle: 45.5, to: 198.85 }, %, 'a')",
|
||||
['231.33', '134.5'],
|
||||
],
|
||||
[
|
||||
'angledLineToY',
|
||||
"angledLineToY({ angle: 45.5, to: 198.85 }, %, 'a')",
|
||||
['134.51', '45.5'],
|
||||
],
|
||||
[
|
||||
'angledLineThatIntersects',
|
||||
`angledLineThatIntersects({ angle: 45.5, intersectTag: 'b', offset: 198.85 }, %, 'a')`,
|
||||
['918.4', '45.5'],
|
||||
],
|
||||
])(`%s`, async (_, line, [replace1, replace2]) => {
|
||||
const code = makeCode(line)
|
||||
const ast = parse(code)
|
||||
const programMemory = await enginelessExecutor(ast)
|
||||
const lineOfInterest = line
|
||||
const range: [number, number] = [
|
||||
code.indexOf(lineOfInterest),
|
||||
code.indexOf(lineOfInterest) + lineOfInterest.length,
|
||||
]
|
||||
const pathToNode = getNodePathFromSourceRange(ast, range)
|
||||
const dependentSegments = findUsesOfTagInPipe(ast, pathToNode)
|
||||
const modifiedAst = deleteSegmentFromPipeExpression(
|
||||
dependentSegments,
|
||||
ast,
|
||||
programMemory,
|
||||
code,
|
||||
pathToNode
|
||||
)
|
||||
const newCode = recast(modifiedAst)
|
||||
expect(newCode).toBe(makeCode(line, replace1, replace2))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Testing removeSingleConstraintInfo', () => {
|
||||
describe('with mostly object notation', () => {
|
||||
const code = `const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> line([3 + 0, 4 + 0], %)
|
||||
|> angledLine({ angle: 3 + 0, length: 3.14 + 0 }, %)
|
||||
|> lineTo([6.14 + 0, 3.14 + 0], %)
|
||||
|> xLineTo(8 + 0, %)
|
||||
|> yLineTo(5 + 0, %)
|
||||
|> yLine(3.14 + 0, %, 'a')
|
||||
|> xLine(3.14 + 0, %)
|
||||
|> angledLineOfXLength({ angle: 3 + 0, length: 3.14 + 0 }, %)
|
||||
|> angledLineOfYLength({ angle: 30 + 0, length: 3 + 0 }, %)
|
||||
|> angledLineToX({ angle: 12.14 + 0, to: 12 + 0 }, %)
|
||||
|> angledLineToY({ angle: 30 + 0, to: 10.14 + 0 }, %)
|
||||
|> angledLineThatIntersects({
|
||||
angle: 3.14 + 0,
|
||||
intersectTag: 'a',
|
||||
offset: 0 + 0
|
||||
}, %)
|
||||
|> tangentialArcTo([3.14 + 0, 13.14 + 0], %)`
|
||||
test.each([
|
||||
[' line([3 + 0, 4], %)', 'arrayIndex', 1],
|
||||
[
|
||||
'angledLine({ angle: 3, length: 3.14 + 0 }, %)',
|
||||
'objectProperty',
|
||||
'angle',
|
||||
],
|
||||
['lineTo([6.14, 3.14 + 0], %)', 'arrayIndex', 0],
|
||||
['xLineTo(8, %)', '', ''],
|
||||
['yLineTo(5, %)', '', ''],
|
||||
["yLine(3.14, %, 'a')", '', ''],
|
||||
['xLine(3.14, %)', '', ''],
|
||||
[
|
||||
'angledLineOfXLength({ angle: 3, length: 3.14 + 0 }, %)',
|
||||
'objectProperty',
|
||||
'angle',
|
||||
],
|
||||
[
|
||||
'angledLineOfYLength({ angle: 30 + 0, length: 3 }, %)',
|
||||
'objectProperty',
|
||||
'length',
|
||||
],
|
||||
[
|
||||
'angledLineToX({ angle: 12.14 + 0, to: 12 }, %)',
|
||||
'objectProperty',
|
||||
'to',
|
||||
],
|
||||
[
|
||||
'angledLineToY({ angle: 30, to: 10.14 + 0 }, %)',
|
||||
'objectProperty',
|
||||
'angle',
|
||||
],
|
||||
[
|
||||
`angledLineThatIntersects({
|
||||
angle: 3.14 + 0,
|
||||
offset: 0,
|
||||
intersectTag: 'a'
|
||||
}, %)`,
|
||||
'objectProperty',
|
||||
'offset',
|
||||
],
|
||||
['tangentialArcTo([3.14 + 0, 13.14], %)', 'arrayIndex', 1],
|
||||
])('stdlib fn: %s', async (expectedFinish, key, value) => {
|
||||
const ast = parse(code)
|
||||
const programMemory = await enginelessExecutor(ast)
|
||||
const lineOfInterest = expectedFinish.split('(')[0] + '('
|
||||
const range: [number, number] = [
|
||||
code.indexOf(lineOfInterest) + 1,
|
||||
code.indexOf(lineOfInterest) + lineOfInterest.length,
|
||||
]
|
||||
const pathToNode = getNodePathFromSourceRange(ast, range)
|
||||
const mod = removeSingleConstraintInfo(
|
||||
{
|
||||
pathToCallExp: pathToNode,
|
||||
[key]: value,
|
||||
},
|
||||
ast,
|
||||
programMemory
|
||||
)
|
||||
if (!mod) throw new Error('yo is undefined')
|
||||
const recastCode = recast(mod.modifiedAst)
|
||||
expect(recastCode).toContain(expectedFinish)
|
||||
})
|
||||
})
|
||||
describe('with array notation', () => {
|
||||
const code = `const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> angledLine([3.14 + 0, 3.14 + 0], %)
|
||||
|> angledLineOfXLength([3 + 0, 3.14 + 0], %)
|
||||
|> angledLineOfYLength([30 + 0, 3 + 0], %)
|
||||
|> angledLineToX([12.14 + 0, 12 + 0], %)
|
||||
|> angledLineToY([30 + 0, 10.14 + 0], %)`
|
||||
test.each([
|
||||
['angledLine([3, 3.14 + 0], %)', 'arrayIndex', 0],
|
||||
['angledLineOfXLength([3, 3.14 + 0], %)', 'arrayIndex', 0],
|
||||
['angledLineOfYLength([30 + 0, 3], %)', 'arrayIndex', 1],
|
||||
['angledLineToX([12.14 + 0, 12], %)', 'arrayIndex', 1],
|
||||
['angledLineToY([30, 10.14 + 0], %)', 'arrayIndex', 0],
|
||||
])('stdlib fn: %s', async (expectedFinish, key, value) => {
|
||||
const ast = parse(code)
|
||||
const programMemory = await enginelessExecutor(ast)
|
||||
const lineOfInterest = expectedFinish.split('(')[0] + '('
|
||||
const range: [number, number] = [
|
||||
code.indexOf(lineOfInterest) + 1,
|
||||
code.indexOf(lineOfInterest) + lineOfInterest.length,
|
||||
]
|
||||
const pathToNode = getNodePathFromSourceRange(ast, range)
|
||||
const mod = removeSingleConstraintInfo(
|
||||
{
|
||||
pathToCallExp: pathToNode,
|
||||
[key]: value,
|
||||
},
|
||||
ast,
|
||||
programMemory
|
||||
)
|
||||
if (!mod) throw new Error('yo is undefined')
|
||||
const recastCode = recast(mod.modifiedAst)
|
||||
expect(recastCode).toContain(expectedFinish)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -15,17 +15,26 @@ import {
|
||||
BinaryExpression,
|
||||
PathToNode,
|
||||
ProgramMemory,
|
||||
SourceRange,
|
||||
} from './wasm'
|
||||
import {
|
||||
isNodeSafeToReplacePath,
|
||||
findAllPreviousVariables,
|
||||
findAllPreviousVariablesPath,
|
||||
getNodeFromPath,
|
||||
getNodePathFromSourceRange,
|
||||
isNodeSafeToReplace,
|
||||
} from './queryAst'
|
||||
import { addTagForSketchOnFace } from './std/sketch'
|
||||
import { isLiteralArrayOrStatic } from './std/sketchcombos'
|
||||
import { addTagForSketchOnFace, getConstraintInfo } from './std/sketch'
|
||||
import {
|
||||
PathToNodeMap,
|
||||
isLiteralArrayOrStatic,
|
||||
removeSingleConstraint,
|
||||
transformAstSketchLines,
|
||||
} from './std/sketchcombos'
|
||||
import { DefaultPlaneStr } from 'clientSideScene/sceneEntities'
|
||||
import { roundOff } from 'lib/utils'
|
||||
import { isOverlap, roundOff } from 'lib/utils'
|
||||
import { ConstrainInfo } from './std/stdTypes'
|
||||
|
||||
export function startSketchOnDefault(
|
||||
node: Program,
|
||||
@ -241,11 +250,7 @@ export function extrudeSketch(
|
||||
pathToExtrudeArg: PathToNode
|
||||
} {
|
||||
const _node = { ...node }
|
||||
const { node: sketchExpression } = getNodeFromPath(
|
||||
_node,
|
||||
pathToNode,
|
||||
'SketchExpression' // TODO fix this #25
|
||||
)
|
||||
const { node: sketchExpression } = getNodeFromPath(_node, pathToNode)
|
||||
|
||||
// determine if sketchExpression is in a pipeExpression or not
|
||||
const { node: pipeExpression } = getNodeFromPath<PipeExpression>(
|
||||
@ -619,6 +624,34 @@ export function giveSketchFnCallTag(
|
||||
}
|
||||
}
|
||||
|
||||
export function moveValueIntoNewVariablePath(
|
||||
ast: Program,
|
||||
programMemory: ProgramMemory,
|
||||
pathToNode: PathToNode,
|
||||
variableName: string
|
||||
): {
|
||||
modifiedAst: Program
|
||||
pathToReplacedNode?: PathToNode
|
||||
} {
|
||||
const { isSafe, value, replacer } = isNodeSafeToReplacePath(ast, pathToNode)
|
||||
if (!isSafe || value.type === 'Identifier') return { modifiedAst: ast }
|
||||
|
||||
const { insertIndex } = findAllPreviousVariablesPath(
|
||||
ast,
|
||||
programMemory,
|
||||
pathToNode
|
||||
)
|
||||
let _node = JSON.parse(JSON.stringify(ast))
|
||||
const boop = replacer(_node, variableName)
|
||||
_node = boop.modifiedAst
|
||||
_node.body.splice(
|
||||
insertIndex,
|
||||
0,
|
||||
createVariableDeclaration(variableName, value)
|
||||
)
|
||||
return { modifiedAst: _node, pathToReplacedNode: boop.pathToReplaced }
|
||||
}
|
||||
|
||||
export function moveValueIntoNewVariable(
|
||||
ast: Program,
|
||||
programMemory: ProgramMemory,
|
||||
@ -626,6 +659,7 @@ export function moveValueIntoNewVariable(
|
||||
variableName: string
|
||||
): {
|
||||
modifiedAst: Program
|
||||
pathToReplacedNode?: PathToNode
|
||||
} {
|
||||
const { isSafe, value, replacer } = isNodeSafeToReplace(ast, sourceRange)
|
||||
if (!isSafe || value.type === 'Identifier') return { modifiedAst: ast }
|
||||
@ -636,11 +670,124 @@ export function moveValueIntoNewVariable(
|
||||
sourceRange
|
||||
)
|
||||
let _node = JSON.parse(JSON.stringify(ast))
|
||||
_node = replacer(_node, variableName).modifiedAst
|
||||
const { modifiedAst, pathToReplaced } = replacer(_node, variableName)
|
||||
_node = modifiedAst
|
||||
_node.body.splice(
|
||||
insertIndex,
|
||||
0,
|
||||
createVariableDeclaration(variableName, value)
|
||||
)
|
||||
return { modifiedAst: _node }
|
||||
return { modifiedAst: _node, pathToReplacedNode: pathToReplaced }
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a segment from a pipe expression, if the segment has a tag that other segments use, it will remove that value and replace it with the equivalent literal
|
||||
* @param dependentRanges - The ranges of the segments that are dependent on the segment being deleted, this is usually the output of `findUsesOfTagInPipe`
|
||||
*/
|
||||
export function deleteSegmentFromPipeExpression(
|
||||
dependentRanges: SourceRange[],
|
||||
modifiedAst: Program,
|
||||
programMemory: ProgramMemory,
|
||||
code: string,
|
||||
pathToNode: PathToNode
|
||||
): Program {
|
||||
let _modifiedAst: Program = JSON.parse(JSON.stringify(modifiedAst))
|
||||
|
||||
dependentRanges.forEach((range) => {
|
||||
const path = getNodePathFromSourceRange(_modifiedAst, range)
|
||||
|
||||
const callExp = getNodeFromPath<CallExpression>(
|
||||
_modifiedAst,
|
||||
path,
|
||||
'CallExpression',
|
||||
true
|
||||
)
|
||||
const constraintInfo = getConstraintInfo(callExp.node, code, path).find(
|
||||
({ sourceRange }) => isOverlap(sourceRange, range)
|
||||
)
|
||||
if (!constraintInfo) return
|
||||
const input = makeRemoveSingleConstraintInput(
|
||||
constraintInfo.argPosition,
|
||||
callExp.shallowPath
|
||||
)
|
||||
if (!input) return
|
||||
const transform = removeSingleConstraintInfo(
|
||||
{
|
||||
...input,
|
||||
},
|
||||
_modifiedAst,
|
||||
programMemory
|
||||
)
|
||||
if (!transform) return
|
||||
_modifiedAst = transform.modifiedAst
|
||||
})
|
||||
|
||||
const pipeExpression = getNodeFromPath<PipeExpression>(
|
||||
_modifiedAst,
|
||||
pathToNode,
|
||||
'PipeExpression'
|
||||
).node
|
||||
|
||||
const pipeInPathIndex = pathToNode.findIndex(
|
||||
([_, desc]) => desc === 'PipeExpression'
|
||||
)
|
||||
const segmentIndexInPipe = pathToNode[pipeInPathIndex + 1][0] as number
|
||||
pipeExpression.body.splice(segmentIndexInPipe, 1)
|
||||
|
||||
return _modifiedAst
|
||||
}
|
||||
|
||||
export function makeRemoveSingleConstraintInput(
|
||||
argPosition: ConstrainInfo['argPosition'],
|
||||
pathToNode: PathToNode
|
||||
): Parameters<typeof removeSingleConstraintInfo>[0] | false {
|
||||
return argPosition?.type === 'singleValue'
|
||||
? {
|
||||
pathToCallExp: pathToNode,
|
||||
}
|
||||
: argPosition?.type === 'arrayItem'
|
||||
? {
|
||||
pathToCallExp: pathToNode,
|
||||
arrayIndex: argPosition.index,
|
||||
}
|
||||
: argPosition?.type === 'objectProperty'
|
||||
? {
|
||||
pathToCallExp: pathToNode,
|
||||
objectProperty: argPosition.key,
|
||||
}
|
||||
: false
|
||||
}
|
||||
|
||||
export function removeSingleConstraintInfo(
|
||||
{
|
||||
pathToCallExp,
|
||||
arrayIndex,
|
||||
objectProperty,
|
||||
}: {
|
||||
pathToCallExp: PathToNode
|
||||
arrayIndex?: number
|
||||
objectProperty?: string
|
||||
},
|
||||
ast: Program,
|
||||
programMemory: ProgramMemory
|
||||
):
|
||||
| {
|
||||
modifiedAst: Program
|
||||
pathToNodeMap: PathToNodeMap
|
||||
}
|
||||
| false {
|
||||
const transform = removeSingleConstraint({
|
||||
pathToCallExp,
|
||||
arrayIndex,
|
||||
objectProperty,
|
||||
ast,
|
||||
})
|
||||
if (!transform) return false
|
||||
return transformAstSketchLines({
|
||||
ast,
|
||||
selectionRanges: [pathToCallExp],
|
||||
transformInfos: [transform],
|
||||
programMemory,
|
||||
referenceSegName: '',
|
||||
})
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
getNodePathFromSourceRange,
|
||||
doesPipeHaveCallExp,
|
||||
hasExtrudeSketchGroup,
|
||||
findUsesOfTagInPipe,
|
||||
} from './queryAst'
|
||||
import { enginelessExecutor } from '../lib/testHelpers'
|
||||
import {
|
||||
@ -358,3 +359,40 @@ const part001 = startSketchAt([-1.41, 3.46])
|
||||
expect(result).toEqual(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Testing findUsesOfTagInPipe', () => {
|
||||
const exampleCode = `const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt([68.12, 156.65], %)
|
||||
|> line([306.21, 198.82], %)
|
||||
|> line([306.21, 198.85], %, 'seg01')
|
||||
|> angledLine([-65, segLen('seg01', %)], %)
|
||||
|> line([306.21, 198.87], %)
|
||||
|> angledLine([65, segLen('seg01', %)], %)`
|
||||
it('finds the current segment', async () => {
|
||||
const ast = parse(exampleCode)
|
||||
const lineOfInterest = `198.85], %, 'seg01'`
|
||||
const characterIndex =
|
||||
exampleCode.indexOf(lineOfInterest) + lineOfInterest.length
|
||||
const pathToNode = getNodePathFromSourceRange(ast, [
|
||||
characterIndex,
|
||||
characterIndex,
|
||||
])
|
||||
const result = findUsesOfTagInPipe(ast, pathToNode)
|
||||
expect(result).toHaveLength(2)
|
||||
result.forEach((range) => {
|
||||
expect(exampleCode.slice(range[0], range[1])).toContain('segLen')
|
||||
})
|
||||
})
|
||||
it('find no tag if line has no tag', () => {
|
||||
const ast = parse(exampleCode)
|
||||
const lineOfInterest = `line([306.21, 198.82], %)`
|
||||
const characterIndex =
|
||||
exampleCode.indexOf(lineOfInterest) + lineOfInterest.length
|
||||
const pathToNode = getNodePathFromSourceRange(ast, [
|
||||
characterIndex,
|
||||
characterIndex,
|
||||
])
|
||||
const result = findUsesOfTagInPipe(ast, pathToNode)
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
@ -26,10 +26,17 @@ import {
|
||||
getConstraintType,
|
||||
} from './std/sketchcombos'
|
||||
|
||||
/**
|
||||
* Retrieves a node from a given path within a Program node structure, optionally stopping at a specified node type.
|
||||
* This function navigates through the AST (Abstract Syntax Tree) based on the provided path, attempting to locate
|
||||
* and return the node at the end of this path.
|
||||
* By default it will return the node of the deepest "stopAt" type encountered, or the node at the end of the path if no "stopAt" type is provided.
|
||||
* If the "returnEarly" flag is set to true, the function will return as soon as a node of the specified type is found.
|
||||
*/
|
||||
export function getNodeFromPath<T>(
|
||||
node: Program,
|
||||
path: PathToNode,
|
||||
stopAt: string | string[] = '',
|
||||
stopAt?: SyntaxType | SyntaxType[],
|
||||
returnEarly = false
|
||||
): {
|
||||
node: T
|
||||
@ -50,9 +57,10 @@ export function getNodeFromPath<T>(
|
||||
pathsExplored.push(pathItem)
|
||||
}
|
||||
if (
|
||||
Array.isArray(stopAt)
|
||||
typeof stopAt !== 'undefined' &&
|
||||
(Array.isArray(stopAt)
|
||||
? stopAt.includes(currentNode.type)
|
||||
: currentNode.type === stopAt
|
||||
: currentNode.type === stopAt)
|
||||
) {
|
||||
// it will match the deepest node of the type
|
||||
// instead of returning at the first match
|
||||
@ -82,17 +90,20 @@ export function getNodeFromPath<T>(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Functions the same as getNodeFromPath, but returns a curried function that can be called with the stopAt and returnEarly arguments.
|
||||
*/
|
||||
export function getNodeFromPathCurry(
|
||||
node: Program,
|
||||
path: PathToNode
|
||||
): <T>(
|
||||
stopAt: string,
|
||||
stopAt?: SyntaxType | SyntaxType[],
|
||||
returnEarly?: boolean
|
||||
) => {
|
||||
node: T
|
||||
path: PathToNode
|
||||
} {
|
||||
return <T>(stopAt: string = '', returnEarly = false) => {
|
||||
return <T>(stopAt?: SyntaxType | SyntaxType[], returnEarly = false) => {
|
||||
const { node: _node, shallowPath } = getNodeFromPath<T>(
|
||||
node,
|
||||
path,
|
||||
@ -353,29 +364,31 @@ export interface PrevVariable<T> {
|
||||
value: T
|
||||
}
|
||||
|
||||
export function findAllPreviousVariables(
|
||||
export function findAllPreviousVariablesPath(
|
||||
ast: Program,
|
||||
programMemory: ProgramMemory,
|
||||
sourceRange: Selection['range'],
|
||||
path: PathToNode,
|
||||
type: 'number' | 'string' = 'number'
|
||||
): {
|
||||
variables: PrevVariable<typeof type extends 'number' ? number : string>[]
|
||||
bodyPath: PathToNode
|
||||
insertIndex: number
|
||||
} {
|
||||
const path = getNodePathFromSourceRange(ast, sourceRange)
|
||||
const { shallowPath: pathToDec } = getNodeFromPath(
|
||||
const { shallowPath: pathToDec, node } = getNodeFromPath(
|
||||
ast,
|
||||
path,
|
||||
'VariableDeclaration'
|
||||
)
|
||||
|
||||
const startRange = (node as any).start
|
||||
|
||||
const { index: insertIndex, path: bodyPath } = splitPathAtLastIndex(pathToDec)
|
||||
|
||||
const { node: bodyItems } = getNodeFromPath<Program['body']>(ast, bodyPath)
|
||||
|
||||
const variables: PrevVariable<any>[] = []
|
||||
bodyItems?.forEach?.((item) => {
|
||||
if (item.type !== 'VariableDeclaration' || item.end > sourceRange[0]) return
|
||||
if (item.type !== 'VariableDeclaration' || item.end > startRange) return
|
||||
const varName = item.declarations[0].id.name
|
||||
const varValue = programMemory?.root[varName]
|
||||
if (typeof varValue?.value !== type) return
|
||||
@ -392,25 +405,42 @@ export function findAllPreviousVariables(
|
||||
}
|
||||
}
|
||||
|
||||
type ReplacerFn = (_ast: Program, varName: string) => { modifiedAst: Program }
|
||||
|
||||
export function isNodeSafeToReplace(
|
||||
export function findAllPreviousVariables(
|
||||
ast: Program,
|
||||
sourceRange: [number, number]
|
||||
programMemory: ProgramMemory,
|
||||
sourceRange: Selection['range'],
|
||||
type: 'number' | 'string' = 'number'
|
||||
): {
|
||||
variables: PrevVariable<typeof type extends 'number' ? number : string>[]
|
||||
bodyPath: PathToNode
|
||||
insertIndex: number
|
||||
} {
|
||||
const path = getNodePathFromSourceRange(ast, sourceRange)
|
||||
return findAllPreviousVariablesPath(ast, programMemory, path, type)
|
||||
}
|
||||
|
||||
type ReplacerFn = (
|
||||
_ast: Program,
|
||||
varName: string
|
||||
) => { modifiedAst: Program; pathToReplaced: PathToNode }
|
||||
|
||||
export function isNodeSafeToReplacePath(
|
||||
ast: Program,
|
||||
path: PathToNode
|
||||
): {
|
||||
isSafe: boolean
|
||||
value: Value
|
||||
replacer: ReplacerFn
|
||||
} {
|
||||
let path = getNodePathFromSourceRange(ast, sourceRange)
|
||||
if (path[path.length - 1][0] === 'callee') {
|
||||
path = path.slice(0, -1)
|
||||
}
|
||||
const acceptedNodeTypes = [
|
||||
const acceptedNodeTypes: SyntaxType[] = [
|
||||
'BinaryExpression',
|
||||
'Identifier',
|
||||
'CallExpression',
|
||||
'Literal',
|
||||
'UnaryExpression',
|
||||
]
|
||||
const { node: value, deepPath: outPath } = getNodeFromPath(
|
||||
ast,
|
||||
@ -431,10 +461,12 @@ export function isNodeSafeToReplace(
|
||||
const replaceNodeWithIdentifier: ReplacerFn = (_ast, varName) => {
|
||||
const identifier = createIdentifier(varName)
|
||||
const last = finPath[finPath.length - 1]
|
||||
const pathToReplaced = JSON.parse(JSON.stringify(finPath))
|
||||
pathToReplaced[1][0] = pathToReplaced[1][0] + 1
|
||||
const startPath = finPath.slice(0, -1)
|
||||
const nodeToReplace = getNodeFromPath(_ast, startPath).node as any
|
||||
nodeToReplace[last[0]] = identifier
|
||||
return { modifiedAst: _ast }
|
||||
return { modifiedAst: _ast, pathToReplaced }
|
||||
}
|
||||
|
||||
const hasPipeSub = isTypeInValue(finVal as Value, 'PipeSubstitution')
|
||||
@ -450,6 +482,18 @@ export function isNodeSafeToReplace(
|
||||
}
|
||||
}
|
||||
|
||||
export function isNodeSafeToReplace(
|
||||
ast: Program,
|
||||
sourceRange: [number, number]
|
||||
): {
|
||||
isSafe: boolean
|
||||
value: Value
|
||||
replacer: ReplacerFn
|
||||
} {
|
||||
let path = getNodePathFromSourceRange(ast, sourceRange)
|
||||
return isNodeSafeToReplacePath(ast, path)
|
||||
}
|
||||
|
||||
export function isTypeInValue(node: Value, syntaxType: SyntaxType): boolean {
|
||||
if (node.type === syntaxType) return true
|
||||
if (node.type === 'BinaryExpression') return isTypeInBinExp(node, syntaxType)
|
||||
@ -632,3 +676,47 @@ export function isSingleCursorInPipe(
|
||||
if (nodeTypes.includes('PipeExpression')) return true
|
||||
return false
|
||||
}
|
||||
|
||||
export function findUsesOfTagInPipe(
|
||||
ast: Program,
|
||||
pathToNode: PathToNode
|
||||
): SourceRange[] {
|
||||
const stdlibFunctionsThatTakeTagInputs = [
|
||||
'segAng',
|
||||
'segEndX',
|
||||
'segEndY',
|
||||
'segLen',
|
||||
]
|
||||
const node = getNodeFromPath<CallExpression>(
|
||||
ast,
|
||||
pathToNode,
|
||||
'CallExpression'
|
||||
).node
|
||||
if (node.type !== 'CallExpression') return []
|
||||
const tagIndex = node.callee.name === 'close' ? 1 : 2
|
||||
const thirdParam = node.arguments[tagIndex]
|
||||
if (thirdParam?.type !== 'Literal') return []
|
||||
const tag = String(thirdParam.value)
|
||||
|
||||
const varDec = getNodeFromPath<VariableDeclaration>(
|
||||
ast,
|
||||
pathToNode,
|
||||
'VariableDeclaration'
|
||||
).node
|
||||
const dependentRanges: SourceRange[] = []
|
||||
|
||||
traverse(varDec, {
|
||||
enter: (node) => {
|
||||
if (
|
||||
node.type !== 'CallExpression' ||
|
||||
!stdlibFunctionsThatTakeTagInputs.includes(node.callee.name)
|
||||
)
|
||||
return
|
||||
const tagArg = node.arguments[0]
|
||||
if (tagArg.type !== 'Literal') return
|
||||
if (String(tagArg.value) === tag)
|
||||
dependentRanges.push([node.start, node.end])
|
||||
},
|
||||
})
|
||||
return dependentRanges
|
||||
}
|
||||
|
@ -5,9 +5,16 @@ import {
|
||||
getYComponent,
|
||||
getXComponent,
|
||||
addCloseToPipe,
|
||||
getConstraintInfo,
|
||||
} from './sketch'
|
||||
import { parse, recast, initPromise } from '../wasm'
|
||||
import { getNodePathFromSourceRange } from '../queryAst'
|
||||
import {
|
||||
parse,
|
||||
recast,
|
||||
initPromise,
|
||||
SourceRange,
|
||||
CallExpression,
|
||||
} from '../wasm'
|
||||
import { getNodeFromPath, getNodePathFromSourceRange } from '../queryAst'
|
||||
import { enginelessExecutor } from '../../lib/testHelpers'
|
||||
|
||||
const eachQuad: [number, [number, number]][] = [
|
||||
@ -212,3 +219,884 @@ describe('testing addTagForSketchOnFace', () => {
|
||||
expect(recast(modifiedAst)).toBe(expectedCode)
|
||||
})
|
||||
})
|
||||
|
||||
describe('testing getConstraintInfo', () => {
|
||||
describe('object notation', () => {
|
||||
const code = `const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt([0,0], %)
|
||||
|> line([3, 4], %)
|
||||
|> angledLine({
|
||||
angle: 3.14,
|
||||
length: 3.14,
|
||||
}, %)
|
||||
|> lineTo([6.14, 3.14], %)
|
||||
|> xLineTo(8, %)
|
||||
|> yLineTo(5, %)
|
||||
|> yLine(3.14, %, 'a')
|
||||
|> xLine(3.14, %)
|
||||
|> angledLineOfXLength({
|
||||
angle: 3.14,
|
||||
length: 3.14,
|
||||
}, %)
|
||||
|> angledLineOfYLength({
|
||||
angle: 30,
|
||||
length: 3,
|
||||
}, %)
|
||||
|> angledLineToX({
|
||||
angle: 12.14,
|
||||
to: 12,
|
||||
}, %)
|
||||
|> angledLineToY({
|
||||
angle: 30,
|
||||
to: 10.14,
|
||||
}, %)
|
||||
|> angledLineThatIntersects({
|
||||
angle: 3.14,
|
||||
intersectTag: 'a',
|
||||
offset: 0
|
||||
}, %)
|
||||
|> tangentialArcTo([3.14, 13.14], %)`
|
||||
const ast = parse(code)
|
||||
test.each([
|
||||
[
|
||||
'line',
|
||||
[
|
||||
{
|
||||
type: 'xRelative',
|
||||
isConstrained: false,
|
||||
value: '3',
|
||||
sourceRange: [78, 79],
|
||||
argPosition: { type: 'arrayItem', index: 0 },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'line',
|
||||
},
|
||||
{
|
||||
type: 'yRelative',
|
||||
isConstrained: false,
|
||||
value: '4',
|
||||
sourceRange: [81, 82],
|
||||
argPosition: { type: 'arrayItem', index: 1 },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'line',
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
`angledLine(`,
|
||||
[
|
||||
{
|
||||
type: 'angle',
|
||||
isConstrained: false,
|
||||
value: '3.14',
|
||||
sourceRange: [117, 121],
|
||||
argPosition: { type: 'objectProperty', key: 'angle' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLine',
|
||||
},
|
||||
{
|
||||
type: 'length',
|
||||
isConstrained: false,
|
||||
value: '3.14',
|
||||
sourceRange: [135, 139],
|
||||
argPosition: { type: 'objectProperty', key: 'length' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLine',
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'lineTo',
|
||||
[
|
||||
{
|
||||
type: 'xAbsolute',
|
||||
isConstrained: false,
|
||||
value: '6.14',
|
||||
sourceRange: [162, 166],
|
||||
argPosition: { type: 'arrayItem', index: 0 },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'lineTo',
|
||||
},
|
||||
{
|
||||
type: 'yAbsolute',
|
||||
isConstrained: false,
|
||||
value: '3.14',
|
||||
sourceRange: [168, 172],
|
||||
argPosition: { type: 'arrayItem', index: 1 },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'lineTo',
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'xLineTo',
|
||||
[
|
||||
{
|
||||
type: 'horizontal',
|
||||
isConstrained: true,
|
||||
value: 'xLineTo',
|
||||
sourceRange: [183, 190],
|
||||
argPosition: undefined,
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'xLineTo',
|
||||
},
|
||||
{
|
||||
type: 'xAbsolute',
|
||||
isConstrained: false,
|
||||
value: '8',
|
||||
sourceRange: [191, 192],
|
||||
argPosition: { type: 'singleValue' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'xLineTo',
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'yLineTo',
|
||||
[
|
||||
{
|
||||
type: 'vertical',
|
||||
isConstrained: true,
|
||||
value: 'yLineTo',
|
||||
sourceRange: [202, 209],
|
||||
argPosition: undefined,
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'yLineTo',
|
||||
},
|
||||
{
|
||||
type: 'yAbsolute',
|
||||
isConstrained: false,
|
||||
value: '5',
|
||||
sourceRange: [210, 211],
|
||||
argPosition: { type: 'singleValue' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'yLineTo',
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'yLine(',
|
||||
[
|
||||
{
|
||||
type: 'vertical',
|
||||
isConstrained: true,
|
||||
value: 'yLine',
|
||||
sourceRange: [221, 226],
|
||||
argPosition: undefined,
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'yLine',
|
||||
},
|
||||
{
|
||||
type: 'yRelative',
|
||||
isConstrained: false,
|
||||
value: '3.14',
|
||||
sourceRange: [227, 231],
|
||||
argPosition: { type: 'singleValue' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'yLine',
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'xLine(',
|
||||
[
|
||||
{
|
||||
type: 'horizontal',
|
||||
isConstrained: true,
|
||||
value: 'xLine',
|
||||
sourceRange: [246, 251],
|
||||
argPosition: undefined,
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'xLine',
|
||||
},
|
||||
{
|
||||
type: 'xRelative',
|
||||
isConstrained: false,
|
||||
value: '3.14',
|
||||
sourceRange: [252, 256],
|
||||
argPosition: { type: 'singleValue' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'xLine',
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'angledLineOfXLength',
|
||||
[
|
||||
{
|
||||
type: 'angle',
|
||||
isConstrained: false,
|
||||
value: '3.14',
|
||||
sourceRange: [299, 303],
|
||||
argPosition: { type: 'objectProperty', key: 'angle' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineOfXLength',
|
||||
},
|
||||
{
|
||||
type: 'xRelative',
|
||||
isConstrained: false,
|
||||
value: '3.14',
|
||||
sourceRange: [317, 321],
|
||||
argPosition: { type: 'objectProperty', key: 'length' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineOfXLength',
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'angledLineOfYLength',
|
||||
[
|
||||
{
|
||||
type: 'angle',
|
||||
isConstrained: false,
|
||||
value: '30',
|
||||
sourceRange: [369, 371],
|
||||
argPosition: { type: 'objectProperty', key: 'angle' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineOfYLength',
|
||||
},
|
||||
{
|
||||
type: 'yRelative',
|
||||
isConstrained: false,
|
||||
value: '3',
|
||||
sourceRange: [385, 386],
|
||||
argPosition: { type: 'objectProperty', key: 'length' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineOfYLength',
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'angledLineToX',
|
||||
[
|
||||
{
|
||||
type: 'angle',
|
||||
isConstrained: false,
|
||||
value: '12.14',
|
||||
sourceRange: [428, 433],
|
||||
argPosition: { type: 'objectProperty', key: 'angle' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineToX',
|
||||
},
|
||||
{
|
||||
type: 'xAbsolute',
|
||||
isConstrained: false,
|
||||
value: '12',
|
||||
sourceRange: [443, 445],
|
||||
argPosition: { type: 'objectProperty', key: 'to' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineToX',
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'angledLineToY',
|
||||
[
|
||||
{
|
||||
type: 'angle',
|
||||
isConstrained: false,
|
||||
value: '30',
|
||||
sourceRange: [487, 489],
|
||||
argPosition: { type: 'objectProperty', key: 'angle' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineToY',
|
||||
},
|
||||
{
|
||||
type: 'yAbsolute',
|
||||
isConstrained: false,
|
||||
value: '10.14',
|
||||
sourceRange: [499, 504],
|
||||
argPosition: { type: 'objectProperty', key: 'to' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineToY',
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'angledLineThatIntersects',
|
||||
[
|
||||
{
|
||||
type: 'angle',
|
||||
isConstrained: false,
|
||||
value: '3.14',
|
||||
sourceRange: [557, 561],
|
||||
argPosition: { type: 'objectProperty', key: 'angle' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineThatIntersects',
|
||||
},
|
||||
{
|
||||
type: 'intersectionOffset',
|
||||
isConstrained: false,
|
||||
value: '0',
|
||||
sourceRange: [598, 599],
|
||||
argPosition: { type: 'objectProperty', key: 'offset' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineThatIntersects',
|
||||
},
|
||||
{
|
||||
type: 'intersectionTag',
|
||||
isConstrained: false,
|
||||
value: "'a'",
|
||||
sourceRange: [581, 584],
|
||||
argPosition: {
|
||||
key: 'intersectTag',
|
||||
type: 'objectProperty',
|
||||
},
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineThatIntersects',
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'tangentialArcTo',
|
||||
[
|
||||
{
|
||||
type: 'tangentialWithPrevious',
|
||||
isConstrained: true,
|
||||
value: 'tangentialArcTo',
|
||||
sourceRange: [613, 628],
|
||||
argPosition: undefined,
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'tangentialArcTo',
|
||||
},
|
||||
{
|
||||
type: 'xAbsolute',
|
||||
isConstrained: false,
|
||||
value: '3.14',
|
||||
sourceRange: [630, 634],
|
||||
argPosition: { type: 'arrayItem', index: 0 },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'tangentialArcTo',
|
||||
},
|
||||
{
|
||||
type: 'yAbsolute',
|
||||
isConstrained: false,
|
||||
value: '13.14',
|
||||
sourceRange: [636, 641],
|
||||
argPosition: { type: 'arrayItem', index: 1 },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'tangentialArcTo',
|
||||
},
|
||||
],
|
||||
],
|
||||
])('testing %s when inputs are unconstrained', (functionName, expected) => {
|
||||
const sourceRange: SourceRange = [
|
||||
code.indexOf(functionName),
|
||||
code.indexOf(functionName) + functionName.length,
|
||||
]
|
||||
const pathToNode = getNodePathFromSourceRange(ast, sourceRange)
|
||||
const callExp = getNodeFromPath<CallExpression>(
|
||||
ast,
|
||||
pathToNode,
|
||||
'CallExpression'
|
||||
).node
|
||||
const result = getConstraintInfo(callExp, code, pathToNode)
|
||||
expect(result).toEqual(expected)
|
||||
})
|
||||
})
|
||||
describe('array notation', () => {
|
||||
const code = `const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> line([3, 4], %)
|
||||
|> angledLine([3.14, 3.14], %)
|
||||
|> lineTo([6.14, 3.14], %)
|
||||
|> xLineTo(8, %)
|
||||
|> yLineTo(5, %)
|
||||
|> yLine(3.14, %, 'a')
|
||||
|> xLine(3.14, %)
|
||||
|> angledLineOfXLength([3.14, 3.14], %)
|
||||
|> angledLineOfYLength([30, 3], %)
|
||||
|> angledLineToX([12, 12], %)
|
||||
|> angledLineToY([30, 10], %)
|
||||
|> angledLineThatIntersects({
|
||||
angle: 3.14,
|
||||
intersectTag: 'a',
|
||||
offset: 0
|
||||
}, %)
|
||||
|> tangentialArcTo([3.14, 13.14], %)`
|
||||
const ast = parse(code)
|
||||
test.each([
|
||||
[
|
||||
`angledLine(`,
|
||||
[
|
||||
{
|
||||
type: 'angle',
|
||||
isConstrained: false,
|
||||
value: '3.14',
|
||||
sourceRange: [112, 116],
|
||||
argPosition: { type: 'arrayItem', index: 0 },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLine',
|
||||
},
|
||||
{
|
||||
type: 'length',
|
||||
isConstrained: false,
|
||||
value: '3.14',
|
||||
sourceRange: [118, 122],
|
||||
argPosition: { type: 'arrayItem', index: 1 },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLine',
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'angledLineOfXLength',
|
||||
[
|
||||
{
|
||||
type: 'angle',
|
||||
isConstrained: false,
|
||||
value: '3.14',
|
||||
sourceRange: [278, 282],
|
||||
argPosition: { type: 'arrayItem', index: 0 },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineOfXLength',
|
||||
},
|
||||
{
|
||||
type: 'xRelative',
|
||||
isConstrained: false,
|
||||
value: '3.14',
|
||||
sourceRange: [284, 288],
|
||||
argPosition: { type: 'arrayItem', index: 1 },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineOfXLength',
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'angledLineOfYLength',
|
||||
[
|
||||
{
|
||||
type: 'angle',
|
||||
isConstrained: false,
|
||||
value: '30',
|
||||
sourceRange: [322, 324],
|
||||
argPosition: { type: 'arrayItem', index: 0 },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineOfYLength',
|
||||
},
|
||||
{
|
||||
type: 'yRelative',
|
||||
isConstrained: false,
|
||||
value: '3',
|
||||
sourceRange: [326, 327],
|
||||
argPosition: { type: 'arrayItem', index: 1 },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineOfYLength',
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'angledLineToX',
|
||||
[
|
||||
{
|
||||
type: 'angle',
|
||||
isConstrained: false,
|
||||
value: '12',
|
||||
sourceRange: [355, 357],
|
||||
argPosition: { type: 'arrayItem', index: 0 },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineToX',
|
||||
},
|
||||
{
|
||||
type: 'xAbsolute',
|
||||
isConstrained: false,
|
||||
value: '12',
|
||||
sourceRange: [359, 361],
|
||||
argPosition: { type: 'arrayItem', index: 1 },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineToX',
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'angledLineToY',
|
||||
[
|
||||
{
|
||||
type: 'angle',
|
||||
isConstrained: false,
|
||||
value: '30',
|
||||
sourceRange: [389, 391],
|
||||
argPosition: { type: 'arrayItem', index: 0 },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineToY',
|
||||
},
|
||||
{
|
||||
type: 'yAbsolute',
|
||||
isConstrained: false,
|
||||
value: '10',
|
||||
sourceRange: [393, 395],
|
||||
argPosition: { type: 'arrayItem', index: 1 },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineToY',
|
||||
},
|
||||
],
|
||||
],
|
||||
])('testing %s when inputs are unconstrained', (functionName, expected) => {
|
||||
const sourceRange: SourceRange = [
|
||||
code.indexOf(functionName),
|
||||
code.indexOf(functionName) + functionName.length,
|
||||
]
|
||||
const pathToNode = getNodePathFromSourceRange(ast, sourceRange)
|
||||
const callExp = getNodeFromPath<CallExpression>(
|
||||
ast,
|
||||
pathToNode,
|
||||
'CallExpression'
|
||||
).node
|
||||
const result = getConstraintInfo(callExp, code, pathToNode)
|
||||
expect(result).toEqual(expected)
|
||||
})
|
||||
})
|
||||
describe('constrained', () => {
|
||||
const code = `const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> line([3 + 0, 4 + 0], %)
|
||||
|> angledLine({ angle: 3.14 + 0, length: 3.14 + 0 }, %)
|
||||
|> lineTo([6.14 + 0, 3.14 + 0], %)
|
||||
|> xLineTo(8 + 0, %)
|
||||
|> yLineTo(5 + 0, %)
|
||||
|> yLine(3.14 + 0, %, 'a')
|
||||
|> xLine(3.14 + 0, %)
|
||||
|> angledLineOfXLength({ angle: 3.14 + 0, length: 3.14 + 0 }, %)
|
||||
|> angledLineOfYLength({ angle: 30 + 0, length: 3 + 0 }, %)
|
||||
|> angledLineToX({ angle: 12.14 + 0, to: 12 + 0 }, %)
|
||||
|> angledLineToY({ angle: 30 + 0, to: 10.14 + 0 }, %)
|
||||
|> angledLineThatIntersects({
|
||||
angle: 3.14 + 0,
|
||||
intersectTag: 'a',
|
||||
offset: 0 + 0
|
||||
}, %)
|
||||
|> tangentialArcTo([3.14 + 0, 13.14 + 0], %)`
|
||||
const ast = parse(code)
|
||||
test.each([
|
||||
[
|
||||
'line',
|
||||
[
|
||||
{
|
||||
type: 'xRelative',
|
||||
isConstrained: true,
|
||||
value: '3 + 0',
|
||||
sourceRange: [83, 88],
|
||||
argPosition: { type: 'arrayItem', index: 0 },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'line',
|
||||
},
|
||||
{
|
||||
type: 'yRelative',
|
||||
isConstrained: true,
|
||||
value: '4 + 0',
|
||||
sourceRange: [90, 95],
|
||||
argPosition: { type: 'arrayItem', index: 1 },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'line',
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
`angledLine(`,
|
||||
[
|
||||
{
|
||||
type: 'angle',
|
||||
isConstrained: true,
|
||||
value: '3.14 + 0',
|
||||
sourceRange: [128, 136],
|
||||
argPosition: { type: 'objectProperty', key: 'angle' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLine',
|
||||
},
|
||||
{
|
||||
type: 'length',
|
||||
isConstrained: true,
|
||||
value: '3.14 + 0',
|
||||
sourceRange: [146, 154],
|
||||
argPosition: { type: 'objectProperty', key: 'length' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLine',
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'lineTo',
|
||||
[
|
||||
{
|
||||
type: 'xAbsolute',
|
||||
isConstrained: true,
|
||||
value: '6.14 + 0',
|
||||
sourceRange: [176, 184],
|
||||
argPosition: { type: 'arrayItem', index: 0 },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'lineTo',
|
||||
},
|
||||
{
|
||||
type: 'yAbsolute',
|
||||
isConstrained: true,
|
||||
value: '3.14 + 0',
|
||||
sourceRange: [186, 194],
|
||||
argPosition: { type: 'arrayItem', index: 1 },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'lineTo',
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'xLineTo',
|
||||
[
|
||||
{
|
||||
type: 'horizontal',
|
||||
isConstrained: true,
|
||||
value: 'xLineTo',
|
||||
sourceRange: [207, 214],
|
||||
argPosition: undefined,
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'xLineTo',
|
||||
},
|
||||
{
|
||||
type: 'xAbsolute',
|
||||
isConstrained: true,
|
||||
value: '8 + 0',
|
||||
sourceRange: [215, 220],
|
||||
argPosition: { type: 'singleValue' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'xLineTo',
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'yLineTo',
|
||||
[
|
||||
{
|
||||
type: 'vertical',
|
||||
isConstrained: true,
|
||||
value: 'yLineTo',
|
||||
sourceRange: [232, 239],
|
||||
argPosition: undefined,
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'yLineTo',
|
||||
},
|
||||
{
|
||||
type: 'yAbsolute',
|
||||
isConstrained: true,
|
||||
value: '5 + 0',
|
||||
sourceRange: [240, 245],
|
||||
argPosition: { type: 'singleValue' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'yLineTo',
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'yLine(',
|
||||
[
|
||||
{
|
||||
type: 'vertical',
|
||||
isConstrained: true,
|
||||
value: 'yLine',
|
||||
sourceRange: [257, 262],
|
||||
argPosition: undefined,
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'yLine',
|
||||
},
|
||||
{
|
||||
type: 'yRelative',
|
||||
isConstrained: true,
|
||||
value: '3.14 + 0',
|
||||
sourceRange: [263, 271],
|
||||
argPosition: { type: 'singleValue' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'yLine',
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'xLine(',
|
||||
[
|
||||
{
|
||||
type: 'horizontal',
|
||||
isConstrained: true,
|
||||
value: 'xLine',
|
||||
sourceRange: [288, 293],
|
||||
argPosition: undefined,
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'xLine',
|
||||
},
|
||||
{
|
||||
type: 'xRelative',
|
||||
isConstrained: true,
|
||||
value: '3.14 + 0',
|
||||
sourceRange: [294, 302],
|
||||
argPosition: { type: 'singleValue' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'xLine',
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'angledLineOfXLength',
|
||||
[
|
||||
{
|
||||
type: 'angle',
|
||||
isConstrained: true,
|
||||
value: '3.14 + 0',
|
||||
sourceRange: [343, 351],
|
||||
argPosition: { type: 'objectProperty', key: 'angle' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineOfXLength',
|
||||
},
|
||||
{
|
||||
type: 'xRelative',
|
||||
isConstrained: true,
|
||||
value: '3.14 + 0',
|
||||
sourceRange: [361, 369],
|
||||
argPosition: { type: 'objectProperty', key: 'length' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineOfXLength',
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'angledLineOfYLength',
|
||||
[
|
||||
{
|
||||
type: 'angle',
|
||||
isConstrained: true,
|
||||
value: '30 + 0',
|
||||
sourceRange: [412, 418],
|
||||
argPosition: { type: 'objectProperty', key: 'angle' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineOfYLength',
|
||||
},
|
||||
{
|
||||
type: 'yRelative',
|
||||
isConstrained: true,
|
||||
value: '3 + 0',
|
||||
sourceRange: [428, 433],
|
||||
argPosition: { type: 'objectProperty', key: 'length' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineOfYLength',
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'angledLineToX',
|
||||
[
|
||||
{
|
||||
type: 'angle',
|
||||
isConstrained: true,
|
||||
value: '12.14 + 0',
|
||||
sourceRange: [470, 479],
|
||||
argPosition: { type: 'objectProperty', key: 'angle' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineToX',
|
||||
},
|
||||
{
|
||||
type: 'xAbsolute',
|
||||
isConstrained: true,
|
||||
value: '12 + 0',
|
||||
sourceRange: [485, 491],
|
||||
argPosition: { type: 'objectProperty', key: 'to' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineToX',
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'angledLineToY',
|
||||
[
|
||||
{
|
||||
type: 'angle',
|
||||
isConstrained: true,
|
||||
value: '30 + 0',
|
||||
sourceRange: [528, 534],
|
||||
argPosition: { type: 'objectProperty', key: 'angle' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineToY',
|
||||
},
|
||||
{
|
||||
type: 'yAbsolute',
|
||||
isConstrained: true,
|
||||
value: '10.14 + 0',
|
||||
sourceRange: [540, 549],
|
||||
argPosition: { type: 'objectProperty', key: 'to' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineToY',
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'angledLineThatIntersects',
|
||||
[
|
||||
{
|
||||
type: 'angle',
|
||||
isConstrained: true,
|
||||
value: '3.14 + 0',
|
||||
sourceRange: [606, 614],
|
||||
argPosition: { type: 'objectProperty', key: 'angle' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineThatIntersects',
|
||||
},
|
||||
{
|
||||
type: 'intersectionOffset',
|
||||
isConstrained: true,
|
||||
value: '0 + 0',
|
||||
sourceRange: [661, 666],
|
||||
argPosition: { type: 'objectProperty', key: 'offset' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineThatIntersects',
|
||||
},
|
||||
{
|
||||
type: 'intersectionTag',
|
||||
isConstrained: false,
|
||||
value: "'a'",
|
||||
sourceRange: [639, 642],
|
||||
argPosition: { key: 'intersectTag', type: 'objectProperty' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineThatIntersects',
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'tangentialArcTo',
|
||||
[
|
||||
{
|
||||
type: 'tangentialWithPrevious',
|
||||
isConstrained: true,
|
||||
value: 'tangentialArcTo',
|
||||
sourceRange: [687, 702],
|
||||
argPosition: undefined,
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'tangentialArcTo',
|
||||
},
|
||||
{
|
||||
type: 'xAbsolute',
|
||||
isConstrained: true,
|
||||
value: '3.14 + 0',
|
||||
sourceRange: [704, 712],
|
||||
argPosition: { type: 'arrayItem', index: 0 },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'tangentialArcTo',
|
||||
},
|
||||
{
|
||||
type: 'yAbsolute',
|
||||
isConstrained: true,
|
||||
value: '13.14 + 0',
|
||||
sourceRange: [714, 723],
|
||||
argPosition: { type: 'arrayItem', index: 1 },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'tangentialArcTo',
|
||||
},
|
||||
],
|
||||
],
|
||||
])('testing %s when inputs are unconstrained', (functionName, expected) => {
|
||||
const sourceRange: SourceRange = [
|
||||
code.indexOf(functionName),
|
||||
code.indexOf(functionName) + functionName.length,
|
||||
]
|
||||
const pathToNode = getNodePathFromSourceRange(ast, sourceRange)
|
||||
const callExp = getNodeFromPath<CallExpression>(
|
||||
ast,
|
||||
pathToNode,
|
||||
'CallExpression'
|
||||
).node
|
||||
const result = getConstraintInfo(callExp, code, pathToNode)
|
||||
expect(result).toEqual(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -11,17 +11,32 @@ import {
|
||||
Value,
|
||||
Literal,
|
||||
VariableDeclaration,
|
||||
} from '../wasm'
|
||||
} from 'lang/wasm'
|
||||
import {
|
||||
getNodeFromPath,
|
||||
getNodeFromPathCurry,
|
||||
getNodePathFromSourceRange,
|
||||
} from '../queryAst'
|
||||
import { isLiteralArrayOrStatic } from './sketchcombos'
|
||||
} from 'lang/queryAst'
|
||||
import {
|
||||
LineInputsType,
|
||||
isLiteralArrayOrStatic,
|
||||
isNotLiteralArrayOrStatic,
|
||||
} from 'lang/std/sketchcombos'
|
||||
import { toolTips, ToolTip } from '../../useStore'
|
||||
import { createPipeExpression, splitPathAtPipeExpression } from '../modifyAst'
|
||||
|
||||
import { SketchLineHelper, ModifyAstBase, TransformCallback } from './stdTypes'
|
||||
import {
|
||||
SketchLineHelper,
|
||||
ModifyAstBase,
|
||||
TransformCallback,
|
||||
ConstrainInfo,
|
||||
RawValues,
|
||||
ArrayItemInput,
|
||||
ObjectPropertyInput,
|
||||
SingleValueInput,
|
||||
VarValueKeys,
|
||||
ArrayOrObjItemInput,
|
||||
} from 'lang/std/stdTypes'
|
||||
|
||||
import {
|
||||
createLiteral,
|
||||
@ -32,8 +47,8 @@ import {
|
||||
mutateArrExp,
|
||||
mutateObjExpProp,
|
||||
findUniqueName,
|
||||
} from '../modifyAst'
|
||||
import { roundOff, getLength, getAngle } from '../../lib/utils'
|
||||
} from 'lang/modifyAst'
|
||||
import { roundOff, getLength, getAngle } from 'lib/utils'
|
||||
import { perpendicularDistance } from 'sketch-helpers'
|
||||
|
||||
export type Coords2d = [number, number]
|
||||
@ -85,6 +100,210 @@ export function createFirstArg(
|
||||
throw new Error('all sketch line types should have been covered')
|
||||
}
|
||||
|
||||
type AbbreviatedInput =
|
||||
| ArrayItemInput<any>['index']
|
||||
| ObjectPropertyInput<any>['key']
|
||||
| SingleValueInput<any>['type']
|
||||
| undefined
|
||||
|
||||
const constrainInfo = (
|
||||
a: ConstrainInfo['type'],
|
||||
b: ConstrainInfo['isConstrained'],
|
||||
c: ConstrainInfo['value'],
|
||||
f: ConstrainInfo['stdLibFnName'],
|
||||
g: AbbreviatedInput,
|
||||
d: ConstrainInfo['sourceRange'],
|
||||
e: ConstrainInfo['pathToNode']
|
||||
): ConstrainInfo => ({
|
||||
type: a,
|
||||
isConstrained: b,
|
||||
value: c,
|
||||
sourceRange: d,
|
||||
argPosition:
|
||||
g === 'singleValue'
|
||||
? { type: 'singleValue' }
|
||||
: typeof g === 'number'
|
||||
? { type: 'arrayItem', index: g }
|
||||
: typeof g === 'string'
|
||||
? { type: 'objectProperty', key: g }
|
||||
: undefined,
|
||||
pathToNode: e,
|
||||
stdLibFnName: f,
|
||||
})
|
||||
|
||||
const commonConstraintInfoHelper = (
|
||||
callExp: CallExpression,
|
||||
inputConstrainTypes: [ConstrainInfo['type'], ConstrainInfo['type']],
|
||||
stdLibFnName: ConstrainInfo['stdLibFnName'],
|
||||
abbreviatedInputs: [
|
||||
{
|
||||
arrayInput?: 0 | 1
|
||||
objInput?: ObjectPropertyInput<any>['key']
|
||||
},
|
||||
{
|
||||
arrayInput?: 0 | 1
|
||||
objInput?: ObjectPropertyInput<any>['key']
|
||||
}
|
||||
],
|
||||
code: string,
|
||||
pathToNode: PathToNode
|
||||
) => {
|
||||
if (callExp.type !== 'CallExpression') return []
|
||||
const firstArg = callExp.arguments?.[0]
|
||||
const isArr = firstArg.type === 'ArrayExpression'
|
||||
if (!isArr && firstArg.type !== 'ObjectExpression') return []
|
||||
const pathToArrayExpression: PathToNode = [
|
||||
...pathToNode,
|
||||
['arguments', 'CallExpression'],
|
||||
[0, 'index'],
|
||||
isArr
|
||||
? ['elements', 'ArrayExpression']
|
||||
: ['properties', 'ObjectExpression'],
|
||||
]
|
||||
const pathToFirstArg: PathToNode = isArr
|
||||
? [...pathToArrayExpression, [0, 'index']]
|
||||
: [
|
||||
...pathToArrayExpression,
|
||||
[
|
||||
firstArg.properties.findIndex(
|
||||
(a) => a.key.name === abbreviatedInputs[0].objInput
|
||||
),
|
||||
'index',
|
||||
],
|
||||
['value', 'Property'],
|
||||
]
|
||||
|
||||
const pathToSecondArg: PathToNode = isArr
|
||||
? [...pathToArrayExpression, [1, 'index']]
|
||||
: [
|
||||
...pathToArrayExpression,
|
||||
[
|
||||
firstArg.properties.findIndex(
|
||||
(a) => a.key.name === abbreviatedInputs[1].objInput
|
||||
),
|
||||
'index',
|
||||
],
|
||||
['value', 'Property'],
|
||||
]
|
||||
|
||||
const input1 = isArr
|
||||
? firstArg.elements[0]
|
||||
: firstArg.properties.find(
|
||||
(a) => a.key.name === abbreviatedInputs[0].objInput
|
||||
)?.value
|
||||
const input2 = isArr
|
||||
? firstArg.elements[1]
|
||||
: firstArg.properties.find(
|
||||
(a) => a.key.name === abbreviatedInputs[1].objInput
|
||||
)?.value
|
||||
|
||||
const constraints: ConstrainInfo[] = []
|
||||
if (input1)
|
||||
constraints.push(
|
||||
constrainInfo(
|
||||
inputConstrainTypes[0],
|
||||
isNotLiteralArrayOrStatic(input1),
|
||||
code.slice(input1.start, input1.end),
|
||||
stdLibFnName,
|
||||
isArr ? abbreviatedInputs[0].arrayInput : abbreviatedInputs[0].objInput,
|
||||
[input1.start, input1.end],
|
||||
pathToFirstArg
|
||||
)
|
||||
)
|
||||
if (input2)
|
||||
constraints.push(
|
||||
constrainInfo(
|
||||
inputConstrainTypes[1],
|
||||
isNotLiteralArrayOrStatic(input2),
|
||||
code.slice(input2.start, input2.end),
|
||||
stdLibFnName,
|
||||
isArr ? abbreviatedInputs[1].arrayInput : abbreviatedInputs[1].objInput,
|
||||
[input2.start, input2.end],
|
||||
pathToSecondArg
|
||||
)
|
||||
)
|
||||
|
||||
return constraints
|
||||
}
|
||||
|
||||
const horzVertConstraintInfoHelper = (
|
||||
callExp: CallExpression,
|
||||
inputConstrainTypes: [ConstrainInfo['type'], ConstrainInfo['type']],
|
||||
stdLibFnName: ConstrainInfo['stdLibFnName'],
|
||||
abbreviatedInput: AbbreviatedInput,
|
||||
code: string,
|
||||
pathToNode: PathToNode
|
||||
) => {
|
||||
if (callExp.type !== 'CallExpression') return []
|
||||
const firstArg = callExp.arguments?.[0]
|
||||
const callee = callExp.callee
|
||||
const pathToFirstArg: PathToNode = [
|
||||
...pathToNode,
|
||||
['arguments', 'CallExpression'],
|
||||
[0, 'index'],
|
||||
]
|
||||
const pathToCallee: PathToNode = [...pathToNode, ['callee', 'CallExpression']]
|
||||
return [
|
||||
constrainInfo(
|
||||
inputConstrainTypes[0],
|
||||
true,
|
||||
callee.name,
|
||||
stdLibFnName,
|
||||
undefined,
|
||||
[callee.start, callee.end],
|
||||
pathToCallee
|
||||
),
|
||||
constrainInfo(
|
||||
inputConstrainTypes[1],
|
||||
isNotLiteralArrayOrStatic(callExp.arguments?.[0]),
|
||||
code.slice(firstArg.start, firstArg.end),
|
||||
stdLibFnName,
|
||||
abbreviatedInput,
|
||||
[firstArg.start, firstArg.end],
|
||||
pathToFirstArg
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
function arrayRawValuesHelper(a: Array<[Literal, LineInputsType]>): RawValues {
|
||||
return a.map(
|
||||
([literal, argType], index): ArrayItemInput<Literal> => ({
|
||||
type: 'arrayItem',
|
||||
index: index === 0 ? 0 : 1,
|
||||
argType,
|
||||
value: literal,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
function arrOrObjectRawValuesHelper(
|
||||
a: Array<[Literal, LineInputsType, VarValueKeys]>
|
||||
): RawValues {
|
||||
return a.map(
|
||||
([literal, argType, key], index): ArrayOrObjItemInput<Literal> => ({
|
||||
type: 'arrayOrObjItem',
|
||||
// key: argType,w
|
||||
index: index === 0 ? 0 : 1,
|
||||
key,
|
||||
argType,
|
||||
value: literal,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
function singleRawValueHelper(
|
||||
literal: Literal,
|
||||
argType: LineInputsType
|
||||
): RawValues {
|
||||
return [
|
||||
{
|
||||
type: 'singleValue',
|
||||
argType,
|
||||
value: literal,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
export const lineTo: SketchLineHelper = {
|
||||
add: ({
|
||||
node,
|
||||
@ -114,6 +333,10 @@ export const lineTo: SketchLineHelper = {
|
||||
if (replaceExisting && createCallback) {
|
||||
const { callExp, valueUsedInTransform } = createCallback(
|
||||
newVals,
|
||||
arrayRawValuesHelper([
|
||||
[createLiteral(roundOff(to[0], 2)), 'xAbsolute'],
|
||||
[createLiteral(roundOff(to[1], 2)), 'yAbsolute'],
|
||||
]),
|
||||
referencedSegment
|
||||
)
|
||||
pipe.body[callIndex] = callExp
|
||||
@ -150,6 +373,14 @@ export const lineTo: SketchLineHelper = {
|
||||
}
|
||||
},
|
||||
addTag: addTag(),
|
||||
getConstraintInfo: (callExp, ...args) =>
|
||||
commonConstraintInfoHelper(
|
||||
callExp,
|
||||
['xAbsolute', 'yAbsolute'],
|
||||
'lineTo',
|
||||
[{ arrayInput: 0 }, { arrayInput: 1 }],
|
||||
...args
|
||||
),
|
||||
}
|
||||
|
||||
export const line: SketchLineHelper = {
|
||||
@ -207,6 +438,10 @@ export const line: SketchLineHelper = {
|
||||
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
|
||||
const { callExp, valueUsedInTransform } = createCallback(
|
||||
[newXVal, newYVal],
|
||||
arrayRawValuesHelper([
|
||||
[createLiteral(roundOff(to[0] - from[0], 2)), 'xRelative'],
|
||||
[createLiteral(roundOff(to[1] - from[1], 2)), 'yRelative'],
|
||||
]),
|
||||
referencedSegment
|
||||
)
|
||||
pipe.body[callIndex] = callExp
|
||||
@ -266,6 +501,14 @@ export const line: SketchLineHelper = {
|
||||
}
|
||||
},
|
||||
addTag: addTag(),
|
||||
getConstraintInfo: (callExp, ...args) =>
|
||||
commonConstraintInfoHelper(
|
||||
callExp,
|
||||
['xRelative', 'yRelative'],
|
||||
'line',
|
||||
[{ arrayInput: 0 }, { arrayInput: 1 }],
|
||||
...args
|
||||
),
|
||||
}
|
||||
|
||||
export const xLineTo: SketchLineHelper = {
|
||||
@ -278,7 +521,10 @@ export const xLineTo: SketchLineHelper = {
|
||||
|
||||
if (replaceExisting && createCallback) {
|
||||
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
|
||||
const { callExp, valueUsedInTransform } = createCallback([newVal, newVal])
|
||||
const { callExp, valueUsedInTransform } = createCallback(
|
||||
[newVal, newVal],
|
||||
singleRawValueHelper(newVal, 'xAbsolute')
|
||||
)
|
||||
pipe.body[callIndex] = callExp
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
@ -314,6 +560,14 @@ export const xLineTo: SketchLineHelper = {
|
||||
}
|
||||
},
|
||||
addTag: addTag(),
|
||||
getConstraintInfo: (callExp, ...args) =>
|
||||
horzVertConstraintInfoHelper(
|
||||
callExp,
|
||||
['horizontal', 'xAbsolute'],
|
||||
'xLineTo',
|
||||
'singleValue',
|
||||
...args
|
||||
),
|
||||
}
|
||||
|
||||
export const yLineTo: SketchLineHelper = {
|
||||
@ -326,7 +580,10 @@ export const yLineTo: SketchLineHelper = {
|
||||
|
||||
if (replaceExisting && createCallback) {
|
||||
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
|
||||
const { callExp, valueUsedInTransform } = createCallback([newVal, newVal])
|
||||
const { callExp, valueUsedInTransform } = createCallback(
|
||||
[newVal, newVal],
|
||||
singleRawValueHelper(newVal, 'yAbsolute')
|
||||
)
|
||||
pipe.body[callIndex] = callExp
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
@ -362,6 +619,14 @@ export const yLineTo: SketchLineHelper = {
|
||||
}
|
||||
},
|
||||
addTag: addTag(),
|
||||
getConstraintInfo: (callExp, ...args) =>
|
||||
horzVertConstraintInfoHelper(
|
||||
callExp,
|
||||
['vertical', 'yAbsolute'],
|
||||
'yLineTo',
|
||||
'singleValue',
|
||||
...args
|
||||
),
|
||||
}
|
||||
|
||||
export const xLine: SketchLineHelper = {
|
||||
@ -375,10 +640,10 @@ export const xLine: SketchLineHelper = {
|
||||
|
||||
if (replaceExisting && createCallback) {
|
||||
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
|
||||
const { callExp, valueUsedInTransform } = createCallback([
|
||||
firstArg,
|
||||
firstArg,
|
||||
])
|
||||
const { callExp, valueUsedInTransform } = createCallback(
|
||||
[firstArg, firstArg],
|
||||
singleRawValueHelper(firstArg, 'xRelative')
|
||||
)
|
||||
pipe.body[callIndex] = callExp
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
@ -412,6 +677,14 @@ export const xLine: SketchLineHelper = {
|
||||
}
|
||||
},
|
||||
addTag: addTag(),
|
||||
getConstraintInfo: (callExp, ...args) =>
|
||||
horzVertConstraintInfoHelper(
|
||||
callExp,
|
||||
['horizontal', 'xRelative'],
|
||||
'xLine',
|
||||
'singleValue',
|
||||
...args
|
||||
),
|
||||
}
|
||||
|
||||
export const yLine: SketchLineHelper = {
|
||||
@ -422,7 +695,10 @@ export const yLine: SketchLineHelper = {
|
||||
const newVal = createLiteral(roundOff(to[1] - from[1], 2))
|
||||
if (replaceExisting && createCallback) {
|
||||
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
|
||||
const { callExp, valueUsedInTransform } = createCallback([newVal, newVal])
|
||||
const { callExp, valueUsedInTransform } = createCallback(
|
||||
[newVal, newVal],
|
||||
singleRawValueHelper(newVal, 'yRelative')
|
||||
)
|
||||
pipe.body[callIndex] = callExp
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
@ -456,6 +732,14 @@ export const yLine: SketchLineHelper = {
|
||||
}
|
||||
},
|
||||
addTag: addTag(),
|
||||
getConstraintInfo: (callExp, ...args) =>
|
||||
horzVertConstraintInfoHelper(
|
||||
callExp,
|
||||
['vertical', 'yRelative'],
|
||||
'yLine',
|
||||
'singleValue',
|
||||
...args
|
||||
),
|
||||
}
|
||||
|
||||
export const tangentialArcTo: SketchLineHelper = {
|
||||
@ -485,6 +769,10 @@ export const tangentialArcTo: SketchLineHelper = {
|
||||
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
|
||||
const { callExp, valueUsedInTransform } = createCallback(
|
||||
[toX, toY],
|
||||
arrayRawValuesHelper([
|
||||
[createLiteral(roundOff(to[0], 2)), 'xAbsolute'],
|
||||
[createLiteral(roundOff(to[1], 2)), 'yAbsolute'],
|
||||
]),
|
||||
referencedSegment
|
||||
)
|
||||
pipe.body[callIndex] = callExp
|
||||
@ -534,8 +822,54 @@ export const tangentialArcTo: SketchLineHelper = {
|
||||
pathToNode,
|
||||
}
|
||||
},
|
||||
// TODO copy-paste from angledLine
|
||||
addTag: addTag(),
|
||||
getConstraintInfo: (callExp: CallExpression, code, pathToNode) => {
|
||||
if (callExp.type !== 'CallExpression') return []
|
||||
const firstArg = callExp.arguments?.[0]
|
||||
if (firstArg.type !== 'ArrayExpression') return []
|
||||
const callee = callExp.callee
|
||||
const pathToCallee: PathToNode = [
|
||||
...pathToNode,
|
||||
['callee', 'CallExpression'],
|
||||
]
|
||||
const pathToArrayExpression: PathToNode = [
|
||||
...pathToNode,
|
||||
['arguments', 'CallExpression'],
|
||||
[0, 'index'],
|
||||
['elements', 'ArrayExpression'],
|
||||
]
|
||||
const pathToFirstArg: PathToNode = [...pathToArrayExpression, [0, 'index']]
|
||||
const pathToSecondArg: PathToNode = [...pathToArrayExpression, [1, 'index']]
|
||||
return [
|
||||
constrainInfo(
|
||||
'tangentialWithPrevious',
|
||||
true,
|
||||
callee.name,
|
||||
'tangentialArcTo',
|
||||
undefined,
|
||||
[callee.start, callee.end],
|
||||
pathToCallee
|
||||
),
|
||||
constrainInfo(
|
||||
'xAbsolute',
|
||||
isNotLiteralArrayOrStatic(firstArg.elements[0]),
|
||||
code.slice(firstArg.elements[0].start, firstArg.elements[0].end),
|
||||
'tangentialArcTo',
|
||||
0,
|
||||
[firstArg.elements[0].start, firstArg.elements[0].end],
|
||||
pathToFirstArg
|
||||
),
|
||||
constrainInfo(
|
||||
'yAbsolute',
|
||||
isNotLiteralArrayOrStatic(firstArg.elements[1]),
|
||||
code.slice(firstArg.elements[1].start, firstArg.elements[1].end),
|
||||
'tangentialArcTo',
|
||||
1,
|
||||
[firstArg.elements[1].start, firstArg.elements[1].end],
|
||||
pathToSecondArg
|
||||
),
|
||||
]
|
||||
},
|
||||
}
|
||||
export const angledLine: SketchLineHelper = {
|
||||
add: ({
|
||||
@ -562,6 +896,10 @@ export const angledLine: SketchLineHelper = {
|
||||
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
|
||||
const { callExp, valueUsedInTransform } = createCallback(
|
||||
[newAngleVal, newLengthVal],
|
||||
arrOrObjectRawValuesHelper([
|
||||
[newAngleVal, 'angle', 'angle'],
|
||||
[newLengthVal, 'length', 'length'],
|
||||
]),
|
||||
referencedSegment
|
||||
)
|
||||
pipe.body[callIndex] = callExp
|
||||
@ -602,6 +940,17 @@ export const angledLine: SketchLineHelper = {
|
||||
}
|
||||
},
|
||||
addTag: addTag(),
|
||||
getConstraintInfo: (callExp, ...args) =>
|
||||
commonConstraintInfoHelper(
|
||||
callExp,
|
||||
['angle', 'length'],
|
||||
'angledLine',
|
||||
[
|
||||
{ arrayInput: 0, objInput: 'angle' },
|
||||
{ arrayInput: 1, objInput: 'length' },
|
||||
],
|
||||
...args
|
||||
),
|
||||
}
|
||||
|
||||
export const angledLineOfXLength: SketchLineHelper = {
|
||||
@ -631,7 +980,13 @@ export const angledLineOfXLength: SketchLineHelper = {
|
||||
const angle = createLiteral(roundOff(getAngle(from, to), 0))
|
||||
const xLength = createLiteral(roundOff(Math.abs(from[0] - to[0]), 2) || 0.1)
|
||||
const newLine = createCallback
|
||||
? createCallback([angle, xLength]).callExp
|
||||
? createCallback(
|
||||
[angle, xLength],
|
||||
arrOrObjectRawValuesHelper([
|
||||
[angle, 'angle', 'angle'],
|
||||
[xLength, 'xRelative', 'length'],
|
||||
])
|
||||
).callExp
|
||||
: createCallExpression('angledLineOfXLength', [
|
||||
createArrayExpression([angle, xLength]),
|
||||
createPipeSubstitution(),
|
||||
@ -675,6 +1030,17 @@ export const angledLineOfXLength: SketchLineHelper = {
|
||||
}
|
||||
},
|
||||
addTag: addTag(),
|
||||
getConstraintInfo: (callExp, ...args) =>
|
||||
commonConstraintInfoHelper(
|
||||
callExp,
|
||||
['angle', 'xRelative'],
|
||||
'angledLineOfXLength',
|
||||
[
|
||||
{ arrayInput: 0, objInput: 'angle' },
|
||||
{ arrayInput: 1, objInput: 'length' },
|
||||
],
|
||||
...args
|
||||
),
|
||||
}
|
||||
|
||||
export const angledLineOfYLength: SketchLineHelper = {
|
||||
@ -705,7 +1071,13 @@ export const angledLineOfYLength: SketchLineHelper = {
|
||||
const angle = createLiteral(roundOff(getAngle(from, to), 0))
|
||||
const yLength = createLiteral(roundOff(Math.abs(from[1] - to[1]), 2) || 0.1)
|
||||
const newLine = createCallback
|
||||
? createCallback([angle, yLength]).callExp
|
||||
? createCallback(
|
||||
[angle, yLength],
|
||||
arrOrObjectRawValuesHelper([
|
||||
[angle, 'angle', 'angle'],
|
||||
[yLength, 'yRelative', 'length'],
|
||||
])
|
||||
).callExp
|
||||
: createCallExpression('angledLineOfYLength', [
|
||||
createArrayExpression([angle, yLength]),
|
||||
createPipeSubstitution(),
|
||||
@ -749,6 +1121,17 @@ export const angledLineOfYLength: SketchLineHelper = {
|
||||
}
|
||||
},
|
||||
addTag: addTag(),
|
||||
getConstraintInfo: (callExp, ...args) =>
|
||||
commonConstraintInfoHelper(
|
||||
callExp,
|
||||
['angle', 'yRelative'],
|
||||
'angledLineOfYLength',
|
||||
[
|
||||
{ arrayInput: 0, objInput: 'angle' },
|
||||
{ arrayInput: 1, objInput: 'length' },
|
||||
],
|
||||
...args
|
||||
),
|
||||
}
|
||||
|
||||
export const angledLineToX: SketchLineHelper = {
|
||||
@ -772,6 +1155,10 @@ export const angledLineToX: SketchLineHelper = {
|
||||
if (replaceExisting && createCallback) {
|
||||
const { callExp, valueUsedInTransform } = createCallback(
|
||||
[angle, xArg],
|
||||
arrOrObjectRawValuesHelper([
|
||||
[angle, 'angle', 'angle'],
|
||||
[xArg, 'xAbsolute', 'to'],
|
||||
]),
|
||||
referencedSegment
|
||||
)
|
||||
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
|
||||
@ -818,6 +1205,17 @@ export const angledLineToX: SketchLineHelper = {
|
||||
}
|
||||
},
|
||||
addTag: addTag(),
|
||||
getConstraintInfo: (callExp, ...args) =>
|
||||
commonConstraintInfoHelper(
|
||||
callExp,
|
||||
['angle', 'xAbsolute'],
|
||||
'angledLineToX',
|
||||
[
|
||||
{ arrayInput: 0, objInput: 'angle' },
|
||||
{ arrayInput: 1, objInput: 'to' },
|
||||
],
|
||||
...args
|
||||
),
|
||||
}
|
||||
|
||||
export const angledLineToY: SketchLineHelper = {
|
||||
@ -842,6 +1240,10 @@ export const angledLineToY: SketchLineHelper = {
|
||||
if (replaceExisting && createCallback) {
|
||||
const { callExp, valueUsedInTransform } = createCallback(
|
||||
[angle, yArg],
|
||||
arrOrObjectRawValuesHelper([
|
||||
[angle, 'angle', 'angle'],
|
||||
[yArg, 'yAbsolute', 'to'],
|
||||
]),
|
||||
referencedSegment
|
||||
)
|
||||
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
|
||||
@ -888,6 +1290,17 @@ export const angledLineToY: SketchLineHelper = {
|
||||
}
|
||||
},
|
||||
addTag: addTag(),
|
||||
getConstraintInfo: (callExp, ...args) =>
|
||||
commonConstraintInfoHelper(
|
||||
callExp,
|
||||
['angle', 'yAbsolute'],
|
||||
'angledLineToY',
|
||||
[
|
||||
{ arrayInput: 0, objInput: 'angle' },
|
||||
{ arrayInput: 1, objInput: 'to' },
|
||||
],
|
||||
...args
|
||||
),
|
||||
}
|
||||
|
||||
export const angledLineThatIntersects: SketchLineHelper = {
|
||||
@ -921,7 +1334,23 @@ export const angledLineThatIntersects: SketchLineHelper = {
|
||||
)
|
||||
|
||||
if (replaceExisting && createCallback) {
|
||||
const { callExp, valueUsedInTransform } = createCallback([angle, offset])
|
||||
const { callExp, valueUsedInTransform } = createCallback(
|
||||
[angle, offset],
|
||||
[
|
||||
{
|
||||
type: 'objectProperty',
|
||||
key: 'angle',
|
||||
value: angle,
|
||||
argType: 'angle',
|
||||
},
|
||||
{
|
||||
type: 'objectProperty',
|
||||
key: 'offset',
|
||||
value: offset,
|
||||
argType: 'intersectionOffset',
|
||||
},
|
||||
]
|
||||
)
|
||||
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
|
||||
pipe.body[callIndex] = callExp
|
||||
return {
|
||||
@ -976,7 +1405,86 @@ export const angledLineThatIntersects: SketchLineHelper = {
|
||||
pathToNode,
|
||||
}
|
||||
},
|
||||
addTag: addTag(), // TODO might be wrong
|
||||
addTag: addTag(),
|
||||
getConstraintInfo: (callExp: CallExpression, code, pathToNode) => {
|
||||
if (callExp.type !== 'CallExpression') return []
|
||||
const firstArg = callExp.arguments?.[0]
|
||||
if (firstArg.type !== 'ObjectExpression') return []
|
||||
const angleIndex = firstArg.properties.findIndex(
|
||||
(p) => p.key.name === 'angle'
|
||||
)
|
||||
const offsetIndex = firstArg.properties.findIndex(
|
||||
(p) => p.key.name === 'offset'
|
||||
)
|
||||
const intersectTag = firstArg.properties.findIndex(
|
||||
(p) => p.key.name === 'intersectTag'
|
||||
)
|
||||
const returnVal = []
|
||||
const pathToObjectExp: PathToNode = [
|
||||
...pathToNode,
|
||||
['arguments', 'CallExpression'],
|
||||
[0, 'index'],
|
||||
['properties', 'ObjectExpression'],
|
||||
]
|
||||
if (angleIndex !== -1) {
|
||||
const angle = firstArg.properties[angleIndex]?.value
|
||||
const pathToAngleProp: PathToNode = [
|
||||
...pathToObjectExp,
|
||||
[angleIndex, 'index'],
|
||||
['value', 'Property'],
|
||||
]
|
||||
returnVal.push(
|
||||
constrainInfo(
|
||||
'angle',
|
||||
isNotLiteralArrayOrStatic(angle),
|
||||
code.slice(angle.start, angle.end),
|
||||
'angledLineThatIntersects',
|
||||
'angle',
|
||||
[angle.start, angle.end],
|
||||
pathToAngleProp
|
||||
)
|
||||
)
|
||||
}
|
||||
if (offsetIndex !== -1) {
|
||||
const offset = firstArg.properties[offsetIndex]?.value
|
||||
const pathToOffsetProp: PathToNode = [
|
||||
...pathToObjectExp,
|
||||
[offsetIndex, 'index'],
|
||||
['value', 'Property'],
|
||||
]
|
||||
returnVal.push(
|
||||
constrainInfo(
|
||||
'intersectionOffset',
|
||||
isNotLiteralArrayOrStatic(offset),
|
||||
code.slice(offset.start, offset.end),
|
||||
'angledLineThatIntersects',
|
||||
'offset',
|
||||
[offset.start, offset.end],
|
||||
pathToOffsetProp
|
||||
)
|
||||
)
|
||||
}
|
||||
if (intersectTag !== -1) {
|
||||
const tag = firstArg.properties[intersectTag]?.value
|
||||
const pathToTagProp: PathToNode = [
|
||||
...pathToObjectExp,
|
||||
[intersectTag, 'index'],
|
||||
['value', 'Property'],
|
||||
]
|
||||
returnVal.push(
|
||||
constrainInfo(
|
||||
'intersectionTag',
|
||||
isNotLiteralArrayOrStatic(tag),
|
||||
code.slice(tag.start, tag.end),
|
||||
'angledLineThatIntersects',
|
||||
'intersectTag',
|
||||
[tag.start, tag.end],
|
||||
pathToTagProp
|
||||
)
|
||||
)
|
||||
}
|
||||
return returnVal
|
||||
},
|
||||
}
|
||||
|
||||
export const updateStartProfileAtArgs: SketchLineHelper['updateArgs'] = ({
|
||||
@ -1048,6 +1556,20 @@ export function changeSketchArguments(
|
||||
throw new Error(`not a sketch line helper: ${callExpression?.callee?.name}`)
|
||||
}
|
||||
|
||||
export function getConstraintInfo(
|
||||
callExpression: CallExpression,
|
||||
code: string,
|
||||
pathToNode: PathToNode
|
||||
): ConstrainInfo[] {
|
||||
const fnName = callExpression?.callee?.name || ''
|
||||
if (!(fnName in sketchLineHelperMap)) return []
|
||||
return sketchLineHelperMap[fnName].getConstraintInfo(
|
||||
callExpression,
|
||||
code,
|
||||
pathToNode
|
||||
)
|
||||
}
|
||||
|
||||
export function compareVec2Epsilon(
|
||||
vec1: [number, number],
|
||||
vec2: [number, number],
|
||||
@ -1162,7 +1684,7 @@ export function addCloseToPipe({
|
||||
export function replaceSketchLine({
|
||||
node,
|
||||
programMemory,
|
||||
sourceRange,
|
||||
pathToNode: _pathToNode,
|
||||
fnName,
|
||||
to,
|
||||
from,
|
||||
@ -1171,7 +1693,7 @@ export function replaceSketchLine({
|
||||
}: {
|
||||
node: Program
|
||||
programMemory: ProgramMemory
|
||||
sourceRange: SourceRange
|
||||
pathToNode: PathToNode
|
||||
fnName: ToolTip
|
||||
to: [number, number]
|
||||
from: [number, number]
|
||||
@ -1185,13 +1707,12 @@ export function replaceSketchLine({
|
||||
if (![...toolTips, 'intersect'].includes(fnName))
|
||||
throw new Error('not a tooltip')
|
||||
const _node = { ...node }
|
||||
const thePath = getNodePathFromSourceRange(_node, sourceRange)
|
||||
|
||||
const { add } = sketchLineHelperMap[fnName]
|
||||
const { modifiedAst, valueUsedInTransform, pathToNode } = add({
|
||||
node: _node,
|
||||
previousProgramMemory: programMemory,
|
||||
pathToNode: thePath,
|
||||
pathToNode: _pathToNode,
|
||||
referencedSegment,
|
||||
to,
|
||||
from,
|
||||
@ -1406,5 +1927,5 @@ export function getFirstArg(callExp: CallExpression): {
|
||||
// TODO probably needs it's own implementation
|
||||
return getFirstArgValuesForXYFns(callExp)
|
||||
}
|
||||
throw new Error('unexpected call expression')
|
||||
throw new Error('unexpected call expression: ' + name)
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { getNodeFromPath } from 'lang/queryAst'
|
||||
import { ToolTip, toolTips } from '../../useStore'
|
||||
import {
|
||||
Program,
|
||||
@ -6,8 +7,27 @@ import {
|
||||
SketchGroup,
|
||||
SourceRange,
|
||||
Path,
|
||||
PathToNode,
|
||||
Value,
|
||||
} from '../wasm'
|
||||
|
||||
export function getSketchSegmentFromPathToNode(
|
||||
sketchGroup: SketchGroup,
|
||||
ast: Program,
|
||||
pathToNode: PathToNode
|
||||
): {
|
||||
segment: SketchGroup['value'][number]
|
||||
index: number
|
||||
} {
|
||||
// TODO: once pathTodNode is stored on program memory as part of execution,
|
||||
// we can check if the pathToNode matches the pathToNode of the sketchGroup.
|
||||
// For now we fall back to the sourceRange
|
||||
const node = getNodeFromPath<Value>(ast, pathToNode).node
|
||||
if (!node || typeof node.start !== 'number' || !node.end)
|
||||
throw new Error('no node found')
|
||||
const sourceRange: SourceRange = [node.start, node.end]
|
||||
return getSketchSegmentFromSourceRange(sketchGroup, sourceRange)
|
||||
}
|
||||
export function getSketchSegmentFromSourceRange(
|
||||
sketchGroup: SketchGroup,
|
||||
[rangeStart, rangeEnd]: SourceRange
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { TransformCallback } from './stdTypes'
|
||||
import { TransformCallback, VarValues } from './stdTypes'
|
||||
import { toolTips, ToolTip } from '../../useStore'
|
||||
import { Selections, Selection } from 'lib/selections'
|
||||
import {
|
||||
@ -17,6 +17,7 @@ import {
|
||||
isValueZero,
|
||||
} from '../queryAst'
|
||||
import {
|
||||
createArrayExpression,
|
||||
createBinaryExpression,
|
||||
createBinaryExpressionWithUnary,
|
||||
createCallExpression,
|
||||
@ -27,17 +28,27 @@ import {
|
||||
createUnaryExpression,
|
||||
giveSketchFnCallTag,
|
||||
} from '../modifyAst'
|
||||
import { createFirstArg, getFirstArg, replaceSketchLine } from './sketch'
|
||||
import { getSketchSegmentFromSourceRange } from './sketchConstraints'
|
||||
import {
|
||||
createFirstArg,
|
||||
getConstraintInfo,
|
||||
getFirstArg,
|
||||
replaceSketchLine,
|
||||
} from './sketch'
|
||||
import {
|
||||
getSketchSegmentFromPathToNode,
|
||||
getSketchSegmentFromSourceRange,
|
||||
} from './sketchConstraints'
|
||||
import { getAngle, roundOff, normaliseAngle } from '../../lib/utils'
|
||||
|
||||
type LineInputsType =
|
||||
export type LineInputsType =
|
||||
| 'xAbsolute'
|
||||
| 'yAbsolute'
|
||||
| 'xRelative'
|
||||
| 'yRelative'
|
||||
| 'angle'
|
||||
| 'length'
|
||||
| 'intersectionOffset'
|
||||
| 'intersectionTag'
|
||||
|
||||
export type ConstraintType =
|
||||
| 'equalLength'
|
||||
@ -70,6 +81,31 @@ function createCallWrapper(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstracts creation of a callExpression ready for use for a sketchCombo transform
|
||||
* Assume it exists within a pipe and adds the pipe substitution
|
||||
* @param tool line, lineTo, angledLine, etc
|
||||
* @param val The first argument to the function
|
||||
* @param tag
|
||||
* @param valueUsedInTransform
|
||||
* @returns
|
||||
*/
|
||||
function createStdlibCallExpression(
|
||||
tool: ToolTip,
|
||||
val: Value,
|
||||
tag?: Value,
|
||||
valueUsedInTransform?: number
|
||||
): ReturnType<TransformCallback> {
|
||||
const args = [val, createPipeSubstitution()]
|
||||
if (tag) {
|
||||
args.push(tag)
|
||||
}
|
||||
return {
|
||||
callExp: createCallExpression(tool, args),
|
||||
valueUsedInTransform,
|
||||
}
|
||||
}
|
||||
|
||||
function intersectCallWrapper({
|
||||
fnName,
|
||||
angleVal,
|
||||
@ -106,6 +142,7 @@ function intersectCallWrapper({
|
||||
export type TransformInfo = {
|
||||
tooltip: ToolTip
|
||||
createNode: (a: {
|
||||
varValues: VarValues
|
||||
varValA: Value // x / angle
|
||||
varValB: Value // y / length or x y for angledLineOfXlength etc
|
||||
referenceSegName: string
|
||||
@ -145,7 +182,7 @@ const basicAngledLineCreateNode =
|
||||
varValToUse: 'ang' | 'len' | 'none' = 'none'
|
||||
): TransformInfo['createNode'] =>
|
||||
({ referenceSegName, tag, forceValueUsedInTransform, varValA, varValB }) =>
|
||||
(args, path) => {
|
||||
(args, _, path) => {
|
||||
const refAng = path ? getAngle(path?.from, path?.to) : 0
|
||||
const nonForcedAng =
|
||||
varValToUse === 'ang'
|
||||
@ -252,7 +289,7 @@ const setHorzVertDistanceCreateNode =
|
||||
index = xOrY === 'x' ? 0 : 1
|
||||
): TransformInfo['createNode'] =>
|
||||
({ referenceSegName, tag, forceValueUsedInTransform }) => {
|
||||
return (args, referencedSegment) => {
|
||||
return (args, _, referencedSegment) => {
|
||||
const valueUsedInTransform = roundOff(
|
||||
getArgLiteralVal(args?.[index]) - (referencedSegment?.to?.[index] || 0),
|
||||
2
|
||||
@ -279,7 +316,7 @@ const setHorzVertDistanceForAngleLineCreateNode =
|
||||
index = xOrY === 'x' ? 0 : 1
|
||||
): TransformInfo['createNode'] =>
|
||||
({ referenceSegName, tag, forceValueUsedInTransform, varValA }) => {
|
||||
return (args, referencedSegment) => {
|
||||
return (args, _, referencedSegment) => {
|
||||
const valueUsedInTransform = roundOff(
|
||||
getArgLiteralVal(args?.[1]) - (referencedSegment?.to?.[index] || 0),
|
||||
2
|
||||
@ -305,7 +342,7 @@ const setAbsDistanceCreateNode =
|
||||
index = xOrY === 'x' ? 0 : 1
|
||||
): TransformInfo['createNode'] =>
|
||||
({ tag, forceValueUsedInTransform }) => {
|
||||
return (args, referencedSegment) => {
|
||||
return (args, _, referencedSegment) => {
|
||||
const valueUsedInTransform = roundOff(
|
||||
getArgLiteralVal(args?.[index]) - (referencedSegment?.to?.[index] || 0),
|
||||
2
|
||||
@ -335,7 +372,7 @@ const setAbsDistanceForAngleLineCreateNode =
|
||||
index = xOrY === 'x' ? 0 : 1
|
||||
): TransformInfo['createNode'] =>
|
||||
({ tag, forceValueUsedInTransform, varValA }) => {
|
||||
return (args, referencedSegment) => {
|
||||
return (args) => {
|
||||
const valueUsedInTransform = roundOff(getArgLiteralVal(args?.[1]), 2)
|
||||
const val =
|
||||
(forceValueUsedInTransform as BinaryPart) ||
|
||||
@ -352,7 +389,7 @@ const setAbsDistanceForAngleLineCreateNode =
|
||||
const setHorVertDistanceForXYLines =
|
||||
(xOrY: 'x' | 'y'): TransformInfo['createNode'] =>
|
||||
({ referenceSegName, tag, forceValueUsedInTransform }) => {
|
||||
return (args, referencedSegment) => {
|
||||
return (args, _, referencedSegment) => {
|
||||
const index = xOrY === 'x' ? 0 : 1
|
||||
const valueUsedInTransform = roundOff(
|
||||
getArgLiteralVal(args?.[index]) - (referencedSegment?.to?.[index] || 0),
|
||||
@ -381,7 +418,7 @@ const setHorzVertDistanceConstraintLineCreateNode =
|
||||
varVal,
|
||||
])
|
||||
|
||||
return (args, referencedSegment) => {
|
||||
return (args, _, referencedSegment) => {
|
||||
const makeBinExp = (index: 0 | 1) => {
|
||||
const arg = getArgLiteralVal(args?.[index])
|
||||
return createBinaryExpressionWithUnary([
|
||||
@ -449,7 +486,7 @@ const setAngledIntersectForAngledLines: TransformInfo['createNode'] =
|
||||
const setAngleBetweenCreateNode =
|
||||
(tranformToType: 'none' | 'xAbs' | 'yAbs'): TransformInfo['createNode'] =>
|
||||
({ referenceSegName, tag, forceValueUsedInTransform, varValA, varValB }) => {
|
||||
return (args, referencedSegment) => {
|
||||
return (args, _, referencedSegment) => {
|
||||
const refAngle = referencedSegment
|
||||
? getAngle(referencedSegment?.from, referencedSegment?.to)
|
||||
: 0
|
||||
@ -1168,6 +1205,92 @@ export function getRemoveConstraintsTransform(
|
||||
return false
|
||||
}
|
||||
|
||||
export function removeSingleConstraint({
|
||||
pathToCallExp,
|
||||
arrayIndex,
|
||||
objectProperty,
|
||||
ast,
|
||||
}: {
|
||||
pathToCallExp: PathToNode
|
||||
arrayIndex?: number
|
||||
objectProperty?: string
|
||||
ast: Program
|
||||
}): TransformInfo | false {
|
||||
const callExp = getNodeFromPath<CallExpression>(
|
||||
ast,
|
||||
pathToCallExp,
|
||||
'CallExpression'
|
||||
).node
|
||||
if (callExp.type !== 'CallExpression') throw new Error('Invalid node type')
|
||||
|
||||
const transform: TransformInfo = {
|
||||
tooltip: callExp.callee.name as any,
|
||||
createNode:
|
||||
({ tag, referenceSegName, varValues }) =>
|
||||
(_, rawValues) => {
|
||||
if (objectProperty) {
|
||||
const expression: Parameters<typeof createObjectExpression>[0] = {}
|
||||
varValues.forEach((varValue) => {
|
||||
if (
|
||||
varValue.type !== 'objectProperty' &&
|
||||
varValue.type !== 'arrayOrObjItem'
|
||||
)
|
||||
return
|
||||
const literal = rawValues.find(
|
||||
(rawValue) =>
|
||||
(rawValue.type === 'objectProperty' ||
|
||||
rawValue.type === 'arrayOrObjItem') &&
|
||||
rawValue.key === objectProperty
|
||||
)?.value
|
||||
const value =
|
||||
(varValue.key === objectProperty && literal) || varValue.value
|
||||
expression[varValue.key] = value
|
||||
})
|
||||
const objExp = createObjectExpression(expression)
|
||||
return createStdlibCallExpression(
|
||||
callExp.callee.name as any,
|
||||
objExp,
|
||||
tag
|
||||
)
|
||||
}
|
||||
if (typeof arrayIndex === 'number') {
|
||||
const values = varValues.map((varValue) => {
|
||||
if (
|
||||
(varValue.type === 'arrayItem' ||
|
||||
varValue.type === 'arrayOrObjItem') &&
|
||||
varValue.index === arrayIndex
|
||||
) {
|
||||
const literal = rawValues.find(
|
||||
(rawValue) =>
|
||||
(rawValue.type === 'arrayItem' ||
|
||||
rawValue.type === 'arrayOrObjItem') &&
|
||||
rawValue.index === arrayIndex
|
||||
)?.value
|
||||
return (
|
||||
(varValue.index === arrayIndex && literal) || varValue.value
|
||||
)
|
||||
}
|
||||
return varValue.value
|
||||
})
|
||||
return createStdlibCallExpression(
|
||||
callExp.callee.name as any,
|
||||
createArrayExpression(values),
|
||||
tag
|
||||
)
|
||||
}
|
||||
|
||||
// if (typeof arrayIndex !== 'number' || !objectProperty) must be single value input xLine, yLineTo etc
|
||||
|
||||
return createCallWrapper(
|
||||
callExp.callee.name as any,
|
||||
rawValues[0].value,
|
||||
tag
|
||||
)
|
||||
},
|
||||
}
|
||||
return transform
|
||||
}
|
||||
|
||||
function getTransformMapPath(
|
||||
sketchFnExp: CallExpression,
|
||||
constraintType: ConstraintType
|
||||
@ -1392,7 +1515,7 @@ export function transformAstSketchLines({
|
||||
referencedSegmentRange,
|
||||
}: {
|
||||
ast: Program
|
||||
selectionRanges: Selections
|
||||
selectionRanges: Selections | PathToNode[]
|
||||
transformInfos: TransformInfo[]
|
||||
programMemory: ProgramMemory
|
||||
referenceSegName: string
|
||||
@ -1408,15 +1531,13 @@ export function transformAstSketchLines({
|
||||
let _valueUsedInTransform // TODO should this be an array?
|
||||
const pathToNodeMap: PathToNodeMap = {}
|
||||
|
||||
selectionRanges.codeBasedSelections.forEach(({ range }, index) => {
|
||||
const processSelection = (_pathToNode: PathToNode, index: number) => {
|
||||
const callBack = transformInfos?.[index].createNode
|
||||
const transformTo = transformInfos?.[index].tooltip
|
||||
|
||||
if (!callBack || !transformTo) throw new Error('no callback helper')
|
||||
|
||||
const getNode = getNodeFromPathCurry(
|
||||
node,
|
||||
getNodePathFromSourceRange(node, range)
|
||||
)
|
||||
const getNode = getNodeFromPathCurry(node, _pathToNode)
|
||||
|
||||
const callExp = getNode<CallExpression>('CallExpression')?.node
|
||||
const varDec = getNode<VariableDeclarator>('VariableDeclarator').node
|
||||
@ -1436,11 +1557,47 @@ export function transformAstSketchLines({
|
||||
''
|
||||
const [varValA, varValB] = Array.isArray(val) ? val : [val, val]
|
||||
|
||||
const varValues: VarValues = []
|
||||
|
||||
getConstraintInfo(callExp, '', _pathToNode).forEach((a) => {
|
||||
if (
|
||||
a.type === 'tangentialWithPrevious' ||
|
||||
a.type === 'horizontal' ||
|
||||
a.type === 'vertical'
|
||||
)
|
||||
return
|
||||
if (a?.argPosition?.type === 'arrayItem') {
|
||||
varValues.push({
|
||||
type: 'arrayItem',
|
||||
index: a.argPosition.index,
|
||||
value: getNodeFromPath<Value>(ast, a.pathToNode).node,
|
||||
argType: a.type,
|
||||
})
|
||||
} else if (a?.argPosition?.type === 'objectProperty') {
|
||||
varValues.push({
|
||||
type: 'objectProperty',
|
||||
key: a.argPosition.key,
|
||||
value: getNodeFromPath<Value>(ast, a.pathToNode).node,
|
||||
argType: a.type,
|
||||
})
|
||||
} else if (a?.argPosition?.type === 'singleValue') {
|
||||
varValues.push({
|
||||
type: 'singleValue',
|
||||
argType: a.type,
|
||||
value: getNodeFromPath<Value>(ast, a.pathToNode).node,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const varName = varDec.id.name
|
||||
const sketchGroup = programMemory.root?.[varName]
|
||||
if (!sketchGroup || sketchGroup.type !== 'SketchGroup')
|
||||
throw new Error('not a sketch group')
|
||||
const seg = getSketchSegmentFromSourceRange(sketchGroup, range).segment
|
||||
const seg = getSketchSegmentFromPathToNode(
|
||||
sketchGroup,
|
||||
ast,
|
||||
_pathToNode
|
||||
).segment
|
||||
const referencedSegment = referencedSegmentRange
|
||||
? getSketchSegmentFromSourceRange(sketchGroup, referencedSegmentRange)
|
||||
.segment
|
||||
@ -1450,13 +1607,14 @@ export function transformAstSketchLines({
|
||||
{
|
||||
node: node,
|
||||
programMemory,
|
||||
sourceRange: range,
|
||||
pathToNode: _pathToNode,
|
||||
referencedSegment,
|
||||
fnName: transformTo || (callExp.callee.name as ToolTip),
|
||||
to,
|
||||
from,
|
||||
createCallback: callBack({
|
||||
referenceSegName: _referencedSegmentName,
|
||||
varValues,
|
||||
varValA,
|
||||
varValB,
|
||||
tag: callBackTag,
|
||||
@ -1470,7 +1628,16 @@ export function transformAstSketchLines({
|
||||
if (typeof valueUsedInTransform === 'number') {
|
||||
_valueUsedInTransform = valueUsedInTransform
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if ('codeBasedSelections' in selectionRanges) {
|
||||
selectionRanges.codeBasedSelections.forEach(({ range }, index) =>
|
||||
processSelection(getNodePathFromSourceRange(node, range), index)
|
||||
)
|
||||
} else {
|
||||
selectionRanges.forEach(processSelection)
|
||||
}
|
||||
|
||||
return {
|
||||
modifiedAst: node,
|
||||
valueUsedInTransform: _valueUsedInTransform,
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { ToolTip } from 'useStore'
|
||||
import {
|
||||
ProgramMemory,
|
||||
Path,
|
||||
@ -5,9 +6,11 @@ import {
|
||||
Program,
|
||||
Value,
|
||||
PathToNode,
|
||||
CallExpression,
|
||||
Literal,
|
||||
} from '../wasm'
|
||||
import { ToolTip } from '../../useStore'
|
||||
import { EngineCommandManager } from './engineConnection'
|
||||
import { LineInputsType } from './sketchcombos'
|
||||
|
||||
export interface InternalFirstArg {
|
||||
programMemory: ProgramMemory
|
||||
@ -44,16 +47,70 @@ interface updateArgs extends ModifyAstBase {
|
||||
to: [number, number]
|
||||
}
|
||||
|
||||
export type VarValueKeys = 'angle' | 'offset' | 'length' | 'to' | 'intersectTag'
|
||||
export interface SingleValueInput<T> {
|
||||
type: 'singleValue'
|
||||
argType: LineInputsType
|
||||
value: T
|
||||
}
|
||||
export interface ArrayItemInput<T> {
|
||||
type: 'arrayItem'
|
||||
index: 0 | 1
|
||||
argType: LineInputsType
|
||||
value: T
|
||||
}
|
||||
export interface ObjectPropertyInput<T> {
|
||||
type: 'objectProperty'
|
||||
key: VarValueKeys
|
||||
argType: LineInputsType
|
||||
value: T
|
||||
}
|
||||
|
||||
export interface ArrayOrObjItemInput<T> {
|
||||
type: 'arrayOrObjItem'
|
||||
key: VarValueKeys
|
||||
index: 0 | 1
|
||||
argType: LineInputsType
|
||||
value: T
|
||||
}
|
||||
|
||||
export type _VarValue<T> =
|
||||
| SingleValueInput<T>
|
||||
| ArrayItemInput<T>
|
||||
| ObjectPropertyInput<T>
|
||||
| ArrayOrObjItemInput<T>
|
||||
|
||||
export type VarValue = _VarValue<Value>
|
||||
export type RawValue = _VarValue<Literal>
|
||||
|
||||
export type VarValues = Array<VarValue>
|
||||
export type RawValues = Array<RawValue>
|
||||
|
||||
type SimplifiedVarValue =
|
||||
| {
|
||||
type: 'singleValue'
|
||||
}
|
||||
| { type: 'arrayItem'; index: 0 | 1 }
|
||||
| { type: 'objectProperty'; key: VarValueKeys }
|
||||
|
||||
export type TransformCallback = (
|
||||
args: [Value, Value],
|
||||
literalValues: RawValues,
|
||||
referencedSegment?: Path
|
||||
) => {
|
||||
callExp: Value
|
||||
valueUsedInTransform?: number
|
||||
}
|
||||
|
||||
export type SketchCallTransfromMap = {
|
||||
[key in ToolTip]: TransformCallback
|
||||
export interface ConstrainInfo {
|
||||
stdLibFnName: ToolTip
|
||||
type: LineInputsType | 'vertical' | 'horizontal' | 'tangentialWithPrevious'
|
||||
isConstrained: boolean
|
||||
sourceRange: SourceRange
|
||||
pathToNode: PathToNode
|
||||
value: string
|
||||
calculatedValue?: any
|
||||
argPosition?: SimplifiedVarValue
|
||||
}
|
||||
|
||||
export interface SketchLineHelper {
|
||||
@ -70,4 +127,9 @@ export interface SketchLineHelper {
|
||||
modifiedAst: Program
|
||||
tag: string
|
||||
}
|
||||
getConstraintInfo: (
|
||||
callExp: CallExpression,
|
||||
code: string,
|
||||
pathToNode: PathToNode
|
||||
) => ConstrainInfo[]
|
||||
}
|
||||
|
@ -21,12 +21,28 @@ export function getLength(a: [number, number], b: [number, number]): number {
|
||||
return Math.sqrt(x * x + y * y)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the angle in degrees between two points in a 2D space.
|
||||
* The angle is normalized to the range [-180, 180].
|
||||
*
|
||||
* @param a The first point as a tuple [x, y].
|
||||
* @param b The second point as a tuple [x, y].
|
||||
* @returns The normalized angle in degrees between point a and point b.
|
||||
*/
|
||||
export function getAngle(a: [number, number], b: [number, number]): number {
|
||||
const x = b[0] - a[0]
|
||||
const y = b[1] - a[1]
|
||||
return normaliseAngle((Math.atan2(y, x) * 180) / Math.PI)
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes an angle to the range [-180, 180].
|
||||
*
|
||||
* This function takes an angle in degrees and normalizes it so that the result is always within the range of -180 to 180 degrees. This is useful for ensuring consistent angle measurements where the direction (positive or negative) is significant.
|
||||
*
|
||||
* @param angle The angle in degrees to be normalized.
|
||||
* @returns The normalized angle in the range [-180, 180].
|
||||
*/
|
||||
export function normaliseAngle(angle: number): number {
|
||||
const result = ((angle % 360) + 360) % 360
|
||||
return result > 180 ? result - 360 : result
|
||||
|
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user