* Add ability to pick default plane in feature tree in 'Sketch no face' mode * add ability to select deoffset plane where starting a new sketch * use selectDefaultSketchPlane * refactor: remove some duplication * warning cleanups * feature tree items selectable depedngin on no face sketch mode * lint * fix small jump because of border:none when going into and back from 'No face sketch' mode * grey out items other than offset planes in 'No face sketch' mode * start sketching on plane in context menu * sketch on offset plane with context menu * add ability to right click on default plane and start sketch on it * default planes in feature tree should be selectable because of right click context menu * add right click Start sketch option for selected plane on the canvas * selectDefaultSketchPlane returns error now * circular deps * move select functions to lib/selections.ts to avoid circular deps * add test for clicking on feature tree after starting a new sketch * graphite suggestion * fix bug of not being able to create offset plane using another offset plane with command bar * add ability to select default plane on feature when going through the Offset plane command bar flow
This commit is contained in:
@ -187,6 +187,68 @@ sketch001 = startProfile(sketch002, at = [12.34, -12.34])
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).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', () => {
|
||||
const doEditSegmentsByDraggingHandle = async (
|
||||
page: Page,
|
||||
|
@ -24,7 +24,12 @@ import {
|
||||
getOperationVariableName,
|
||||
stdLibMap,
|
||||
} from '@src/lib/operations'
|
||||
import { editorManager, kclManager, rustContext } from '@src/lib/singletons'
|
||||
import {
|
||||
editorManager,
|
||||
kclManager,
|
||||
rustContext,
|
||||
sceneInfra,
|
||||
} from '@src/lib/singletons'
|
||||
import {
|
||||
featureTreeMachine,
|
||||
featureTreeMachineDefaultContext,
|
||||
@ -34,11 +39,20 @@ import {
|
||||
kclEditorActor,
|
||||
selectionEventSelector,
|
||||
} 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 = () => {
|
||||
const isEditorMounted = useSelector(kclEditorActor, editorIsMountedSelector)
|
||||
const lastSelectionEvent = useSelector(kclEditorActor, selectionEventSelector)
|
||||
const { send: modelingSend, state: modelingState } = useModelingContext()
|
||||
|
||||
const sketchNoFace = modelingState.matches('Sketch no face')
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [_featureTreeState, featureTreeSend] = useMachine(
|
||||
featureTreeMachine.provide({
|
||||
@ -195,6 +209,7 @@ export const FeatureTreePane = () => {
|
||||
key={key}
|
||||
item={operation}
|
||||
send={featureTreeSend}
|
||||
sketchNoFace={sketchNoFace}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
@ -251,6 +266,7 @@ const OperationItemWrapper = ({
|
||||
customSuffix,
|
||||
className,
|
||||
selectable = true,
|
||||
greyedOut = false,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLButtonElement> & {
|
||||
icon: CustomIconName
|
||||
@ -262,18 +278,19 @@ const OperationItemWrapper = ({
|
||||
menuItems?: ComponentProps<typeof ContextMenu>['items']
|
||||
errors?: Diagnostic[]
|
||||
selectable?: boolean
|
||||
greyedOut?: boolean
|
||||
}) => {
|
||||
const menuRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
return (
|
||||
<div
|
||||
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"
|
||||
>
|
||||
<button
|
||||
{...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" />
|
||||
<div className="flex flex-1 items-baseline align-baseline">
|
||||
@ -311,6 +328,7 @@ const OperationItemWrapper = ({
|
||||
const OperationItem = (props: {
|
||||
item: Operation
|
||||
send: Prop<Actor<typeof featureTreeMachine>, 'send'>
|
||||
sketchNoFace: boolean
|
||||
}) => {
|
||||
const kclContext = useKclContext()
|
||||
const name = getOperationLabel(props.item)
|
||||
@ -343,6 +361,12 @@ const OperationItem = (props: {
|
||||
}, [kclContext.diagnostics.length])
|
||||
|
||||
function selectOperation() {
|
||||
if (props.sketchNoFace) {
|
||||
if (isOffsetPlane(props.item)) {
|
||||
const artifact = findOperationArtifact(props.item)
|
||||
void selectOffsetSketchPlane(artifact)
|
||||
}
|
||||
} else {
|
||||
if (props.item.type === 'GroupEnd') {
|
||||
return
|
||||
}
|
||||
@ -353,6 +377,7 @@ const OperationItem = (props: {
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(
|
||||
() => [
|
||||
<ContextMenuItem
|
||||
@ -477,6 +516,13 @@ const OperationItem = (props: {
|
||||
</ContextMenuItem>,
|
||||
]
|
||||
: []),
|
||||
...(isOffsetPlane(props.item)
|
||||
? [
|
||||
<ContextMenuItem onClick={startSketchOnOffsetPlane}>
|
||||
Start Sketch
|
||||
</ContextMenuItem>,
|
||||
]
|
||||
: []),
|
||||
...(props.item.type === 'StdLibCall' ||
|
||||
props.item.type === 'VariableDeclaration'
|
||||
? [
|
||||
@ -550,22 +596,63 @@ const OperationItem = (props: {
|
||||
[props.item, props.send]
|
||||
)
|
||||
|
||||
const enabled = !props.sketchNoFace || isOffsetPlane(props.item)
|
||||
|
||||
return (
|
||||
<OperationItemWrapper
|
||||
selectable={enabled}
|
||||
icon={getOperationIcon(props.item)}
|
||||
name={name}
|
||||
variableName={variableName}
|
||||
valueDetail={valueDetail}
|
||||
menuItems={menuItems}
|
||||
onClick={selectOperation}
|
||||
onDoubleClick={enterEditFlow}
|
||||
onDoubleClick={props.sketchNoFace ? undefined : enterEditFlow} // no double click in "Sketch no face" mode
|
||||
errors={errors}
|
||||
greyedOut={!enabled}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const DefaultPlanes = () => {
|
||||
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
|
||||
if (!defaultPlanes) return null
|
||||
@ -603,7 +690,15 @@ const DefaultPlanes = () => {
|
||||
customSuffix={plane.customSuffix}
|
||||
icon={'plane'}
|
||||
name={plane.name}
|
||||
selectable={false}
|
||||
selectable={true}
|
||||
onClick={() => onClickPlane(plane.id)}
|
||||
menuItems={[
|
||||
<ContextMenuItem
|
||||
onClick={() => startSketchOnDefaultPlane(plane.id)}
|
||||
>
|
||||
Start Sketch
|
||||
</ContextMenuItem>,
|
||||
]}
|
||||
visibilityToggle={{
|
||||
visible: modelingState.context.defaultPlaneVisibility[plane.key],
|
||||
onVisibilityChange: () => {
|
||||
@ -620,3 +715,17 @@ const DefaultPlanes = () => {
|
||||
</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 type { AxisNames } from '@src/lib/constants'
|
||||
import { VIEW_NAMES_SEMANTIC } from '@src/lib/constants'
|
||||
import { sceneInfra } from '@src/lib/singletons'
|
||||
import { reportRejection } from '@src/lib/trap'
|
||||
import { kclManager, sceneInfra } from '@src/lib/singletons'
|
||||
import { err, reportRejection } from '@src/lib/trap'
|
||||
import { useSettings } from '@src/lib/singletons'
|
||||
import { resetCameraPosition } from '@src/lib/resetCameraPosition'
|
||||
import type { Selections } from '@src/lib/selections'
|
||||
import {
|
||||
selectDefaultSketchPlane,
|
||||
selectOffsetSketchPlane,
|
||||
} from '@src/lib/selections'
|
||||
|
||||
export function useViewControlMenuItems() {
|
||||
const { state: modelingState, send: modelingSend } = useModelingContext()
|
||||
const selectedPlaneId = getCurrentPlaneId(
|
||||
modelingState.context.selectionRanges
|
||||
)
|
||||
|
||||
const settings = useSettings()
|
||||
const shouldLockView =
|
||||
modelingState.matches('Sketch') &&
|
||||
@ -56,9 +65,35 @@ export function useViewControlMenuItems() {
|
||||
Center view on selection
|
||||
</ContextMenuItem>,
|
||||
<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 />,
|
||||
],
|
||||
[VIEW_NAMES_SEMANTIC, shouldLockView]
|
||||
[VIEW_NAMES_SEMANTIC, shouldLockView, selectedPlaneId]
|
||||
)
|
||||
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 type { CallExpressionKw, PathToNode } from '@src/lang/wasm'
|
||||
import { defaultSourceRange } from '@src/lang/sourceRange'
|
||||
import type { DefaultPlaneStr } from '@src/lib/planes'
|
||||
import { getEventForSelectWithPoint } from '@src/lib/selections'
|
||||
import {
|
||||
getEventForSelectWithPoint,
|
||||
selectDefaultSketchPlane,
|
||||
selectOffsetSketchPlane,
|
||||
} from '@src/lib/selections'
|
||||
import {
|
||||
editorManager,
|
||||
engineCommandManager,
|
||||
kclManager,
|
||||
rustContext,
|
||||
sceneEntitiesManager,
|
||||
sceneInfra,
|
||||
} from '@src/lib/singletons'
|
||||
@ -96,131 +98,18 @@ export function useEngineConnectionSubscriptions() {
|
||||
;(async () => {
|
||||
let planeOrFaceId = data.entity_id
|
||||
if (!planeOrFaceId) return
|
||||
|
||||
const defaultSketchPlaneSelected =
|
||||
selectDefaultSketchPlane(planeOrFaceId)
|
||||
if (
|
||||
rustContext.defaultPlanes?.xy === planeOrFaceId ||
|
||||
rustContext.defaultPlanes?.xz === planeOrFaceId ||
|
||||
rustContext.defaultPlanes?.yz === planeOrFaceId ||
|
||||
rustContext.defaultPlanes?.negXy === planeOrFaceId ||
|
||||
rustContext.defaultPlanes?.negXz === planeOrFaceId ||
|
||||
rustContext.defaultPlanes?.negYz === planeOrFaceId
|
||||
!err(defaultSketchPlaneSelected) &&
|
||||
defaultSketchPlaneSelected
|
||||
) {
|
||||
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
|
||||
}
|
||||
|
||||
const artifact = kclManager.artifactGraph.get(planeOrFaceId)
|
||||
|
||||
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,
|
||||
},
|
||||
})
|
||||
if (await selectOffsetSketchPlane(artifact)) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -483,13 +483,13 @@ export function sketchOnExtrudedFace(
|
||||
*/
|
||||
export function addOffsetPlane({
|
||||
node,
|
||||
defaultPlane,
|
||||
plane,
|
||||
insertIndex,
|
||||
offset,
|
||||
planeName,
|
||||
}: {
|
||||
node: Node<Program>
|
||||
defaultPlane: DefaultPlaneStr
|
||||
plane: Node<Literal> | Node<Name> // Can be DefaultPlaneStr or string for offsetPlanes
|
||||
insertIndex?: number
|
||||
offset: Expr
|
||||
planeName?: string
|
||||
@ -500,11 +500,9 @@ export function addOffsetPlane({
|
||||
|
||||
const newPlane = createVariableDeclaration(
|
||||
newPlaneName,
|
||||
createCallExpressionStdLibKw(
|
||||
'offsetPlane',
|
||||
createLiteral(defaultPlane.toUpperCase()),
|
||||
[createLabeledArg('offset', offset)]
|
||||
)
|
||||
createCallExpressionStdLibKw('offsetPlane', plane, [
|
||||
createLabeledArg('offset', offset),
|
||||
])
|
||||
)
|
||||
|
||||
const insertAt =
|
||||
|
@ -34,6 +34,7 @@ import {
|
||||
kclManager,
|
||||
rustContext,
|
||||
sceneEntitiesManager,
|
||||
sceneInfra,
|
||||
} from '@src/lib/singletons'
|
||||
import { err } from '@src/lib/trap'
|
||||
import {
|
||||
@ -803,3 +804,156 @@ export function getSemanticSelectionType(selectionType: Artifact['type'][]) {
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -2609,15 +2609,15 @@ export const modelingMachine = setup({
|
||||
insertIndex = nodeToEdit[1][0]
|
||||
}
|
||||
|
||||
// Extract the default plane from selection
|
||||
const plane = selection.otherSelections[0]
|
||||
if (!(plane && plane instanceof Object && 'name' in plane))
|
||||
const selectedPlane = getSelectedPlane(selection)
|
||||
if (!selectedPlane) {
|
||||
return trap('No plane selected')
|
||||
}
|
||||
|
||||
// Get the default plane name from the selection
|
||||
const offsetPlaneResult = addOffsetPlane({
|
||||
node: ast,
|
||||
defaultPlane: plane.name,
|
||||
plane: selectedPlane,
|
||||
offset:
|
||||
'variableName' in distance
|
||||
? distance.variableIdentifierAst
|
||||
@ -5520,6 +5520,33 @@ export function isEditingExistingSketch({
|
||||
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({
|
||||
sketchDetails,
|
||||
}: {
|
||||
|
Reference in New Issue
Block a user