snap to profile start (#1616)

* fix camera set from debug panel

* add snap to for closing sketch

* clean up file that shouldn't have been committed
This commit is contained in:
Kurt Hutten
2024-03-04 08:14:37 +11:00
committed by GitHub
parent 223b5952aa
commit 4d4a1d66e8
8 changed files with 193 additions and 106 deletions

View File

@ -1127,3 +1127,86 @@ test('Can edit segments by dragging their handles', async ({
|> line([14.69, 2.73], %)
|> tangentialArcTo([27.6, -3.25], %)`)
})
test('Snap to close works (at any scale)', async ({ page }) => {
const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
const doSnapAtDifferentScales = async (
camPos: [number, number, number],
expectedCode: string
) => {
await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(100)
await u.openAndClearDebugPanel()
await u.updateCamPosition(camPos)
await u.closeDebugPanel()
// select a plane
await page.mouse.click(700, 200)
await expect(page.locator('.cm-content')).toHaveText(
`const part001 = startSketchOn('XZ')`
)
let prevContent = await page.locator('.cm-content').innerText()
const pointA = [700, 200]
const pointB = [900, 200]
const pointC = [900, 400]
// draw three lines
await page.mouse.click(pointA[0], pointA[1])
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
await page.mouse.click(pointB[0], pointB[1])
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
await page.mouse.click(pointC[0], pointC[1])
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
await page.mouse.move(pointA[0] - 12, pointA[1] + 12)
const pointNotQuiteA = [pointA[0] - 7, pointA[1] + 7]
await page.mouse.move(pointNotQuiteA[0], pointNotQuiteA[1], { steps: 10 })
await page.mouse.click(pointNotQuiteA[0], pointNotQuiteA[1])
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
await expect(page.locator('.cm-content')).toHaveText(expectedCode)
// exit sketch
await u.openAndClearDebugPanel()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.removeCurrentCode()
}
const codeTemplate = (
scale = 1,
fudge = 0
) => `const part001 = startSketchOn('XZ')
|> startProfileAt([${roundOff(scale * 87.68)}, ${roundOff(scale * 43.84)}], %)
|> line([${roundOff(scale * 175.36)}, 0], %)
|> line([0, -${roundOff(scale * 175.37) + fudge}], %)
|> close(%)`
await doSnapAtDifferentScales([0, 100, 100], codeTemplate(0.01, 0.01))
await doSnapAtDifferentScales([0, 10000, 10000], codeTemplate())
})

View File

@ -31,12 +31,6 @@ test.beforeEach(async ({ context, page }) => {
test.setTimeout(60000)
const commonPoints = {
startAt: '[26.38, -35.59]',
num1: 26.63,
num2: 53.01,
}
test('change camera, show planes', async ({ page, context }) => {
const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
@ -46,21 +40,21 @@ test('change camera, show planes', async ({ page, context }) => {
const camPos: [number, number, number] = [0, 85, 85]
await u.updateCamPosition(camPos)
await page.waitForTimeout(200)
// rotate
await u.closeDebugPanel()
await page.mouse.move(700, 200)
await page.mouse.down({ button: 'right' })
await page.mouse.move(600, 300)
await page.mouse.move(600, 300, { steps: 10 })
await page.mouse.up({ button: 'right' })
await u.openDebugPanel()
await page.waitForTimeout(500)
await u.clearCommandLogs()
await u.openAndClearDebugPanel()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await u.closeDebugPanel()
await page.waitForTimeout(200)
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
@ -70,6 +64,7 @@ test('change camera, show planes', async ({ page, context }) => {
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await u.updateCamPosition(camPos)
await page.waitForTimeout(200)
await u.clearCommandLogs()
await u.closeDebugPanel()
@ -77,7 +72,7 @@ test('change camera, show planes', async ({ page, context }) => {
await page.keyboard.down('Shift')
await page.mouse.move(600, 200)
await page.mouse.down({ button: 'right' })
await page.mouse.move(700, 200)
await page.mouse.move(700, 200, { steps: 10 })
await page.mouse.up({ button: 'right' })
await page.keyboard.up('Shift')
@ -87,37 +82,13 @@ test('change camera, show planes', async ({ page, context }) => {
await page.getByRole('button', { name: 'Start Sketch' }).click()
await u.closeDebugPanel()
await page.waitForTimeout(200)
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
})
await u.openAndClearDebugPanel()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await u.updateCamPosition(camPos)
await u.clearCommandLogs()
await u.closeDebugPanel()
// zoom
await page.keyboard.down('Control')
await page.mouse.move(700, 400)
await page.mouse.down({ button: 'right' })
await page.mouse.move(700, 300)
await page.mouse.up({ button: 'right' })
await page.keyboard.up('Control')
await u.openDebugPanel()
await page.waitForTimeout(300)
await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await u.closeDebugPanel()
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
})
// zoom was too unstable to keep as a PW test)
})
test('exports of each format should work', async ({ page, context }) => {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 78 KiB

View File

@ -209,6 +209,7 @@ export class CameraControls {
this.camera.zoom = camProps.zoom || 1
}
this.camera.updateProjectionMatrix()
console.log('doing this thing', camProps)
this.update(true)
}
@ -569,7 +570,7 @@ export class CameraControls {
update = (forceUpdate = false) => {
// If there are any changes that need to be applied to the camera, apply them here.
let didChange = forceUpdate
let didChange = false
if (this.pendingRotation) {
this.rotateCamera(this.pendingRotation.x, this.pendingRotation.y)
this.pendingRotation = null // Clear the pending rotation after applying it
@ -621,8 +622,8 @@ export class CameraControls {
// Update the camera's matrices
this.camera.updateMatrixWorld()
if (didChange) {
this.onCameraChange()
if (didChange || forceUpdate) {
this.onCameraChange(forceUpdate)
}
// damping would be implemented here in update if we choose to add it.
@ -898,7 +899,7 @@ export class CameraControls {
this.reactCameraPropertiesCallback(a)
}, 200)
onCameraChange = () => {
onCameraChange = (forceUpdate = false) => {
const distance = this.target.distanceTo(this.camera.position)
if (this.camera.far / 2.1 < distance || this.camera.far / 1.9 > distance) {
this.camera.far = distance * 2
@ -906,7 +907,7 @@ export class CameraControls {
this.camera.updateProjectionMatrix()
}
if (this.syncDirection === 'clientToEngine')
if (this.syncDirection === 'clientToEngine' || forceUpdate)
throttledUpdateEngineCamera({
quaternion: this.camera.quaternion,
position: this.camera.position,

View File

@ -3,10 +3,13 @@ import {
DoubleSide,
ExtrudeGeometry,
Group,
Intersection,
LineCurve3,
Matrix4,
Mesh,
MeshBasicMaterial,
Object3D,
Object3DEventMap,
OrthographicCamera,
PerspectiveCamera,
PlaneGeometry,
@ -24,6 +27,7 @@ import {
defaultPlaneColor,
getSceneScale,
INTERSECTION_PLANE_LAYER,
OnMouseEnterLeaveArgs,
RAYCASTABLE_PLANE,
sceneInfra,
SKETCH_GROUP_SEGMENTS,
@ -64,7 +68,6 @@ import {
addCloseToPipe,
addNewSketchLn,
changeSketchArguments,
compareVec2Epsilon2,
updateStartProfileAtArgs,
} from 'lang/std/sketch'
import { isReducedMotion, throttle } from 'lib/utils'
@ -300,8 +303,8 @@ class SceneEntities {
: perspScale(sceneInfra.camControls.camera, dummy)) /
sceneInfra._baseUnitMultiplier
let segPathToNode = getNodePathFromSourceRange(
draftSegment ? truncatedAst : kclManager.ast,
const segPathToNode = getNodePathFromSourceRange(
kclManager.ast,
sketchGroup.start.__geoMeta.sourceRange
)
const _profileStart = profileStart({
@ -319,9 +322,23 @@ class SceneEntities {
sketchGroup.value.forEach((segment, index) => {
let segPathToNode = getNodePathFromSourceRange(
draftSegment ? truncatedAst : kclManager.ast,
kclManager.ast,
segment.__geoMeta.sourceRange
)
if (draftSegment && (sketchGroup.value[index - 1] || sketchGroup.start)) {
const previousSegment =
sketchGroup.value[index - 1] || sketchGroup.start
const previousSegmentPathToNode = getNodePathFromSourceRange(
kclManager.ast,
previousSegment.__geoMeta.sourceRange
)
const bodyIndex = previousSegmentPathToNode[1][0]
segPathToNode = getNodePathFromSourceRange(
truncatedAst,
segment.__geoMeta.sourceRange
)
segPathToNode[1][0] = bodyIndex
}
const isDraftSegment =
draftSegment && index === sketchGroup.value.length - 1
let seg
@ -364,11 +381,12 @@ class SceneEntities {
this.scene.add(group)
if (!draftSegment) {
sceneInfra.setCallbacks({
onDrag: ({ selected, intersectionPoint, mouseEvent }) => {
onDrag: ({ selected, intersectionPoint, mouseEvent, intersects }) => {
if (mouseEvent.which !== 1) return
this.onDragSegment({
object: selected,
intersection2d: intersectionPoint.twoD,
intersects,
sketchPathToNode,
})
},
@ -389,81 +407,32 @@ class SceneEntities {
if (!event) return
sceneInfra.modelingSend(event)
},
onMouseEnter: ({ selected }) => {
// TODO change the color of the segment to yellow?
// Give a few pixels grace around each of the segments
// for hover.
if ([X_AXIS, Y_AXIS].includes(selected?.userData?.type)) {
const obj = selected as Mesh
const mat = obj.material as MeshBasicMaterial
mat.color.set(obj.userData.baseColor)
mat.color.offsetHSL(0, 0, 0.5)
}
const parent = getParentGroup(selected, [
STRAIGHT_SEGMENT,
TANGENTIAL_ARC_TO_SEGMENT,
PROFILE_START,
])
if (parent?.userData?.pathToNode) {
const updatedAst = parse(recast(kclManager.ast))
const node = getNodeFromPath<CallExpression>(
updatedAst,
parent.userData.pathToNode,
'CallExpression'
).node
sceneInfra.highlightCallback([node.start, node.end])
const yellow = 0xffff00
colorSegment(selected, yellow)
return
}
sceneInfra.highlightCallback([0, 0])
},
onMouseLeave: ({ selected }) => {
sceneInfra.highlightCallback([0, 0])
const parent = getParentGroup(selected, [
STRAIGHT_SEGMENT,
TANGENTIAL_ARC_TO_SEGMENT,
PROFILE_START,
])
const isSelected = parent?.userData?.isSelected
colorSegment(selected, isSelected ? 0x0000ff : 0xffffff)
if ([X_AXIS, Y_AXIS].includes(selected?.userData?.type)) {
const obj = selected as Mesh
const mat = obj.material as MeshBasicMaterial
mat.color.set(obj.userData.baseColor)
if (obj.userData.isSelected) mat.color.offsetHSL(0, 0, 0.2)
}
},
...mouseEnterLeaveCallbacks(),
})
} else {
sceneInfra.setCallbacks({
onDrag: () => {},
onClick: async (args) => {
if (!args) return
if (args.mouseEvent.which !== 1) return
const { intersectionPoint } = args
if (!intersectionPoint?.twoD) return
let intersection2d = intersectionPoint?.twoD
const profileStart = args.intersects
.map(({ object }) => getParentGroup(object, [PROFILE_START]))
.find((a) => a?.name === PROFILE_START)
const firstSeg = sketchGroup.value[0]
const isClosingSketch = compareVec2Epsilon2(
firstSeg.from,
[intersectionPoint.twoD.x, intersectionPoint.twoD.y],
0.5
)
let modifiedAst
if (isClosingSketch) {
// TODO close needs a better UX
if (profileStart) {
modifiedAst = addCloseToPipe({
node: kclManager.ast,
programMemory: kclManager.programMemory,
pathToNode: sketchPathToNode,
})
} else {
} else if (intersection2d) {
const lastSegment = sketchGroup.value.slice(-1)[0]
modifiedAst = addNewSketchLn({
node: kclManager.ast,
programMemory: kclManager.programMemory,
to: [intersectionPoint.twoD.x, intersectionPoint.twoD.y],
to: [intersection2d.x, intersection2d.y],
from: [lastSegment.to[0], lastSegment.to[1]],
fnName:
lastSegment.type === 'TangentialArcTo'
@ -471,6 +440,9 @@ class SceneEntities {
: 'line',
pathToNode: sketchPathToNode,
}).modifiedAst
} else {
// return early as we didn't modify the ast
return
}
kclManager.executeAstMock(modifiedAst, { updates: 'code' })
@ -481,6 +453,7 @@ class SceneEntities {
this.onDragSegment({
intersection2d: args.intersectionPoint.twoD,
object: Object.values(this.activeSegments).slice(-1)[0],
intersects: args.intersects,
sketchPathToNode,
draftInfo: {
draftSegment,
@ -490,6 +463,7 @@ class SceneEntities {
},
})
},
...mouseEnterLeaveCallbacks(),
})
}
sceneInfra.camControls.enableRotate = false
@ -526,13 +500,15 @@ class SceneEntities {
)
onDragSegment({
object,
intersection2d,
intersection2d: _intersection2d,
sketchPathToNode,
draftInfo,
intersects,
}: {
object: any
intersection2d: Vector2
sketchPathToNode: PathToNode
intersects: Intersection<Object3D<Object3DEventMap>>[]
draftInfo?: {
draftSegment: DraftSegment
truncatedAst: Program
@ -540,6 +516,15 @@ class SceneEntities {
variableDeclarationName: string
}
}) {
const profileStart =
draftInfo &&
intersects
.map(({ object }) => getParentGroup(object, [PROFILE_START]))
.find((a) => a?.name === PROFILE_START)
const intersection2d = profileStart
? new Vector2(profileStart.position.x, profileStart.position.y)
: _intersection2d
const group = getParentGroup(object, [
STRAIGHT_SEGMENT,
TANGENTIAL_ARC_TO_SEGMENT,
@ -1095,3 +1080,50 @@ function massageFormats(a: any): Vector3 {
? new Vector3(a[0], a[1], a[2])
: new Vector3(a.x, a.y, a.z)
}
function mouseEnterLeaveCallbacks() {
return {
onMouseEnter: ({ selected }: OnMouseEnterLeaveArgs) => {
if ([X_AXIS, Y_AXIS].includes(selected?.userData?.type)) {
const obj = selected as Mesh
const mat = obj.material as MeshBasicMaterial
mat.color.set(obj.userData.baseColor)
mat.color.offsetHSL(0, 0, 0.5)
}
const parent = getParentGroup(selected, [
STRAIGHT_SEGMENT,
TANGENTIAL_ARC_TO_SEGMENT,
PROFILE_START,
])
if (parent?.userData?.pathToNode) {
const updatedAst = parse(recast(kclManager.ast))
const node = getNodeFromPath<CallExpression>(
updatedAst,
parent.userData.pathToNode,
'CallExpression'
).node
sceneInfra.highlightCallback([node.start, node.end])
const yellow = 0xffff00
colorSegment(selected, yellow)
return
}
sceneInfra.highlightCallback([0, 0])
},
onMouseLeave: ({ selected }: OnMouseEnterLeaveArgs) => {
sceneInfra.highlightCallback([0, 0])
const parent = getParentGroup(selected, [
STRAIGHT_SEGMENT,
TANGENTIAL_ARC_TO_SEGMENT,
PROFILE_START,
])
const isSelected = parent?.userData?.isSelected
colorSegment(selected, isSelected ? 0x0000ff : 0xffffff)
if ([X_AXIS, Y_AXIS].includes(selected?.userData?.type)) {
const obj = selected as Mesh
const mat = obj.material as MeshBasicMaterial
mat.color.set(obj.userData.baseColor)
if (obj.userData.isSelected) mat.color.offsetHSL(0, 0, 0.2)
}
},
}
}

View File

@ -48,7 +48,7 @@ export const AXIS_GROUP = 'axisGroup'
export const SKETCH_GROUP_SEGMENTS = 'sketch-group-segments'
export const ARROWHEAD = 'arrowhead'
interface OnMouseEnterLeaveArgs {
export interface OnMouseEnterLeaveArgs {
selected: Object3D<Object3DEventMap>
mouseEvent: MouseEvent
}