Compare commits

...

9 Commits

Author SHA1 Message Date
a7b09f996a A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) 2024-03-03 09:24:32 +00:00
fa0a21b841 trigger ci 2024-03-03 20:14:42 +11:00
6cd66aca78 A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) 2024-03-03 09:01:56 +00:00
1a370cecda fix camera set from debug panel 2024-03-03 19:54:24 +11:00
223b5952aa Refactor mouse event args (#1613)
* refactor mouse event interfaces

Importantly returning multiple intersections from raycastRing, but other clean up

* refactor enter exit args interface

* type tweak
2024-03-03 16:23:16 +11:00
fedffbb384 Grackle: stdlib LineTo function (#1601)
* Bump execution plan

* Grackle: lineTo stdlib function (#1605)

* Remove test JSON output
2024-03-02 18:39:31 -06:00
ed4e3df3b2 unused vars cleanup (#1608) 2024-03-02 20:30:24 +11:00
18d200e790 add test: Can edit segments by dragging their handles (#1607) 2024-03-02 20:08:13 +11:00
0c50a5996d show selected color for start selected at (#1606) 2024-03-02 19:00:24 +11:00
21 changed files with 397 additions and 192 deletions

1
.gitignore vendored
View File

@ -33,6 +33,7 @@ src/wasm-lib/bindings
src/wasm-lib/kcl/bindings
public/wasm_lib_bg.wasm
src/wasm-lib/lcov.info
src/wasm-lib/grackle/test_json_output
e2e/playwright/playwright-secrets.env
e2e/playwright/temp1.png

View File

@ -1053,3 +1053,77 @@ test('Deselecting line tool should mean nothing happens on click', async ({
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
previousCodeContent = await page.locator('.cm-content').innerText()
})
test('Can edit segments by dragging their handles', async ({
page,
context,
}) => {
const u = getUtils(page)
await context.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const part001 = startSketchOn('-XZ')
|> startProfileAt([4.61, -14.01], %)
|> line([12.73, -0.09], %)
|> tangentialArcTo([24.95, -5.38], %)`
)
})
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
const startPX = [652, 418]
const lineEndPX = [794, 416]
const arcEndPX = [893, 318]
const dragPX = 30
await page.getByText('startProfileAt([4.61, -14.01], %)').click()
await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(100)
let prevContent = await page.locator('.cm-content').innerText()
const step5 = { steps: 5 }
// drag startProfieAt handle
await page.mouse.move(startPX[0], startPX[1])
await page.mouse.down()
await page.mouse.move(startPX[0] + dragPX, startPX[1] - dragPX, step5)
await page.mouse.up()
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
// drag line handle
await page.mouse.move(lineEndPX[0] + dragPX, lineEndPX[1] - dragPX)
await page.mouse.down()
await page.mouse.move(
lineEndPX[0] + dragPX * 2,
lineEndPX[1] - dragPX * 2,
step5
)
await page.mouse.up()
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
// drag tangentialArcTo handle
await page.mouse.move(arcEndPX[0], arcEndPX[1])
await page.mouse.down()
await page.mouse.move(arcEndPX[0] + dragPX, arcEndPX[1] - dragPX, step5)
await page.mouse.up()
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
// expect the code to have changed
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([7.01, -11.79], %)
|> line([14.69, 2.73], %)
|> tangentialArcTo([27.6, -3.25], %)`)
})

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: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 91 KiB

View File

@ -3,15 +3,8 @@ import {
createBrowserRouter,
Outlet,
redirect,
useLocation,
RouterProvider,
} from 'react-router-dom'
import {
matchRoutes,
createRoutesFromChildren,
useNavigationType,
} from 'react-router'
import { useEffect } from 'react'
import { ErrorPage } from './components/ErrorPage'
import { Settings } from './routes/Settings'
import Onboarding, { onboardingRoutes } from './routes/Onboarding'

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

@ -364,17 +364,18 @@ class SceneEntities {
this.scene.add(group)
if (!draftSegment) {
sceneInfra.setCallbacks({
onDrag: (args) => {
if (args.event.which !== 1) return
onDrag: ({ selected, intersectionPoint, mouseEvent }) => {
if (mouseEvent.which !== 1) return
this.onDragSegment({
...args,
object: selected,
intersection2d: intersectionPoint.twoD,
sketchPathToNode,
})
},
onMove: () => {},
onClick: (args) => {
if (args?.event.which !== 1) return
if (!args || !args.object) {
if (args?.mouseEvent.which !== 1) return
if (!args || !args.selected) {
sceneInfra.modelingSend({
type: 'Set selection',
data: {
@ -383,22 +384,22 @@ class SceneEntities {
})
return
}
const { object } = args
const event = getEventForSegmentSelection(object)
const { selected } = args
const event = getEventForSegmentSelection(selected)
if (!event) return
sceneInfra.modelingSend(event)
},
onMouseEnter: ({ object }) => {
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(object?.userData?.type)) {
const obj = object as Mesh
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(object, [
const parent = getParentGroup(selected, [
STRAIGHT_SEGMENT,
TANGENTIAL_ARC_TO_SEGMENT,
PROFILE_START,
@ -412,18 +413,22 @@ class SceneEntities {
).node
sceneInfra.highlightCallback([node.start, node.end])
const yellow = 0xffff00
colorSegment(object, yellow)
colorSegment(selected, yellow)
return
}
sceneInfra.highlightCallback([0, 0])
},
onMouseLeave: ({ object }) => {
onMouseLeave: ({ selected }) => {
sceneInfra.highlightCallback([0, 0])
const parent = getParentGroup(object)
const parent = getParentGroup(selected, [
STRAIGHT_SEGMENT,
TANGENTIAL_ARC_TO_SEGMENT,
PROFILE_START,
])
const isSelected = parent?.userData?.isSelected
colorSegment(object, isSelected ? 0x0000ff : 0xffffff)
if ([X_AXIS, Y_AXIS].includes(object?.userData?.type)) {
const obj = object as Mesh
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)
@ -435,14 +440,14 @@ class SceneEntities {
onDrag: () => {},
onClick: async (args) => {
if (!args) return
if (args.event.which !== 1) return
const { intersection2d } = args
if (!intersection2d) return
if (args.mouseEvent.which !== 1) return
const { intersectionPoint } = args
if (!intersectionPoint?.twoD) return
const firstSeg = sketchGroup.value[0]
const isClosingSketch = compareVec2Epsilon2(
firstSeg.from,
[intersection2d.x, intersection2d.y],
[intersectionPoint.twoD.x, intersectionPoint.twoD.y],
0.5
)
let modifiedAst
@ -458,7 +463,7 @@ class SceneEntities {
modifiedAst = addNewSketchLn({
node: kclManager.ast,
programMemory: kclManager.programMemory,
to: [intersection2d.x, intersection2d.y],
to: [intersectionPoint.twoD.x, intersectionPoint.twoD.y],
from: [lastSegment.to[0], lastSegment.to[1]],
fnName:
lastSegment.type === 'TangentialArcTo'
@ -474,7 +479,7 @@ class SceneEntities {
},
onMove: (args) => {
this.onDragSegment({
...args,
intersection2d: args.intersectionPoint.twoD,
object: Object.values(this.activeSegments).slice(-1)[0],
sketchPathToNode,
draftInfo: {
@ -521,15 +526,11 @@ class SceneEntities {
)
onDragSegment({
object,
event,
intersectPoint,
intersection2d,
sketchPathToNode,
draftInfo,
}: {
object: any
event: any
intersectPoint: Vector3
intersection2d: Vector2
sketchPathToNode: PathToNode
draftInfo?: {
@ -847,22 +848,24 @@ class SceneEntities {
}
setupDefaultPlaneHover() {
sceneInfra.setCallbacks({
onMouseEnter: ({ object }) => {
if (object.parent.userData.type !== DEFAULT_PLANES) return
const type: DefaultPlane = object.userData.type
object.material.color = defaultPlaneColor(type, 0.5, 1)
onMouseEnter: ({ selected }) => {
if (!(selected instanceof Mesh && selected.parent)) return
if (selected.parent.userData.type !== DEFAULT_PLANES) return
const type: DefaultPlane = selected.userData.type
selected.material.color = defaultPlaneColor(type, 0.5, 1)
},
onMouseLeave: ({ object }) => {
if (object.parent.userData.type !== DEFAULT_PLANES) return
const type: DefaultPlane = object.userData.type
object.material.color = defaultPlaneColor(type)
onMouseLeave: ({ selected }) => {
if (!(selected instanceof Mesh && selected.parent)) return
if (selected.parent.userData.type !== DEFAULT_PLANES) return
const type: DefaultPlane = selected.userData.type
selected.material.color = defaultPlaneColor(type)
},
onClick: (args) => {
if (!args || !args.object) return
if (args.event.which !== 1) return
const { intersection } = args
const type = intersection.object.name || ''
const posNorm = Number(intersection.normal?.z) > 0
if (!args || !args.intersects?.[0]) return
if (args.mouseEvent.which !== 1) return
const { intersects } = args
const type = intersects?.[0].object.name || ''
const posNorm = Number(intersects?.[0]?.normal?.z) > 0
let planeString: DefaultPlaneStr = posNorm ? 'XY' : '-XY'
let normal: [number, number, number] = posNorm ? [0, 0, 1] : [0, 0, -1]
if (type === YZ_PLANE) {

View File

@ -19,7 +19,7 @@ import {
Object3D,
Object3DEventMap,
} from 'three'
import { Coords2d, compareVec2Epsilon2 } from 'lang/std/sketch'
import { compareVec2Epsilon2 } from 'lang/std/sketch'
import { useModelingContext } from 'hooks/useModelingContext'
import * as TWEEN from '@tweenjs/tween.js'
import { SourceRange } from 'lang/wasm'
@ -48,31 +48,36 @@ export const AXIS_GROUP = 'axisGroup'
export const SKETCH_GROUP_SEGMENTS = 'sketch-group-segments'
export const ARROWHEAD = 'arrowhead'
interface BaseCallbackArgs2 {
object: any
event: any
}
interface BaseCallbackArgs {
event: any
}
interface OnDragCallbackArgs extends BaseCallbackArgs {
object: any
intersection2d: Vector2
intersectPoint: Vector3
intersection: Intersection<Object3D<Object3DEventMap>>
}
interface OnClickCallbackArgs extends BaseCallbackArgs {
intersection2d?: Vector2
intersectPoint: Vector3
intersection: Intersection<Object3D<Object3DEventMap>>
object?: any
interface OnMouseEnterLeaveArgs {
selected: Object3D<Object3DEventMap>
mouseEvent: MouseEvent
}
interface onMoveCallbackArgs {
event: any
intersection2d: Vector2
intersectPoint: Vector3
intersection: Intersection<Object3D<Object3DEventMap>>
interface OnDragCallbackArgs extends OnMouseEnterLeaveArgs {
intersectionPoint: {
twoD: Vector2
threeD: Vector3
}
intersects: Intersection<Object3D<Object3DEventMap>>[]
}
interface OnClickCallbackArgs {
mouseEvent: MouseEvent
intersectionPoint?: {
twoD: Vector2
threeD: Vector3
}
intersects: Intersection<Object3D<Object3DEventMap>>[]
selected?: Object3D<Object3DEventMap>
}
interface OnMoveCallbackArgs {
mouseEvent: MouseEvent
intersectionPoint: {
twoD: Vector2
threeD: Vector3
}
intersects: Intersection<Object3D<Object3DEventMap>>[]
selected?: Object3D<Object3DEventMap>
}
// This singleton class is responsible for all of the under the hood setup for the client side scene.
@ -90,16 +95,16 @@ class SceneInfra {
_baseUnit: BaseUnit = 'mm'
_baseUnitMultiplier = 1
onDragCallback: (arg: OnDragCallbackArgs) => void = () => {}
onMoveCallback: (arg: onMoveCallbackArgs) => void = () => {}
onMoveCallback: (arg: OnMoveCallbackArgs) => void = () => {}
onClickCallback: (arg?: OnClickCallbackArgs) => void = () => {}
onMouseEnter: (arg: BaseCallbackArgs2) => void = () => {}
onMouseLeave: (arg: BaseCallbackArgs2) => void = () => {}
onMouseEnter: (arg: OnMouseEnterLeaveArgs) => void = () => {}
onMouseLeave: (arg: OnMouseEnterLeaveArgs) => void = () => {}
setCallbacks = (callbacks: {
onDrag?: (arg: OnDragCallbackArgs) => void
onMove?: (arg: onMoveCallbackArgs) => void
onMove?: (arg: OnMoveCallbackArgs) => void
onClick?: (arg?: OnClickCallbackArgs) => void
onMouseEnter?: (arg: BaseCallbackArgs2) => void
onMouseLeave?: (arg: BaseCallbackArgs2) => void
onMouseEnter?: (arg: OnMouseEnterLeaveArgs) => void
onMouseLeave?: (arg: OnMouseEnterLeaveArgs) => void
}) => {
this.onDragCallback = callbacks.onDrag || this.onDragCallback
this.onMoveCallback = callbacks.onMove || this.onMoveCallback
@ -142,10 +147,9 @@ class SceneInfra {
currentMouseVector = new Vector2()
selected: {
mouseDownVector: Vector2
object: any
object: Object3D<Object3DEventMap>
hasBeenDragged: boolean
} | null = null
selectedObject: null | any = null
mouseDownVector: null | Vector2 = null
constructor() {
@ -242,8 +246,8 @@ class SceneInfra {
// Dispose of any other resources like geometries, materials, textures
}
getPlaneIntersectPoint = (): {
intersection2d?: Vector2
intersectPoint: Vector3
twoD?: Vector2
threeD?: Vector3
intersection: Intersection<Object3D<Object3DEventMap>>
} | null => {
this.planeRaycaster.setFromCamera(
@ -254,23 +258,11 @@ class SceneInfra {
this.scene.children,
true
)
if (
planeIntersects.length > 0 &&
planeIntersects[0].object.userData.type !== RAYCASTABLE_PLANE
) {
const intersect = planeIntersects[0]
return {
intersectPoint: intersect.point,
intersection: intersect,
}
}
if (
!(
planeIntersects.length > 0 &&
planeIntersects[0].object.userData.type === RAYCASTABLE_PLANE
)
const recastablePlaneIntersect = planeIntersects.find(
(intersect) => intersect.object.name === RAYCASTABLE_PLANE
)
return null
if (!planeIntersects.length) return null
if (!recastablePlaneIntersect) return { intersection: planeIntersects[0] }
const planePosition = planeIntersects[0].object.position
const inversePlaneQuaternion = planeIntersects[0].object.quaternion
.clone()
@ -285,19 +277,21 @@ class SceneInfra {
}
return {
intersection2d: new Vector2(
twoD: new Vector2(
transformedPoint.x / this._baseUnitMultiplier,
transformedPoint.y / this._baseUnitMultiplier
), // z should be 0
intersectPoint: intersectPoint.divideScalar(this._baseUnitMultiplier),
threeD: intersectPoint.divideScalar(this._baseUnitMultiplier),
intersection: planeIntersects[0],
}
}
onMouseMove = (event: MouseEvent) => {
this.currentMouseVector.x = (event.clientX / window.innerWidth) * 2 - 1
this.currentMouseVector.y = -(event.clientY / window.innerHeight) * 2 + 1
onMouseMove = (mouseEvent: MouseEvent) => {
this.currentMouseVector.x = (mouseEvent.clientX / window.innerWidth) * 2 - 1
this.currentMouseVector.y =
-(mouseEvent.clientY / window.innerHeight) * 2 + 1
const planeIntersectPoint = this.getPlaneIntersectPoint()
const intersects = this.raycastRing()
if (this.selected) {
const hasBeenDragged = !compareVec2Epsilon2(
@ -313,47 +307,56 @@ class SceneInfra {
if (
hasBeenDragged &&
planeIntersectPoint &&
planeIntersectPoint.intersection2d
planeIntersectPoint.twoD &&
planeIntersectPoint.threeD
) {
// // console.log('onDrag', this.selected)
this.onDragCallback({
object: this.selected.object,
event,
intersection2d: planeIntersectPoint.intersection2d,
...planeIntersectPoint,
mouseEvent,
intersectionPoint: {
twoD: planeIntersectPoint.twoD,
threeD: planeIntersectPoint.threeD,
},
intersects,
selected: this.selected.object,
})
}
} else if (planeIntersectPoint && planeIntersectPoint.intersection2d) {
} else if (
planeIntersectPoint &&
planeIntersectPoint.twoD &&
planeIntersectPoint.threeD
) {
this.onMoveCallback({
event,
intersection2d: planeIntersectPoint.intersection2d,
...planeIntersectPoint,
mouseEvent,
intersectionPoint: {
twoD: planeIntersectPoint.twoD,
threeD: planeIntersectPoint.threeD,
},
intersects,
})
}
const intersect = this.raycastRing()
if (intersect) {
const firstIntersectObject = intersect.object
if (intersects[0]) {
const firstIntersectObject = intersects[0].object
if (this.hoveredObject !== firstIntersectObject) {
if (this.hoveredObject) {
this.onMouseLeave({
object: this.hoveredObject,
event,
selected: this.hoveredObject,
mouseEvent: mouseEvent,
})
}
this.hoveredObject = firstIntersectObject
this.onMouseEnter({
object: this.hoveredObject,
event,
selected: this.hoveredObject,
mouseEvent: mouseEvent,
})
}
} else {
if (this.hoveredObject) {
this.onMouseLeave({
object: this.hoveredObject,
event,
selected: this.hoveredObject,
mouseEvent: mouseEvent,
})
this.hoveredObject = null
}
@ -363,41 +366,38 @@ class SceneInfra {
raycastRing = (
pixelRadius = 8,
rayRingCount = 32
): Intersection<Object3D<Object3DEventMap>> | undefined => {
): Intersection<Object3D<Object3DEventMap>>[] => {
const mouseDownVector = this.currentMouseVector.clone()
let closestIntersection:
| Intersection<Object3D<Object3DEventMap>>
| undefined = undefined
let closestDistance = Infinity
const intersectionsMap = new Map<
Object3D,
Intersection<Object3D<Object3DEventMap>>
>()
const updateClosestIntersection = (
const updateIntersectionsMap = (
intersections: Intersection<Object3D<Object3DEventMap>>[]
) => {
let intersection = null
for (let i = 0; i < intersections.length; i++) {
if (intersections[i].object.type !== 'GridHelper') {
intersection = intersections[i]
break
intersections.forEach((intersection) => {
if (intersection.object.type !== 'GridHelper') {
const existingIntersection = intersectionsMap.get(intersection.object)
if (
!existingIntersection ||
existingIntersection.distance > intersection.distance
) {
intersectionsMap.set(intersection.object, intersection)
}
}
}
if (!intersection) return
if (intersection.distance < closestDistance) {
closestDistance = intersection.distance
closestIntersection = intersection
}
})
}
// Check the center point
this.raycaster.setFromCamera(mouseDownVector, this.camControls.camera)
updateClosestIntersection(
updateIntersectionsMap(
this.raycaster.intersectObjects(this.scene.children, true)
)
// Check the ring points
for (let i = 0; i < rayRingCount; i++) {
const angle = (i / rayRingCount) * Math.PI * 2
const offsetX = ((pixelRadius * Math.cos(angle)) / window.innerWidth) * 2
const offsetY = ((pixelRadius * Math.sin(angle)) / window.innerHeight) * 2
const ringVector = new Vector2(
@ -405,11 +405,15 @@ class SceneInfra {
mouseDownVector.y - offsetY
)
this.raycaster.setFromCamera(ringVector, this.camControls.camera)
updateClosestIntersection(
updateIntersectionsMap(
this.raycaster.intersectObjects(this.scene.children, true)
)
}
return closestIntersection
// Convert the map values to an array and sort by distance
return Array.from(intersectionsMap.values()).sort(
(a, b) => a.distance - b.distance
)
}
onMouseDown = (event: MouseEvent) => {
@ -417,45 +421,60 @@ class SceneInfra {
this.currentMouseVector.y = -(event.clientY / window.innerHeight) * 2 + 1
const mouseDownVector = this.currentMouseVector.clone()
const intersect = this.raycastRing()
const intersect = this.raycastRing()[0]
if (intersect) {
const intersectParent = intersect?.object?.parent as Group
this.selected = intersectParent.isGroup
? {
mouseDownVector,
object: intersect?.object,
object: intersect.object,
hasBeenDragged: false,
}
: null
}
}
onMouseUp = (event: MouseEvent) => {
this.currentMouseVector.x = (event.clientX / window.innerWidth) * 2 - 1
this.currentMouseVector.y = -(event.clientY / window.innerHeight) * 2 + 1
onMouseUp = (mouseEvent: MouseEvent) => {
this.currentMouseVector.x = (mouseEvent.clientX / window.innerWidth) * 2 - 1
this.currentMouseVector.y =
-(mouseEvent.clientY / window.innerHeight) * 2 + 1
const planeIntersectPoint = this.getPlaneIntersectPoint()
const intersects = this.raycastRing()
if (this.selected) {
if (this.selected.hasBeenDragged) {
// this is where we could fire a onDragEnd event
// console.log('onDragEnd', this.selected)
} else if (planeIntersectPoint) {
} else if (planeIntersectPoint?.twoD && planeIntersectPoint?.threeD) {
// fire onClick event as there was no drags
this.onClickCallback({
object: this.selected?.object,
event,
...planeIntersectPoint,
mouseEvent,
intersectionPoint: {
twoD: planeIntersectPoint.twoD,
threeD: planeIntersectPoint.threeD,
},
intersects,
selected: this.selected.object,
})
} else if (planeIntersectPoint) {
this.onClickCallback({
mouseEvent,
intersects,
})
} else {
this.onClickCallback()
}
// Clear the selected state whether it was dragged or not
this.selected = null
} else if (planeIntersectPoint) {
} else if (planeIntersectPoint?.twoD && planeIntersectPoint?.threeD) {
this.onClickCallback({
event,
...planeIntersectPoint,
mouseEvent,
intersectionPoint: {
twoD: planeIntersectPoint.twoD,
threeD: planeIntersectPoint.threeD,
},
intersects,
})
} else {
this.onClickCallback()

View File

@ -115,6 +115,7 @@ export function straightSegment({
pathToNode,
isSelected: false,
}
group.name = STRAIGHT_SEGMENT
const arrowGroup = createArrowhead(scale)
arrowGroup.position.set(to[0], to[1], 0)
@ -203,6 +204,7 @@ export function tangentialArcToSegment({
pathToNode,
isSelected: false,
}
group.name = TANGENTIAL_ARC_TO_SEGMENT
const arrowGroup = createArrowhead(scale)
arrowGroup.position.set(to[0], to[1], 0)

View File

@ -6,7 +6,6 @@ import {
PipeExpression,
VariableDeclaration,
VariableDeclarator,
ExpressionStatement,
Value,
Literal,
PipeSubstitution,

View File

@ -20,6 +20,7 @@ import {
TANGENTIAL_ARC_TO_SEGMENT,
sceneEntitiesManager,
getParentGroup,
PROFILE_START,
} from 'clientSideScene/sceneEntities'
import { Mesh } from 'three'
import { AXIS_GROUP, X_AXIS } from 'clientSideScene/sceneInfra'
@ -188,7 +189,11 @@ export async function getEventForSelectWithPoint(
export function getEventForSegmentSelection(
obj: any
): ModelingMachineEvent | null {
const group = getParentGroup(obj)
const group = getParentGroup(obj, [
STRAIGHT_SEGMENT,
TANGENTIAL_ARC_TO_SEGMENT,
PROFILE_START,
])
const axisGroup = getParentGroup(obj, [AXIS_GROUP])
if (!group && !axisGroup) return null
if (axisGroup?.userData.type === AXIS_GROUP) {
@ -407,8 +412,8 @@ function updateSceneObjectColors(codeBasedSelections: Selection[]) {
}
Object.values(sceneEntitiesManager.activeSegments).forEach((segmentGroup) => {
if (
![STRAIGHT_SEGMENT, TANGENTIAL_ARC_TO_SEGMENT].includes(
segmentGroup?.userData?.type
![STRAIGHT_SEGMENT, TANGENTIAL_ARC_TO_SEGMENT, PROFILE_START].includes(
segmentGroup?.name
)
)
return

View File

@ -829,13 +829,13 @@ export const modelingMachine = createMachine(
sceneInfra.setCallbacks({
onClick: async (args) => {
if (!args) return
if (args.event.which !== 1) return
const { intersection2d } = args
if (!intersection2d || !sketchPathToNode) return
if (args.mouseEvent.which !== 1) return
const { intersectionPoint } = args
if (!intersectionPoint?.twoD || !sketchPathToNode) return
const { modifiedAst } = addStartProfileAt(
kclManager.ast,
sketchPathToNode,
[intersection2d.x, intersection2d.y]
[intersectionPoint.twoD.x, intersectionPoint.twoD.y]
)
await kclManager.updateAst(modifiedAst, false)
sceneEntitiesManager.removeIntersectionPlane()

View File

@ -1990,7 +1990,7 @@ dependencies = [
[[package]]
name = "kittycad-execution-plan"
version = "0.1.0"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#29086e1079adb82b6427639a779dc58eabcd7f78"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#9c96767289139f03036c2ba40f889f974ca3e976"
dependencies = [
"bytes",
"insta",
@ -2009,7 +2009,7 @@ dependencies = [
[[package]]
name = "kittycad-execution-plan-macros"
version = "0.1.6"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#29086e1079adb82b6427639a779dc58eabcd7f78"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#9c96767289139f03036c2ba40f889f974ca3e976"
dependencies = [
"proc-macro2",
"quote",
@ -2019,7 +2019,7 @@ dependencies = [
[[package]]
name = "kittycad-execution-plan-traits"
version = "0.1.11"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#29086e1079adb82b6427639a779dc58eabcd7f78"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#9c96767289139f03036c2ba40f889f974ca3e976"
dependencies = [
"serde",
"thiserror",
@ -2028,8 +2028,8 @@ dependencies = [
[[package]]
name = "kittycad-modeling-cmds"
version = "0.1.26"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#29086e1079adb82b6427639a779dc58eabcd7f78"
version = "0.1.27"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#9c96767289139f03036c2ba40f889f974ca3e976"
dependencies = [
"anyhow",
"chrono",
@ -2056,8 +2056,8 @@ dependencies = [
[[package]]
name = "kittycad-modeling-cmds-macros"
version = "0.1.1"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#29086e1079adb82b6427639a779dc58eabcd7f78"
version = "0.1.2"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#9c96767289139f03036c2ba40f889f974ca3e976"
dependencies = [
"proc-macro2",
"quote",
@ -2067,7 +2067,7 @@ dependencies = [
[[package]]
name = "kittycad-modeling-session"
version = "0.1.0"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#29086e1079adb82b6427639a779dc58eabcd7f78"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#9c96767289139f03036c2ba40f889f974ca3e976"
dependencies = [
"futures",
"kittycad",

View File

@ -105,6 +105,10 @@ impl BindingScope {
"startSketchAt".into(),
EpBinding::from(KclFunction::StartSketchAt(native_functions::sketch::StartSketchAt)),
),
(
"lineTo".into(),
EpBinding::from(KclFunction::LineTo(native_functions::sketch::LineTo)),
),
]),
parent: None,
}

View File

@ -252,6 +252,7 @@ impl Planner {
} = match callee {
KclFunction::Id(f) => f.call(&mut self.next_addr, args)?,
KclFunction::StartSketchAt(f) => f.call(&mut self.next_addr, args)?,
KclFunction::LineTo(f) => f.call(&mut self.next_addr, args)?,
KclFunction::Add(f) => f.call(&mut self.next_addr, args)?,
KclFunction::UserDefined(f) => {
let UserDefinedFunction {
@ -619,6 +620,7 @@ impl Eq for UserDefinedFunction {}
enum KclFunction {
Id(native_functions::Id),
StartSketchAt(native_functions::sketch::StartSketchAt),
LineTo(native_functions::sketch::LineTo),
Add(native_functions::Add),
UserDefined(UserDefinedFunction),
}

View File

@ -4,4 +4,4 @@ pub mod helpers;
pub mod stdlib_functions;
pub mod types;
pub use stdlib_functions::StartSketchAt;
pub use stdlib_functions::{LineTo, StartSketchAt};

View File

@ -1,4 +1,4 @@
use kittycad_execution_plan::{api_request::ApiRequest, Instruction};
use kittycad_execution_plan::{api_request::ApiRequest, Destination, Instruction};
use kittycad_execution_plan_traits::{Address, InMemory};
use kittycad_modeling_cmds::{id::ModelingCmdId, ModelingCmdEndpoint};
@ -120,11 +120,13 @@ pub fn arg_point2d(
instructions.extend([
Instruction::Copy {
source: single_binding(elements[0].clone(), "startSketchAt", "number", arg_number)?,
destination: start_x,
destination: Destination::Address(start_x),
length: 1,
},
Instruction::Copy {
source: single_binding(elements[1].clone(), "startSketchAt", "number", arg_number)?,
destination: start_y,
destination: Destination::Address(start_y),
length: 1,
},
Instruction::SetPrimitive {
address: start_z,

View File

@ -1,4 +1,4 @@
use kittycad_execution_plan::{api_request::ApiRequest, Instruction};
use kittycad_execution_plan::{api_request::ApiRequest, Destination, Instruction};
use kittycad_execution_plan_traits::{Address, InMemory, Value};
use kittycad_modeling_cmds::{
shared::{Point3d, Point4d},
@ -12,6 +12,82 @@ use super::{
};
use crate::{binding_scope::EpBinding, error::CompileError, native_functions::Callable, EvalPlan};
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct LineTo;
impl Callable for LineTo {
fn call(&self, next_addr: &mut Address, args: Vec<EpBinding>) -> Result<EvalPlan, CompileError> {
let mut instructions = Vec::new();
let fn_name = "lineTo";
// Get both required params.
let mut args_iter = args.into_iter();
let Some(to) = args_iter.next() else {
return Err(CompileError::NotEnoughArgs {
fn_name: fn_name.into(),
required: 2,
actual: 0,
});
};
let Some(sketch_group) = args_iter.next() else {
return Err(CompileError::NotEnoughArgs {
fn_name: fn_name.into(),
required: 2,
actual: 1,
});
};
// Check the type of both required params.
let to = arg_point2d(to, fn_name, &mut instructions, next_addr, 0)?;
let sg = single_binding(sketch_group, fn_name, "sketch group", 1)?;
let id = Uuid::new_v4();
let start_of_line = next_addr.offset(1);
let length_of_3d_point = Point3d::<f64>::default().into_parts().len();
instructions.extend([
// Push the `to` 2D point onto the stack.
Instruction::Copy {
source: to,
length: 2,
destination: Destination::StackPush,
},
// Make it a 3D point.
Instruction::StackExtend { data: vec![0.0.into()] },
// Append the new path segment to memory.
// First comes its tag.
Instruction::SetPrimitive {
address: start_of_line,
value: "Line".to_owned().into(),
},
// Then its end
Instruction::StackPop {
destination: Some(start_of_line + 1),
},
// Then its `relative` field.
Instruction::SetPrimitive {
address: start_of_line + 1 + length_of_3d_point,
value: false.into(),
},
// Send the ExtendPath request
Instruction::ApiRequest(ApiRequest {
endpoint: ModelingCmdEndpoint::ExtendPath,
store_response: None,
arguments: vec![
// Path ID
InMemory::Address(sg + SketchGroup::path_id_offset()),
// Segment
InMemory::Address(start_of_line),
],
cmd_id: id.into(),
}),
]);
// TODO: Create a new SketchGroup from the old one + add the new path, then store it.
Ok(EvalPlan {
instructions,
binding: EpBinding::Single(Address::ZERO + 9999),
})
}
}
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct StartSketchAt;

View File

@ -1,4 +1,4 @@
use kittycad_execution_plan::Instruction;
use kittycad_execution_plan::{Destination, Instruction};
use kittycad_execution_plan_macros::ExecutionPlanValue;
use kittycad_execution_plan_traits::{Address, Value};
use kittycad_modeling_cmds::shared::{Point2d, Point3d, Point4d};
@ -7,6 +7,8 @@ use uuid::Uuid;
/// A sketch group is a collection of paths.
#[derive(Clone, ExecutionPlanValue)]
pub struct SketchGroup {
// NOTE to developers
// Do NOT reorder these fields without updating the _offset() methods below.
/// The id of the sketch group.
pub id: Uuid,
/// What the sketch is on (can be a plane or a face).
@ -26,6 +28,10 @@ pub struct SketchGroup {
}
impl SketchGroup {
/// Get the offset for the `id` field.
pub fn path_id_offset() -> usize {
0
}
pub fn set_base_path(&self, sketch_group: Address, start_point: Address, tag: Option<Address>) -> Vec<Instruction> {
let base_path_addr = sketch_group
+ self.id.into_parts().len()
@ -39,21 +45,24 @@ impl SketchGroup {
// Copy over the `from` field.
Instruction::Copy {
source: start_point,
destination: base_path_addr,
destination: Destination::Address(base_path_addr),
length: 1,
},
// Copy over the `to` field.
Instruction::Copy {
source: start_point,
destination: base_path_addr + self.path_first.from.into_parts().len(),
destination: Destination::Address(base_path_addr + self.path_first.from.into_parts().len()),
length: 1,
},
];
if let Some(tag) = tag {
// Copy over the `name` field.
out.push(Instruction::Copy {
source: tag,
destination: base_path_addr
+ self.path_first.from.into_parts().len()
+ self.path_first.to.into_parts().len(),
destination: Destination::Address(
base_path_addr + self.path_first.from.into_parts().len() + self.path_first.to.into_parts().len(),
),
length: 1,
});
}
out

View File

@ -1048,15 +1048,30 @@ fn store_object_with_array_property() {
#[tokio::test]
async fn stdlib_cube_partial() {
let program = r#"
let cube = startSketchAt([22.0, 33.0])
let cube = startSketchAt([0.0, 0.0])
|> lineTo([4.0, 0.0], %)
"#;
let (plan, _scope) = must_plan(program);
std::fs::write("stdlib_cube_partial.json", serde_json::to_string_pretty(&plan).unwrap()).unwrap();
let (_plan, _scope) = must_plan(program);
let ast = kcl_lib::parser::Parser::new(kcl_lib::token::lexer(program))
.ast()
.unwrap();
let mem = crate::execute(ast, Some(test_client().await)).await.unwrap();
dbg!(mem);
let client = test_client().await;
let _mem = crate::execute(ast, Some(client)).await.unwrap();
// use kittycad_modeling_cmds::{each_cmd, ok_response::OkModelingCmdResponse, ImageFormat, ModelingCmd};
// let out = client
// .run_command(
// uuid::Uuid::new_v4().into(),
// each_cmd::TakeSnapshot {
// format: ImageFormat::Png,
// },
// )
// .await
// .unwrap();
// let out = match out {
// OkModelingCmdResponse::TakeSnapshot(b) => b,
// other => panic!("wrong output: {other:?}"),
// };
// let out: Vec<u8> = out.contents.into();
}
async fn test_client() -> Session {