SketchOnFace UI (#1664)

* always enter edit mode

* initial blocking of extra code-mirror updates

* dry out code

* rejig selections

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* clean up

* stream clean up

* update export

* sketch mode can be entered and exited for extrude faces

But has bugs

* startSketchOn working in some cases, editsketch animation working but not orientation of instersection plane etc

* Revert "A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)"

This reverts commit 406fca4c55.

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* remove comment

* add sketch on face e2e test

* tweenCamToNegYAxis should respect reduced motion

* initial sketch on face working with test

* remove temporary toolbar button and xState flow

* un-used vars

* snapshot test tweak

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* type tidy up

* Revert "A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)"

This reverts commit c39b8ebf95.

* Revert "A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)"

This reverts commit fecf6f490a.

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* rename

* sketch on sketch on sketch

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* typo

* startSketchOn Endcaps

end works, start is weird still

* clear selections for entity_ids that are not recognised

* fix sketch on end cap of second order extrustion

* tiny clean up

* fix sketch on close segment/face

* clean up 'lastCodeMirrorSelectionUpdatedFromScene'

* add code mode test for sketchOnExtrudedFace

* make end cap selection more robust

* update js artifacts for extrudes

* update kcl docs

* clean up

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Kurt Hutten
2024-03-22 10:23:04 +11:00
committed by GitHub
parent e773e932b0
commit 0e916cfd5b
63 changed files with 1860 additions and 720 deletions

View File

@ -5,7 +5,6 @@ import {
Group,
Intersection,
LineCurve3,
Matrix4,
Mesh,
MeshBasicMaterial,
Object3D,
@ -37,7 +36,7 @@ import {
Y_AXIS,
YZ_PLANE,
} from './sceneInfra'
import { isQuaternionVertical } from './helpers'
import { isQuaternionVertical, quaternionFromUpNForward } from './helpers'
import {
CallExpression,
getTangentialArcToInfo,
@ -55,7 +54,7 @@ import {
} from 'lang/wasm'
import { kclManager } from 'lang/KclSingleton'
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
import { executeAst } from 'useStore'
import { executeAst, useStore } from 'useStore'
import { engineCommandManager } from 'lang/std/engineConnection'
import {
createArcGeometry,
@ -70,16 +69,22 @@ import {
changeSketchArguments,
updateStartProfileAtArgs,
} from 'lang/std/sketch'
import { isReducedMotion, throttle } from 'lib/utils'
import { throttle } from 'lib/utils'
import {
createArrayExpression,
createCallExpressionStdLib,
createLiteral,
createPipeSubstitution,
} from 'lang/modifyAst'
import { getEventForSegmentSelection } from 'lib/selections'
import {
getEventForSegmentSelection,
sendSelectEventToEngine,
} from 'lib/selections'
import { getTangentPointFromPreviousArc } from 'lib/utils2d'
import { createGridHelper, orthoScale, perspScale } from './helpers'
import { Models } from '@kittycad/lib'
import { v4 as uuidv4 } from 'uuid'
import { SketchDetails } from 'machines/modelingMachine'
type DraftSegment = 'line' | 'tangentialArcTo'
@ -164,7 +169,7 @@ class SceneEntities {
console.warn('createIntersectionPlane called when it already exists')
return
}
const hundredM = 1000000
const hundredM = 100_0000
const planeGeometry = new PlaneGeometry(hundredM, hundredM)
const planeMaterial = new MeshBasicMaterial({
color: 0xff0000,
@ -178,7 +183,12 @@ class SceneEntities {
this.intersectionPlane.layers.set(INTERSECTION_PLANE_LAYER)
this.scene.add(this.intersectionPlane)
}
createSketchAxis(sketchPathToNode: PathToNode) {
createSketchAxis(
sketchPathToNode: PathToNode,
forward: [number, number, number],
up: [number, number, number],
sketchPosition?: [number, number, number]
) {
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
const baseXColor = 0x000055
const baseYColor = 0x550000
@ -238,14 +248,12 @@ class SceneEntities {
child.layers.set(SKETCH_LAYER)
})
const quat = quaternionFromSketchGroup(
sketchGroupFromPathToNode({
pathToNode: sketchPathToNode,
ast: kclManager.ast,
programMemory: kclManager.programMemory,
})
const quat = quaternionFromUpNForward(
new Vector3(...up),
new Vector3(...forward)
)
this.axisGroup.setRotationFromQuaternion(quat)
sketchPosition && this.axisGroup.position.set(...sketchPosition)
this.scene.add(this.axisGroup)
}
removeIntersectionPlane() {
@ -258,10 +266,16 @@ class SceneEntities {
ast,
// is draft line assumes the last segment is a draft line, and mods it as the user moves the mouse
draftSegment,
forward,
up,
position,
}: {
sketchPathToNode: PathToNode
ast?: Program
draftSegment?: DraftSegment
forward: [number, number, number]
up: [number, number, number]
position?: [number, number, number]
}) {
sceneInfra.resetMouseListeners()
this.createIntersectionPlane()
@ -286,6 +300,7 @@ class SceneEntities {
if (!Array.isArray(sketchGroup?.value)) return
this.sceneProgramMemory = programMemory
const group = new Group()
position && group.position.set(...position)
group.userData = {
type: SKETCH_GROUP_SEGMENTS,
pathToNode: sketchPathToNode,
@ -377,13 +392,18 @@ class SceneEntities {
this.activeSegments[JSON.stringify(segPathToNode)] = seg
})
this.currentSketchQuaternion = quaternionFromSketchGroup(sketchGroup)
this.currentSketchQuaternion = quaternionFromUpNForward(
new Vector3(...up),
new Vector3(...forward)
)
group.setRotationFromQuaternion(this.currentSketchQuaternion)
this.intersectionPlane &&
this.intersectionPlane.setRotationFromQuaternion(
this.currentSketchQuaternion
)
this.intersectionPlane &&
position &&
this.intersectionPlane.position.set(...position)
this.scene.add(group)
if (!draftSegment) {
sceneInfra.setCallbacks({
@ -453,7 +473,13 @@ class SceneEntities {
kclManager.executeAstMock(modifiedAst, { updates: 'code' })
await this.tearDownSketch({ removeAxis: false })
this.setupSketch({ sketchPathToNode, draftSegment })
this.setupSketch({
sketchPathToNode,
draftSegment,
forward,
up,
position,
})
},
onMove: (args) => {
this.onDragSegment({
@ -476,21 +502,37 @@ class SceneEntities {
}
updateAstAndRejigSketch = async (
sketchPathToNode: PathToNode,
modifiedAst: Program
modifiedAst: Program,
forward: [number, number, number],
up: [number, number, number],
origin: [number, number, number]
) => {
await kclManager.updateAst(modifiedAst, false)
await this.tearDownSketch({ removeAxis: false })
this.setupSketch({ sketchPathToNode })
this.setupSketch({ sketchPathToNode, forward, up, position: origin })
}
setUpDraftArc = async (sketchPathToNode: PathToNode) => {
setUpDraftArc = async (
sketchPathToNode: PathToNode,
forward: [number, number, number],
up: [number, number, number]
) => {
await this.tearDownSketch({ removeAxis: false })
await new Promise((resolve) => setTimeout(resolve, 100))
this.setupSketch({ sketchPathToNode, draftSegment: 'tangentialArcTo' })
this.setupSketch({
sketchPathToNode,
draftSegment: 'tangentialArcTo',
forward,
up,
})
}
setUpDraftLine = async (sketchPathToNode: PathToNode) => {
setUpDraftLine = async (
sketchPathToNode: PathToNode,
forward: [number, number, number],
up: [number, number, number]
) => {
await this.tearDownSketch({ removeAxis: false })
await new Promise((resolve) => setTimeout(resolve, 100))
this.setupSketch({ sketchPathToNode, draftSegment: 'line' })
this.setupSketch({ sketchPathToNode, draftSegment: 'line', forward, up })
}
onDraftLineMouseMove = () => {}
prepareTruncatedMemoryAndAst = (
@ -785,10 +827,10 @@ class SceneEntities {
}
}
async animateAfterSketch() {
if (isReducedMotion()) {
sceneInfra.camControls.usePerspectiveCamera()
return
}
// if (isReducedMotion()) {
// sceneInfra.camControls.usePerspectiveCamera()
// return
// }
await sceneInfra.camControls.animateToPerspective()
}
removeSketchGrid() {
@ -853,26 +895,81 @@ class SceneEntities {
const type: DefaultPlane = selected.userData.type
selected.material.color = defaultPlaneColor(type)
},
onClick: (args) => {
onClick: async (args) => {
const checkExtrudeFaceClick = async (): Promise<boolean> => {
const { streamDimensions } = useStore.getState()
const { entity_id } = await sendSelectEventToEngine(
args?.mouseEvent,
document.getElementById('video-stream') as HTMLVideoElement,
streamDimensions
)
if (!entity_id) return false
const artifact = engineCommandManager.artifactMap[entity_id]
if (artifact?.commandType !== 'solid3d_get_extrusion_face_info')
return false
const faceInfo: Models['FaceIsPlanar_type'] = (
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'face_is_planar',
object_id: entity_id,
},
})
)?.data?.data
if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis)
return false
const { z_axis, origin, y_axis } = faceInfo
const pathToNode = getNodePathFromSourceRange(
kclManager.ast,
artifact.range
)
sceneInfra.modelingSend({
type: 'Select default plane',
data: {
type: 'extrudeFace',
zAxis: [z_axis.x, z_axis.y, z_axis.z],
yAxis: [y_axis.x, y_axis.y, y_axis.z],
position: [origin.x, origin.y, origin.z].map(
(num) => num / sceneInfra._baseUnitMultiplier
) as [number, number, number],
extrudeSegmentPathToNode: pathToNode,
cap:
artifact?.additionalData?.type === 'cap'
? artifact.additionalData.info
: 'none',
},
})
return true
}
if (await checkExtrudeFaceClick()) return
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]
let zAxis: [number, number, number] = posNorm ? [0, 0, 1] : [0, 0, -1]
let yAxis: [number, number, number] = [0, 1, 0]
if (type === YZ_PLANE) {
planeString = posNorm ? 'YZ' : '-YZ'
normal = posNorm ? [1, 0, 0] : [-1, 0, 0]
zAxis = posNorm ? [1, 0, 0] : [-1, 0, 0]
yAxis = [0, 0, 1]
} else if (type === XZ_PLANE) {
planeString = posNorm ? 'XZ' : '-XZ'
normal = posNorm ? [0, 1, 0] : [0, -1, 0]
zAxis = posNorm ? [0, 1, 0] : [0, -1, 0]
yAxis = [0, 0, 1]
}
sceneInfra.modelingSend({
type: 'Select default plane',
data: {
type: 'defaultPlane',
plane: planeString,
normal,
zAxis,
yAxis,
},
})
},
@ -1002,33 +1099,10 @@ export function sketchGroupFromPathToNode({
pathToNode,
'VariableDeclarator'
).node
// console.trace('where from?')
return programMemory.root[varDec?.id?.name || ''] as SketchGroup
}
export function quaternionFromSketchGroup(
sketchGroup: SketchGroup
): Quaternion {
// TODO figure what is happening in the executor that it's some times returning
// [x,y,z] and sometimes {x,y,z}
if (!sketchGroup?.zAxis) {
// sometimes sketchGroup is undefined,
// I don't quiet understand the circumstances yet
// and it's very intermittent so leaving this here for now
console.log('no zAxis', sketchGroup)
console.trace('no zAxis')
}
const zAxisVec = massageFormats(sketchGroup?.zAxis)
const yAxisVec = massageFormats(sketchGroup?.yAxis)
const xAxisVec = new Vector3().crossVectors(yAxisVec, zAxisVec).normalize()
let yAxisVecNormalized = yAxisVec.clone().normalize()
let zAxisVecNormalized = zAxisVec.clone().normalize()
let rotationMatrix = new Matrix4()
rotationMatrix.makeBasis(xAxisVec, yAxisVecNormalized, zAxisVecNormalized)
return new Quaternion().setFromRotationMatrix(rotationMatrix)
}
function colorSegment(object: any, color: number) {
const segmentHead = getParentGroup(object, [ARROWHEAD, PROFILE_START])
if (segmentHead) {
@ -1063,10 +1137,68 @@ export function getSketchQuaternion(
programMemory: kclManager.programMemory,
})
const zAxis = sketchGroup?.zAxis || sketchNormalBackUp
return getQuaternionFromZAxis(massageFormats(zAxis))
}
export async function getSketchOrientationDetails(
sketchPathToNode: PathToNode
): Promise<{
quat: Quaternion
sketchDetails: SketchDetails
}> {
const sketchGroup = sketchGroupFromPathToNode({
pathToNode: sketchPathToNode,
ast: kclManager.ast,
programMemory: kclManager.programMemory,
})
if (sketchGroup.on.type === 'plane') {
const zAxis = sketchGroup?.zAxis
return {
quat: getQuaternionFromZAxis(massageFormats(zAxis)),
sketchDetails: {
sketchPathToNode,
zAxis: [zAxis.x, zAxis.y, zAxis.z],
yAxis: [sketchGroup.yAxis.x, sketchGroup.yAxis.y, sketchGroup.yAxis.z],
origin: [0, 0, 0],
},
}
}
if (sketchGroup.on.type === 'face') {
const faceInfo: Models['FaceIsPlanar_type'] = (
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'face_is_planar',
object_id: sketchGroup.on.faceId,
},
})
)?.data?.data
if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis)
throw new Error('faceInfo')
const { z_axis, y_axis, origin } = faceInfo
const quaternion = quaternionFromUpNForward(
new Vector3(y_axis.x, y_axis.y, y_axis.z),
new Vector3(z_axis.x, z_axis.y, z_axis.z)
)
return {
quat: quaternion,
sketchDetails: {
sketchPathToNode,
zAxis: [z_axis.x, z_axis.y, z_axis.z],
yAxis: [y_axis.x, y_axis.y, y_axis.z],
origin: [origin.x, origin.y, origin.z],
},
}
}
throw new Error(
'sketchGroup.on.type not recognized, has a new type been added?'
)
}
export function getQuaternionFromZAxis(zAxis: Vector3): Quaternion {
const dummyCam = new PerspectiveCamera()
dummyCam.up.set(0, 0, 1)
const _zAxis = massageFormats(zAxis)
dummyCam.position.copy(_zAxis)
dummyCam.position.copy(zAxis)
dummyCam.lookAt(0, 0, 0)
dummyCam.updateMatrix()
const quaternion = dummyCam.quaternion.clone()
@ -1075,7 +1207,7 @@ export function getSketchQuaternion(
// because vertical quaternions are a gimbal lock, for the orbit controls
// it's best to set them explicitly to the vertical position with a known good camera up
if (isVert && _zAxis.z < 0) {
if (isVert && zAxis.z < 0) {
quaternion.set(0, 1, 0, 0)
} else if (isVert) {
quaternion.set(0, 0, 0, 1)