Compare commits
2 Commits
alteous/up
...
main
Author | SHA1 | Date | |
---|---|---|---|
5f7a75a327 | |||
01230b0aa1 |
@ -187,6 +187,68 @@ sketch001 = startProfile(sketch002, at = [12.34, -12.34])
|
|||||||
page.getByRole('button', { name: 'Start Sketch' })
|
page.getByRole('button', { name: 'Start Sketch' })
|
||||||
).toBeVisible()
|
).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('Can select planes in Feature Tree after Start Sketch', async ({
|
||||||
|
page,
|
||||||
|
homePage,
|
||||||
|
toolbar,
|
||||||
|
editor,
|
||||||
|
}) => {
|
||||||
|
// Load the app with empty code
|
||||||
|
await page.addInitScript(async () => {
|
||||||
|
localStorage.setItem(
|
||||||
|
'persistCode',
|
||||||
|
`plane001 = offsetPlane(XZ, offset = 5)`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
await page.setBodyDimensions({ width: 1200, height: 500 })
|
||||||
|
|
||||||
|
await homePage.goToModelingScene()
|
||||||
|
|
||||||
|
await test.step('Click Start Sketch button', async () => {
|
||||||
|
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', { name: 'Exit Sketch' })
|
||||||
|
).toBeVisible()
|
||||||
|
await expect(page.getByText('select a plane or face')).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step('Open feature tree and select Front plane (XZ)', async () => {
|
||||||
|
await toolbar.openFeatureTreePane()
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Front plane' }).click()
|
||||||
|
|
||||||
|
await page.waitForTimeout(600)
|
||||||
|
|
||||||
|
await expect(toolbar.lineBtn).toBeEnabled()
|
||||||
|
await editor.expectEditor.toContain('startSketchOn(XZ)')
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', { name: 'Start Sketch' })
|
||||||
|
).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step('Click Start Sketch button again', async () => {
|
||||||
|
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', { name: 'Exit Sketch' })
|
||||||
|
).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step('Select the offset plane', async () => {
|
||||||
|
await toolbar.openFeatureTreePane()
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Offset plane' }).click()
|
||||||
|
|
||||||
|
await page.waitForTimeout(600)
|
||||||
|
|
||||||
|
await expect(toolbar.lineBtn).toBeEnabled()
|
||||||
|
await editor.expectEditor.toContain('startSketchOn(plane001)')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
test('Can edit segments by dragging their handles', () => {
|
test('Can edit segments by dragging their handles', () => {
|
||||||
const doEditSegmentsByDraggingHandle = async (
|
const doEditSegmentsByDraggingHandle = async (
|
||||||
page: Page,
|
page: Page,
|
||||||
|
@ -24,7 +24,12 @@ import {
|
|||||||
getOperationVariableName,
|
getOperationVariableName,
|
||||||
stdLibMap,
|
stdLibMap,
|
||||||
} from '@src/lib/operations'
|
} from '@src/lib/operations'
|
||||||
import { editorManager, kclManager, rustContext } from '@src/lib/singletons'
|
import {
|
||||||
|
editorManager,
|
||||||
|
kclManager,
|
||||||
|
rustContext,
|
||||||
|
sceneInfra,
|
||||||
|
} from '@src/lib/singletons'
|
||||||
import {
|
import {
|
||||||
featureTreeMachine,
|
featureTreeMachine,
|
||||||
featureTreeMachineDefaultContext,
|
featureTreeMachineDefaultContext,
|
||||||
@ -34,11 +39,20 @@ import {
|
|||||||
kclEditorActor,
|
kclEditorActor,
|
||||||
selectionEventSelector,
|
selectionEventSelector,
|
||||||
} from '@src/machines/kclEditorMachine'
|
} from '@src/machines/kclEditorMachine'
|
||||||
|
import type { Plane } from '@rust/kcl-lib/bindings/Artifact'
|
||||||
|
import {
|
||||||
|
selectDefaultSketchPlane,
|
||||||
|
selectOffsetSketchPlane,
|
||||||
|
} from '@src/lib/selections'
|
||||||
|
import type { DefaultPlaneStr } from '@src/lib/planes'
|
||||||
|
|
||||||
export const FeatureTreePane = () => {
|
export const FeatureTreePane = () => {
|
||||||
const isEditorMounted = useSelector(kclEditorActor, editorIsMountedSelector)
|
const isEditorMounted = useSelector(kclEditorActor, editorIsMountedSelector)
|
||||||
const lastSelectionEvent = useSelector(kclEditorActor, selectionEventSelector)
|
const lastSelectionEvent = useSelector(kclEditorActor, selectionEventSelector)
|
||||||
const { send: modelingSend, state: modelingState } = useModelingContext()
|
const { send: modelingSend, state: modelingState } = useModelingContext()
|
||||||
|
|
||||||
|
const sketchNoFace = modelingState.matches('Sketch no face')
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const [_featureTreeState, featureTreeSend] = useMachine(
|
const [_featureTreeState, featureTreeSend] = useMachine(
|
||||||
featureTreeMachine.provide({
|
featureTreeMachine.provide({
|
||||||
@ -195,6 +209,7 @@ export const FeatureTreePane = () => {
|
|||||||
key={key}
|
key={key}
|
||||||
item={operation}
|
item={operation}
|
||||||
send={featureTreeSend}
|
send={featureTreeSend}
|
||||||
|
sketchNoFace={sketchNoFace}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
@ -251,6 +266,7 @@ const OperationItemWrapper = ({
|
|||||||
customSuffix,
|
customSuffix,
|
||||||
className,
|
className,
|
||||||
selectable = true,
|
selectable = true,
|
||||||
|
greyedOut = false,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLButtonElement> & {
|
}: React.HTMLAttributes<HTMLButtonElement> & {
|
||||||
icon: CustomIconName
|
icon: CustomIconName
|
||||||
@ -262,18 +278,19 @@ const OperationItemWrapper = ({
|
|||||||
menuItems?: ComponentProps<typeof ContextMenu>['items']
|
menuItems?: ComponentProps<typeof ContextMenu>['items']
|
||||||
errors?: Diagnostic[]
|
errors?: Diagnostic[]
|
||||||
selectable?: boolean
|
selectable?: boolean
|
||||||
|
greyedOut?: boolean
|
||||||
}) => {
|
}) => {
|
||||||
const menuRef = useRef<HTMLDivElement>(null)
|
const menuRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={menuRef}
|
ref={menuRef}
|
||||||
className={`flex select-none items-center group/item my-0 py-0.5 px-1 ${selectable ? 'focus-within:bg-primary/10 hover:bg-primary/5' : ''}`}
|
className={`flex select-none items-center group/item my-0 py-0.5 px-1 ${selectable ? 'focus-within:bg-primary/10 hover:bg-primary/5' : ''} ${greyedOut ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||||
data-testid="feature-tree-operation-item"
|
data-testid="feature-tree-operation-item"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
{...props}
|
{...props}
|
||||||
className={`reset !py-0.5 !px-1 flex-1 flex items-center gap-2 text-left text-base ${selectable ? 'border-transparent dark:border-transparent' : 'border-none cursor-default'} ${className}`}
|
className={`reset !py-0.5 !px-1 flex-1 flex items-center gap-2 text-left text-base ${selectable ? 'border-transparent dark:border-transparent' : '!border-transparent cursor-default'} ${className}`}
|
||||||
>
|
>
|
||||||
<CustomIcon name={icon} className="w-5 h-5 block" />
|
<CustomIcon name={icon} className="w-5 h-5 block" />
|
||||||
<div className="flex flex-1 items-baseline align-baseline">
|
<div className="flex flex-1 items-baseline align-baseline">
|
||||||
@ -311,6 +328,7 @@ const OperationItemWrapper = ({
|
|||||||
const OperationItem = (props: {
|
const OperationItem = (props: {
|
||||||
item: Operation
|
item: Operation
|
||||||
send: Prop<Actor<typeof featureTreeMachine>, 'send'>
|
send: Prop<Actor<typeof featureTreeMachine>, 'send'>
|
||||||
|
sketchNoFace: boolean
|
||||||
}) => {
|
}) => {
|
||||||
const kclContext = useKclContext()
|
const kclContext = useKclContext()
|
||||||
const name = getOperationLabel(props.item)
|
const name = getOperationLabel(props.item)
|
||||||
@ -343,6 +361,12 @@ const OperationItem = (props: {
|
|||||||
}, [kclContext.diagnostics.length])
|
}, [kclContext.diagnostics.length])
|
||||||
|
|
||||||
function selectOperation() {
|
function selectOperation() {
|
||||||
|
if (props.sketchNoFace) {
|
||||||
|
if (isOffsetPlane(props.item)) {
|
||||||
|
const artifact = findOperationArtifact(props.item)
|
||||||
|
void selectOffsetSketchPlane(artifact)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if (props.item.type === 'GroupEnd') {
|
if (props.item.type === 'GroupEnd') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -353,6 +377,7 @@ const OperationItem = (props: {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For now we can only enter the "edit" flow for the startSketchOn operation.
|
* For now we can only enter the "edit" flow for the startSketchOn operation.
|
||||||
@ -432,6 +457,20 @@ const OperationItem = (props: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function startSketchOnOffsetPlane() {
|
||||||
|
if (isOffsetPlane(props.item)) {
|
||||||
|
const artifact = findOperationArtifact(props.item)
|
||||||
|
if (artifact?.id) {
|
||||||
|
sceneInfra.modelingSend({
|
||||||
|
type: 'Enter sketch',
|
||||||
|
data: { forceNewSketch: true },
|
||||||
|
})
|
||||||
|
|
||||||
|
void selectOffsetSketchPlane(artifact)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const menuItems = useMemo(
|
const menuItems = useMemo(
|
||||||
() => [
|
() => [
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
@ -477,6 +516,13 @@ const OperationItem = (props: {
|
|||||||
</ContextMenuItem>,
|
</ContextMenuItem>,
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
|
...(isOffsetPlane(props.item)
|
||||||
|
? [
|
||||||
|
<ContextMenuItem onClick={startSketchOnOffsetPlane}>
|
||||||
|
Start Sketch
|
||||||
|
</ContextMenuItem>,
|
||||||
|
]
|
||||||
|
: []),
|
||||||
...(props.item.type === 'StdLibCall' ||
|
...(props.item.type === 'StdLibCall' ||
|
||||||
props.item.type === 'VariableDeclaration'
|
props.item.type === 'VariableDeclaration'
|
||||||
? [
|
? [
|
||||||
@ -550,22 +596,63 @@ const OperationItem = (props: {
|
|||||||
[props.item, props.send]
|
[props.item, props.send]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const enabled = !props.sketchNoFace || isOffsetPlane(props.item)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OperationItemWrapper
|
<OperationItemWrapper
|
||||||
|
selectable={enabled}
|
||||||
icon={getOperationIcon(props.item)}
|
icon={getOperationIcon(props.item)}
|
||||||
name={name}
|
name={name}
|
||||||
variableName={variableName}
|
variableName={variableName}
|
||||||
valueDetail={valueDetail}
|
valueDetail={valueDetail}
|
||||||
menuItems={menuItems}
|
menuItems={menuItems}
|
||||||
onClick={selectOperation}
|
onClick={selectOperation}
|
||||||
onDoubleClick={enterEditFlow}
|
onDoubleClick={props.sketchNoFace ? undefined : enterEditFlow} // no double click in "Sketch no face" mode
|
||||||
errors={errors}
|
errors={errors}
|
||||||
|
greyedOut={!enabled}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const DefaultPlanes = () => {
|
const DefaultPlanes = () => {
|
||||||
const { state: modelingState, send } = useModelingContext()
|
const { state: modelingState, send } = useModelingContext()
|
||||||
|
const sketchNoFace = modelingState.matches('Sketch no face')
|
||||||
|
|
||||||
|
const onClickPlane = useCallback(
|
||||||
|
(planeId: string) => {
|
||||||
|
if (sketchNoFace) {
|
||||||
|
selectDefaultSketchPlane(planeId)
|
||||||
|
} else {
|
||||||
|
const foundDefaultPlane =
|
||||||
|
rustContext.defaultPlanes !== null &&
|
||||||
|
Object.entries(rustContext.defaultPlanes).find(
|
||||||
|
([, plane]) => plane === planeId
|
||||||
|
)
|
||||||
|
if (foundDefaultPlane) {
|
||||||
|
send({
|
||||||
|
type: 'Set selection',
|
||||||
|
data: {
|
||||||
|
selectionType: 'defaultPlaneSelection',
|
||||||
|
selection: {
|
||||||
|
name: foundDefaultPlane[0] as DefaultPlaneStr,
|
||||||
|
id: planeId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[sketchNoFace]
|
||||||
|
)
|
||||||
|
|
||||||
|
const startSketchOnDefaultPlane = useCallback((planeId: string) => {
|
||||||
|
sceneInfra.modelingSend({
|
||||||
|
type: 'Enter sketch',
|
||||||
|
data: { forceNewSketch: true },
|
||||||
|
})
|
||||||
|
|
||||||
|
selectDefaultSketchPlane(planeId)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const defaultPlanes = rustContext.defaultPlanes
|
const defaultPlanes = rustContext.defaultPlanes
|
||||||
if (!defaultPlanes) return null
|
if (!defaultPlanes) return null
|
||||||
@ -603,7 +690,15 @@ const DefaultPlanes = () => {
|
|||||||
customSuffix={plane.customSuffix}
|
customSuffix={plane.customSuffix}
|
||||||
icon={'plane'}
|
icon={'plane'}
|
||||||
name={plane.name}
|
name={plane.name}
|
||||||
selectable={false}
|
selectable={true}
|
||||||
|
onClick={() => onClickPlane(plane.id)}
|
||||||
|
menuItems={[
|
||||||
|
<ContextMenuItem
|
||||||
|
onClick={() => startSketchOnDefaultPlane(plane.id)}
|
||||||
|
>
|
||||||
|
Start Sketch
|
||||||
|
</ContextMenuItem>,
|
||||||
|
]}
|
||||||
visibilityToggle={{
|
visibilityToggle={{
|
||||||
visible: modelingState.context.defaultPlaneVisibility[plane.key],
|
visible: modelingState.context.defaultPlaneVisibility[plane.key],
|
||||||
onVisibilityChange: () => {
|
onVisibilityChange: () => {
|
||||||
@ -620,3 +715,17 @@ const DefaultPlanes = () => {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type StdLibCallOp = Extract<Operation, { type: 'StdLibCall' }>
|
||||||
|
|
||||||
|
const isOffsetPlane = (item: Operation): item is StdLibCallOp => {
|
||||||
|
return item.type === 'StdLibCall' && item.name === 'offsetPlane'
|
||||||
|
}
|
||||||
|
|
||||||
|
const findOperationArtifact = (item: StdLibCallOp) => {
|
||||||
|
const nodePath = JSON.stringify(item.nodePath)
|
||||||
|
const artifact = [...kclManager.artifactGraph.values()].find(
|
||||||
|
(a) => JSON.stringify((a as Plane).codeRef?.nodePath) === nodePath
|
||||||
|
)
|
||||||
|
return artifact
|
||||||
|
}
|
||||||
|
@ -10,13 +10,22 @@ import {
|
|||||||
import { useModelingContext } from '@src/hooks/useModelingContext'
|
import { useModelingContext } from '@src/hooks/useModelingContext'
|
||||||
import type { AxisNames } from '@src/lib/constants'
|
import type { AxisNames } from '@src/lib/constants'
|
||||||
import { VIEW_NAMES_SEMANTIC } from '@src/lib/constants'
|
import { VIEW_NAMES_SEMANTIC } from '@src/lib/constants'
|
||||||
import { sceneInfra } from '@src/lib/singletons'
|
import { kclManager, sceneInfra } from '@src/lib/singletons'
|
||||||
import { reportRejection } from '@src/lib/trap'
|
import { err, reportRejection } from '@src/lib/trap'
|
||||||
import { useSettings } from '@src/lib/singletons'
|
import { useSettings } from '@src/lib/singletons'
|
||||||
import { resetCameraPosition } from '@src/lib/resetCameraPosition'
|
import { resetCameraPosition } from '@src/lib/resetCameraPosition'
|
||||||
|
import type { Selections } from '@src/lib/selections'
|
||||||
|
import {
|
||||||
|
selectDefaultSketchPlane,
|
||||||
|
selectOffsetSketchPlane,
|
||||||
|
} from '@src/lib/selections'
|
||||||
|
|
||||||
export function useViewControlMenuItems() {
|
export function useViewControlMenuItems() {
|
||||||
const { state: modelingState, send: modelingSend } = useModelingContext()
|
const { state: modelingState, send: modelingSend } = useModelingContext()
|
||||||
|
const selectedPlaneId = getCurrentPlaneId(
|
||||||
|
modelingState.context.selectionRanges
|
||||||
|
)
|
||||||
|
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
const shouldLockView =
|
const shouldLockView =
|
||||||
modelingState.matches('Sketch') &&
|
modelingState.matches('Sketch') &&
|
||||||
@ -56,9 +65,35 @@ export function useViewControlMenuItems() {
|
|||||||
Center view on selection
|
Center view on selection
|
||||||
</ContextMenuItem>,
|
</ContextMenuItem>,
|
||||||
<ContextMenuDivider />,
|
<ContextMenuDivider />,
|
||||||
|
<ContextMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
if (selectedPlaneId) {
|
||||||
|
sceneInfra.modelingSend({
|
||||||
|
type: 'Enter sketch',
|
||||||
|
data: { forceNewSketch: true },
|
||||||
|
})
|
||||||
|
|
||||||
|
const defaultSketchPlaneSelected =
|
||||||
|
selectDefaultSketchPlane(selectedPlaneId)
|
||||||
|
if (
|
||||||
|
!err(defaultSketchPlaneSelected) &&
|
||||||
|
defaultSketchPlaneSelected
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const artifact = kclManager.artifactGraph.get(selectedPlaneId)
|
||||||
|
void selectOffsetSketchPlane(artifact)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={!selectedPlaneId}
|
||||||
|
>
|
||||||
|
Start sketch on selection
|
||||||
|
</ContextMenuItem>,
|
||||||
|
<ContextMenuDivider />,
|
||||||
<ContextMenuItemRefresh />,
|
<ContextMenuItemRefresh />,
|
||||||
],
|
],
|
||||||
[VIEW_NAMES_SEMANTIC, shouldLockView]
|
[VIEW_NAMES_SEMANTIC, shouldLockView, selectedPlaneId]
|
||||||
)
|
)
|
||||||
return menuItems
|
return menuItems
|
||||||
}
|
}
|
||||||
@ -77,3 +112,21 @@ export function ViewControlContextMenu({
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCurrentPlaneId(selectionRanges: Selections): string | null {
|
||||||
|
const defaultPlane = selectionRanges.otherSelections.find(
|
||||||
|
(selection) => typeof selection === 'object' && 'name' in selection
|
||||||
|
)
|
||||||
|
if (defaultPlane) {
|
||||||
|
return defaultPlane.id
|
||||||
|
}
|
||||||
|
|
||||||
|
const planeSelection = selectionRanges.graphSelections.find(
|
||||||
|
(selection) => selection.artifact?.type === 'plane'
|
||||||
|
)
|
||||||
|
if (planeSelection) {
|
||||||
|
return planeSelection.artifact?.id || null
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
@ -14,13 +14,15 @@ import {
|
|||||||
import { isTopLevelModule } from '@src/lang/util'
|
import { isTopLevelModule } from '@src/lang/util'
|
||||||
import type { CallExpressionKw, PathToNode } from '@src/lang/wasm'
|
import type { CallExpressionKw, PathToNode } from '@src/lang/wasm'
|
||||||
import { defaultSourceRange } from '@src/lang/sourceRange'
|
import { defaultSourceRange } from '@src/lang/sourceRange'
|
||||||
import type { DefaultPlaneStr } from '@src/lib/planes'
|
import {
|
||||||
import { getEventForSelectWithPoint } from '@src/lib/selections'
|
getEventForSelectWithPoint,
|
||||||
|
selectDefaultSketchPlane,
|
||||||
|
selectOffsetSketchPlane,
|
||||||
|
} from '@src/lib/selections'
|
||||||
import {
|
import {
|
||||||
editorManager,
|
editorManager,
|
||||||
engineCommandManager,
|
engineCommandManager,
|
||||||
kclManager,
|
kclManager,
|
||||||
rustContext,
|
|
||||||
sceneEntitiesManager,
|
sceneEntitiesManager,
|
||||||
sceneInfra,
|
sceneInfra,
|
||||||
} from '@src/lib/singletons'
|
} from '@src/lib/singletons'
|
||||||
@ -96,131 +98,18 @@ export function useEngineConnectionSubscriptions() {
|
|||||||
;(async () => {
|
;(async () => {
|
||||||
let planeOrFaceId = data.entity_id
|
let planeOrFaceId = data.entity_id
|
||||||
if (!planeOrFaceId) return
|
if (!planeOrFaceId) return
|
||||||
|
|
||||||
|
const defaultSketchPlaneSelected =
|
||||||
|
selectDefaultSketchPlane(planeOrFaceId)
|
||||||
if (
|
if (
|
||||||
rustContext.defaultPlanes?.xy === planeOrFaceId ||
|
!err(defaultSketchPlaneSelected) &&
|
||||||
rustContext.defaultPlanes?.xz === planeOrFaceId ||
|
defaultSketchPlaneSelected
|
||||||
rustContext.defaultPlanes?.yz === planeOrFaceId ||
|
|
||||||
rustContext.defaultPlanes?.negXy === planeOrFaceId ||
|
|
||||||
rustContext.defaultPlanes?.negXz === planeOrFaceId ||
|
|
||||||
rustContext.defaultPlanes?.negYz === planeOrFaceId
|
|
||||||
) {
|
) {
|
||||||
let planeId = planeOrFaceId
|
|
||||||
const defaultPlaneStrMap: Record<string, DefaultPlaneStr> = {
|
|
||||||
[rustContext.defaultPlanes.xy]: 'XY',
|
|
||||||
[rustContext.defaultPlanes.xz]: 'XZ',
|
|
||||||
[rustContext.defaultPlanes.yz]: 'YZ',
|
|
||||||
[rustContext.defaultPlanes.negXy]: '-XY',
|
|
||||||
[rustContext.defaultPlanes.negXz]: '-XZ',
|
|
||||||
[rustContext.defaultPlanes.negYz]: '-YZ',
|
|
||||||
}
|
|
||||||
// TODO can we get this information from rust land when it creates the default planes?
|
|
||||||
// maybe returned from make_default_planes (src/wasm-lib/src/wasm.rs)
|
|
||||||
let zAxis: [number, number, number] = [0, 0, 1]
|
|
||||||
let yAxis: [number, number, number] = [0, 1, 0]
|
|
||||||
|
|
||||||
// get unit vector from camera position to target
|
|
||||||
const camVector = sceneInfra.camControls.camera.position
|
|
||||||
.clone()
|
|
||||||
.sub(sceneInfra.camControls.target)
|
|
||||||
|
|
||||||
if (rustContext.defaultPlanes?.xy === planeId) {
|
|
||||||
zAxis = [0, 0, 1]
|
|
||||||
yAxis = [0, 1, 0]
|
|
||||||
if (camVector.z < 0) {
|
|
||||||
zAxis = [0, 0, -1]
|
|
||||||
planeId = rustContext.defaultPlanes?.negXy || ''
|
|
||||||
}
|
|
||||||
} else if (rustContext.defaultPlanes?.yz === planeId) {
|
|
||||||
zAxis = [1, 0, 0]
|
|
||||||
yAxis = [0, 0, 1]
|
|
||||||
if (camVector.x < 0) {
|
|
||||||
zAxis = [-1, 0, 0]
|
|
||||||
planeId = rustContext.defaultPlanes?.negYz || ''
|
|
||||||
}
|
|
||||||
} else if (rustContext.defaultPlanes?.xz === planeId) {
|
|
||||||
zAxis = [0, 1, 0]
|
|
||||||
yAxis = [0, 0, 1]
|
|
||||||
planeId = rustContext.defaultPlanes?.negXz || ''
|
|
||||||
if (camVector.y < 0) {
|
|
||||||
zAxis = [0, -1, 0]
|
|
||||||
planeId = rustContext.defaultPlanes?.xz || ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sceneInfra.modelingSend({
|
|
||||||
type: 'Select sketch plane',
|
|
||||||
data: {
|
|
||||||
type: 'defaultPlane',
|
|
||||||
planeId: planeId,
|
|
||||||
plane: defaultPlaneStrMap[planeId],
|
|
||||||
zAxis,
|
|
||||||
yAxis,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const artifact = kclManager.artifactGraph.get(planeOrFaceId)
|
const artifact = kclManager.artifactGraph.get(planeOrFaceId)
|
||||||
|
if (await selectOffsetSketchPlane(artifact)) {
|
||||||
if (artifact?.type === 'plane') {
|
|
||||||
const planeInfo =
|
|
||||||
await sceneEntitiesManager.getFaceDetails(planeOrFaceId)
|
|
||||||
|
|
||||||
// Apply camera-based orientation logic similar to default planes
|
|
||||||
let zAxis: [number, number, number] = [
|
|
||||||
planeInfo.z_axis.x,
|
|
||||||
planeInfo.z_axis.y,
|
|
||||||
planeInfo.z_axis.z,
|
|
||||||
]
|
|
||||||
let yAxis: [number, number, number] = [
|
|
||||||
planeInfo.y_axis.x,
|
|
||||||
planeInfo.y_axis.y,
|
|
||||||
planeInfo.y_axis.z,
|
|
||||||
]
|
|
||||||
|
|
||||||
// Get camera vector to determine which side of the plane we're viewing from
|
|
||||||
const camVector = sceneInfra.camControls.camera.position
|
|
||||||
.clone()
|
|
||||||
.sub(sceneInfra.camControls.target)
|
|
||||||
|
|
||||||
// Determine the canonical (absolute) plane orientation
|
|
||||||
const absZAxis: [number, number, number] = [
|
|
||||||
Math.abs(zAxis[0]),
|
|
||||||
Math.abs(zAxis[1]),
|
|
||||||
Math.abs(zAxis[2]),
|
|
||||||
]
|
|
||||||
|
|
||||||
// Find the dominant axis (like default planes do)
|
|
||||||
const maxComponent = Math.max(...absZAxis)
|
|
||||||
const dominantAxisIndex = absZAxis.indexOf(maxComponent)
|
|
||||||
|
|
||||||
// Check camera position against canonical orientation (like default planes)
|
|
||||||
const cameraComponents = [camVector.x, camVector.y, camVector.z]
|
|
||||||
let negated = cameraComponents[dominantAxisIndex] < 0
|
|
||||||
if (dominantAxisIndex === 1) {
|
|
||||||
// offset of the XZ is being weird, not sure if this is a camera bug
|
|
||||||
negated = !negated
|
|
||||||
}
|
|
||||||
|
|
||||||
sceneInfra.modelingSend({
|
|
||||||
type: 'Select sketch plane',
|
|
||||||
data: {
|
|
||||||
type: 'offsetPlane',
|
|
||||||
zAxis,
|
|
||||||
yAxis,
|
|
||||||
position: [
|
|
||||||
planeInfo.origin.x,
|
|
||||||
planeInfo.origin.y,
|
|
||||||
planeInfo.origin.z,
|
|
||||||
].map((num) => num / sceneInfra._baseUnitMultiplier) as [
|
|
||||||
number,
|
|
||||||
number,
|
|
||||||
number,
|
|
||||||
],
|
|
||||||
planeId: planeOrFaceId,
|
|
||||||
pathToNode: artifact.codeRef.pathToNode,
|
|
||||||
negated,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -483,13 +483,13 @@ export function sketchOnExtrudedFace(
|
|||||||
*/
|
*/
|
||||||
export function addOffsetPlane({
|
export function addOffsetPlane({
|
||||||
node,
|
node,
|
||||||
defaultPlane,
|
plane,
|
||||||
insertIndex,
|
insertIndex,
|
||||||
offset,
|
offset,
|
||||||
planeName,
|
planeName,
|
||||||
}: {
|
}: {
|
||||||
node: Node<Program>
|
node: Node<Program>
|
||||||
defaultPlane: DefaultPlaneStr
|
plane: Node<Literal> | Node<Name> // Can be DefaultPlaneStr or string for offsetPlanes
|
||||||
insertIndex?: number
|
insertIndex?: number
|
||||||
offset: Expr
|
offset: Expr
|
||||||
planeName?: string
|
planeName?: string
|
||||||
@ -500,11 +500,9 @@ export function addOffsetPlane({
|
|||||||
|
|
||||||
const newPlane = createVariableDeclaration(
|
const newPlane = createVariableDeclaration(
|
||||||
newPlaneName,
|
newPlaneName,
|
||||||
createCallExpressionStdLibKw(
|
createCallExpressionStdLibKw('offsetPlane', plane, [
|
||||||
'offsetPlane',
|
createLabeledArg('offset', offset),
|
||||||
createLiteral(defaultPlane.toUpperCase()),
|
])
|
||||||
[createLabeledArg('offset', offset)]
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const insertAt =
|
const insertAt =
|
||||||
|
@ -336,14 +336,6 @@ export async function doPromptEdit({
|
|||||||
const toastId = toast.loading('Submitting to Text-to-CAD API...')
|
const toastId = toast.loading('Submitting to Text-to-CAD API...')
|
||||||
|
|
||||||
let submitResult
|
let submitResult
|
||||||
|
|
||||||
// work around for @kittycad/lib not really being built for the browser
|
|
||||||
;(window as any).process = {
|
|
||||||
env: {
|
|
||||||
ZOO_API_TOKEN: token,
|
|
||||||
ZOO_HOST: withAPIBaseURL(''),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
submitResult = await submitPromptToEditToQueue({
|
submitResult = await submitPromptToEditToQueue({
|
||||||
prompt,
|
prompt,
|
||||||
|
@ -34,6 +34,7 @@ import {
|
|||||||
kclManager,
|
kclManager,
|
||||||
rustContext,
|
rustContext,
|
||||||
sceneEntitiesManager,
|
sceneEntitiesManager,
|
||||||
|
sceneInfra,
|
||||||
} from '@src/lib/singletons'
|
} from '@src/lib/singletons'
|
||||||
import { err } from '@src/lib/trap'
|
import { err } from '@src/lib/trap'
|
||||||
import {
|
import {
|
||||||
@ -803,3 +804,156 @@ export function getSemanticSelectionType(selectionType: Artifact['type'][]) {
|
|||||||
|
|
||||||
return Array.from(semanticSelectionType)
|
return Array.from(semanticSelectionType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function selectDefaultSketchPlane(
|
||||||
|
defaultPlaneId: string
|
||||||
|
): Error | boolean {
|
||||||
|
const defaultPlanes = rustContext.defaultPlanes
|
||||||
|
if (!defaultPlanes) {
|
||||||
|
return new Error('No default planes defined in rustContext')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
![
|
||||||
|
defaultPlanes.xy,
|
||||||
|
defaultPlanes.xz,
|
||||||
|
defaultPlanes.yz,
|
||||||
|
defaultPlanes.negXy,
|
||||||
|
defaultPlanes.negXz,
|
||||||
|
defaultPlanes.negYz,
|
||||||
|
].includes(defaultPlaneId)
|
||||||
|
) {
|
||||||
|
// Supplied defaultPlaneId is not a valid default plane id
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const camVector = sceneInfra.camControls.camera.position
|
||||||
|
.clone()
|
||||||
|
.sub(sceneInfra.camControls.target)
|
||||||
|
|
||||||
|
// TODO can we get this information from rust land when it creates the default planes?
|
||||||
|
// maybe returned from make_default_planes (src/wasm-lib/src/wasm.rs)
|
||||||
|
let zAxis: [number, number, number] = [0, 0, 1]
|
||||||
|
let yAxis: [number, number, number] = [0, 1, 0]
|
||||||
|
|
||||||
|
if (defaultPlanes?.xy === defaultPlaneId) {
|
||||||
|
zAxis = [0, 0, 1]
|
||||||
|
yAxis = [0, 1, 0]
|
||||||
|
if (camVector.z < 0) {
|
||||||
|
zAxis = [0, 0, -1]
|
||||||
|
defaultPlaneId = defaultPlanes?.negXy || ''
|
||||||
|
}
|
||||||
|
} else if (defaultPlanes?.yz === defaultPlaneId) {
|
||||||
|
zAxis = [1, 0, 0]
|
||||||
|
yAxis = [0, 0, 1]
|
||||||
|
if (camVector.x < 0) {
|
||||||
|
zAxis = [-1, 0, 0]
|
||||||
|
defaultPlaneId = defaultPlanes?.negYz || ''
|
||||||
|
}
|
||||||
|
} else if (defaultPlanes?.xz === defaultPlaneId) {
|
||||||
|
zAxis = [0, 1, 0]
|
||||||
|
yAxis = [0, 0, 1]
|
||||||
|
defaultPlaneId = defaultPlanes?.negXz || ''
|
||||||
|
if (camVector.y < 0) {
|
||||||
|
zAxis = [0, -1, 0]
|
||||||
|
defaultPlaneId = defaultPlanes?.xz || ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultPlaneStrMap: Record<string, DefaultPlaneStr> = {
|
||||||
|
[defaultPlanes.xy]: 'XY',
|
||||||
|
[defaultPlanes.xz]: 'XZ',
|
||||||
|
[defaultPlanes.yz]: 'YZ',
|
||||||
|
[defaultPlanes.negXy]: '-XY',
|
||||||
|
[defaultPlanes.negXz]: '-XZ',
|
||||||
|
[defaultPlanes.negYz]: '-YZ',
|
||||||
|
}
|
||||||
|
|
||||||
|
sceneInfra.modelingSend({
|
||||||
|
type: 'Select sketch plane',
|
||||||
|
data: {
|
||||||
|
type: 'defaultPlane',
|
||||||
|
planeId: defaultPlaneId,
|
||||||
|
plane: defaultPlaneStrMap[defaultPlaneId],
|
||||||
|
zAxis,
|
||||||
|
yAxis,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function selectOffsetSketchPlane(artifact: Artifact | undefined) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (artifact?.type === 'plane') {
|
||||||
|
const planeId = artifact.id
|
||||||
|
void sceneEntitiesManager
|
||||||
|
.getFaceDetails(planeId)
|
||||||
|
.then((planeInfo) => {
|
||||||
|
// Apply camera-based orientation logic similar to default planes
|
||||||
|
let zAxis: [number, number, number] = [
|
||||||
|
planeInfo.z_axis.x,
|
||||||
|
planeInfo.z_axis.y,
|
||||||
|
planeInfo.z_axis.z,
|
||||||
|
]
|
||||||
|
let yAxis: [number, number, number] = [
|
||||||
|
planeInfo.y_axis.x,
|
||||||
|
planeInfo.y_axis.y,
|
||||||
|
planeInfo.y_axis.z,
|
||||||
|
]
|
||||||
|
|
||||||
|
// Get camera vector to determine which side of the plane we're viewing from
|
||||||
|
const camVector = sceneInfra.camControls.camera.position
|
||||||
|
.clone()
|
||||||
|
.sub(sceneInfra.camControls.target)
|
||||||
|
|
||||||
|
// Determine the canonical (absolute) plane orientation
|
||||||
|
const absZAxis: [number, number, number] = [
|
||||||
|
Math.abs(zAxis[0]),
|
||||||
|
Math.abs(zAxis[1]),
|
||||||
|
Math.abs(zAxis[2]),
|
||||||
|
]
|
||||||
|
|
||||||
|
// Find the dominant axis (like default planes do)
|
||||||
|
const maxComponent = Math.max(...absZAxis)
|
||||||
|
const dominantAxisIndex = absZAxis.indexOf(maxComponent)
|
||||||
|
|
||||||
|
// Check camera position against canonical orientation (like default planes)
|
||||||
|
const cameraComponents = [camVector.x, camVector.y, camVector.z]
|
||||||
|
let negated = cameraComponents[dominantAxisIndex] < 0
|
||||||
|
if (dominantAxisIndex === 1) {
|
||||||
|
// offset of the XZ is being weird, not sure if this is a camera bug
|
||||||
|
negated = !negated
|
||||||
|
}
|
||||||
|
sceneInfra.modelingSend({
|
||||||
|
type: 'Select sketch plane',
|
||||||
|
data: {
|
||||||
|
type: 'offsetPlane',
|
||||||
|
zAxis,
|
||||||
|
yAxis,
|
||||||
|
position: [
|
||||||
|
planeInfo.origin.x,
|
||||||
|
planeInfo.origin.y,
|
||||||
|
planeInfo.origin.z,
|
||||||
|
].map((num) => num / sceneInfra._baseUnitMultiplier) as [
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
],
|
||||||
|
planeId,
|
||||||
|
pathToNode: artifact.codeRef.pathToNode,
|
||||||
|
negated,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
resolve(true)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Error getting face details:', error)
|
||||||
|
resolve(false)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// selectOffsetSketchPlane called with an invalid artifact type',
|
||||||
|
resolve(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -216,9 +216,9 @@ async function getAndSyncStoredToken(input: {
|
|||||||
['api token', !!VITE_KITTYCAD_API_TOKEN],
|
['api token', !!VITE_KITTYCAD_API_TOKEN],
|
||||||
])
|
])
|
||||||
if (token) {
|
if (token) {
|
||||||
|
if (isDesktop()) {
|
||||||
// has just logged in, update storage
|
// has just logged in, update storage
|
||||||
localStorage.setItem(TOKEN_PERSIST_KEY, token)
|
localStorage.setItem(TOKEN_PERSIST_KEY, token)
|
||||||
if (isDesktop()) {
|
|
||||||
await writeTokenFile(token)
|
await writeTokenFile(token)
|
||||||
}
|
}
|
||||||
return token
|
return token
|
||||||
|
@ -2609,15 +2609,15 @@ export const modelingMachine = setup({
|
|||||||
insertIndex = nodeToEdit[1][0]
|
insertIndex = nodeToEdit[1][0]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract the default plane from selection
|
const selectedPlane = getSelectedPlane(selection)
|
||||||
const plane = selection.otherSelections[0]
|
if (!selectedPlane) {
|
||||||
if (!(plane && plane instanceof Object && 'name' in plane))
|
|
||||||
return trap('No plane selected')
|
return trap('No plane selected')
|
||||||
|
}
|
||||||
|
|
||||||
// Get the default plane name from the selection
|
// Get the default plane name from the selection
|
||||||
const offsetPlaneResult = addOffsetPlane({
|
const offsetPlaneResult = addOffsetPlane({
|
||||||
node: ast,
|
node: ast,
|
||||||
defaultPlane: plane.name,
|
plane: selectedPlane,
|
||||||
offset:
|
offset:
|
||||||
'variableName' in distance
|
'variableName' in distance
|
||||||
? distance.variableIdentifierAst
|
? distance.variableIdentifierAst
|
||||||
@ -5520,6 +5520,33 @@ export function isEditingExistingSketch({
|
|||||||
return (hasStartProfileAt && maybePipeExpression.body.length > 1) || hasCircle
|
return (hasStartProfileAt && maybePipeExpression.body.length > 1) || hasCircle
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getSelectedPlane = (
|
||||||
|
selection: Selections
|
||||||
|
): Node<Name> | Node<Literal> | undefined => {
|
||||||
|
const defaultPlane = selection.otherSelections[0]
|
||||||
|
if (
|
||||||
|
defaultPlane &&
|
||||||
|
defaultPlane instanceof Object &&
|
||||||
|
'name' in defaultPlane
|
||||||
|
) {
|
||||||
|
return createLiteral(defaultPlane.name.toUpperCase())
|
||||||
|
}
|
||||||
|
|
||||||
|
const offsetPlane = selection.graphSelections[0]
|
||||||
|
if (offsetPlane.artifact?.type === 'plane') {
|
||||||
|
const artifactId = offsetPlane.artifact?.id
|
||||||
|
const variableName = Object.entries(kclManager.variables).find(
|
||||||
|
([_, value]) => {
|
||||||
|
return value?.type === 'Plane' && value.value?.artifactId === artifactId
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const offsetPlaneName = variableName?.[0]
|
||||||
|
return offsetPlaneName ? createLocalName(offsetPlaneName) : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
export function pipeHasCircle({
|
export function pipeHasCircle({
|
||||||
sketchDetails,
|
sketchDetails,
|
||||||
}: {
|
}: {
|
||||||
|
Reference in New Issue
Block a user