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:
@ -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())
|
||||
})
|
||||
|
@ -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 |
Binary file not shown.
Before Width: | Height: | Size: 115 KiB |
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
Reference in New Issue
Block a user