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 commit406fca4c55. * 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 commitc39b8ebf95. * Revert "A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)" This reverts commitfecf6f490a. * 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:
		@ -69,6 +69,8 @@ const part001 = startSketchOn('XY')
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
 | 
			
		||||
@ -69,6 +69,8 @@ const part001 = startSketchOn('XY')
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
 | 
			
		||||
@ -76,6 +76,8 @@ startSketchOn('XY')
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
@ -244,6 +246,8 @@ startSketchOn('XY')
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
 | 
			
		||||
@ -76,6 +76,8 @@ startSketchOn('XZ')
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
@ -244,6 +246,8 @@ startSketchOn('XZ')
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
 | 
			
		||||
@ -77,6 +77,8 @@ startSketchOn('YZ')
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
@ -245,6 +247,8 @@ startSketchOn('YZ')
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
 | 
			
		||||
@ -82,6 +82,8 @@ const part001 = startSketchOn('XY')
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
@ -250,6 +252,8 @@ const part001 = startSketchOn('XY')
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
 | 
			
		||||
@ -76,6 +76,8 @@ startSketchOn('XY')
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
@ -244,6 +246,8 @@ startSketchOn('XY')
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
 | 
			
		||||
@ -75,6 +75,8 @@ startSketchOn('XY')
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
@ -243,6 +245,8 @@ startSketchOn('XY')
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
 | 
			
		||||
@ -86,6 +86,8 @@ startSketchOn('-YZ')
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
@ -254,6 +256,8 @@ startSketchOn('-YZ')
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
 | 
			
		||||
@ -79,6 +79,8 @@ startSketchOn('XY')
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
@ -247,6 +249,8 @@ startSketchOn('XY')
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
 | 
			
		||||
@ -71,6 +71,8 @@ const rectangle = startSketchOn('XY')
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
@ -135,6 +137,8 @@ const rectangle = startSketchOn('XY')
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
@ -302,6 +306,8 @@ const rectangle = startSketchOn('XY')
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
 | 
			
		||||
@ -72,6 +72,8 @@ startSketchOn('YZ')
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
@ -240,6 +242,8 @@ startSketchOn('YZ')
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
 | 
			
		||||
@ -67,6 +67,8 @@ startSketchOn('XY')
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
 | 
			
		||||
@ -68,6 +68,8 @@ const square = startSketchOn('XY')
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
@ -236,6 +238,8 @@ const square = startSketchOn('XY')
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
@ -403,6 +407,8 @@ const square = startSketchOn('XY')
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
 | 
			
		||||
@ -66,6 +66,8 @@ startSketchOn("YZ")
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
 | 
			
		||||
@ -66,6 +66,8 @@ startSketchOn("YZ")
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
 | 
			
		||||
@ -66,6 +66,8 @@ startSketchOn('-XY')
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
@ -234,6 +236,8 @@ startSketchOn('-XY')
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
 | 
			
		||||
@ -72,6 +72,8 @@ const part = rectShape([0, 0], 20, 20)
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
@ -240,6 +242,8 @@ const part = rectShape([0, 0], 20, 20)
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
 | 
			
		||||
@ -80,6 +80,8 @@ const part = startSketchOn('XY')
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
 | 
			
		||||
@ -77,6 +77,8 @@ const part = startSketchOn('XY')
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
 | 
			
		||||
@ -69,6 +69,8 @@ const part001 = startSketchOn('XY')
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
 | 
			
		||||
@ -67,6 +67,8 @@ startSketchOn("YZ")
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
 | 
			
		||||
@ -67,6 +67,8 @@ startSketchOn("YZ")
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
 | 
			
		||||
@ -67,6 +67,8 @@ startSketchOn("YZ")
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
 | 
			
		||||
@ -57,6 +57,8 @@ startSketchOn('XY')
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
@ -128,6 +130,8 @@ startSketchOn('XY')
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
 | 
			
		||||
@ -65,6 +65,8 @@ startSketchAt([0, 0])
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
 | 
			
		||||
@ -254,6 +254,8 @@ string
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -75,6 +75,8 @@ startSketchOn('-YZ')
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
@ -243,6 +245,8 @@ startSketchOn('-YZ')
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
 | 
			
		||||
@ -65,6 +65,8 @@ startSketchOn('-YZ')
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
@ -233,6 +235,8 @@ startSketchOn('-YZ')
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
 | 
			
		||||
@ -66,6 +66,8 @@ startSketchOn('YZ')
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
@ -234,6 +236,8 @@ startSketchOn('YZ')
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
 | 
			
		||||
@ -66,6 +66,8 @@ startSketchOn('XY')
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
@ -234,6 +236,8 @@ startSketchOn('XY')
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
 | 
			
		||||
@ -66,6 +66,8 @@ startSketchOn('XY')
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
@ -234,6 +236,8 @@ startSketchOn('XY')
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
 | 
			
		||||
@ -67,6 +67,8 @@ startSketchOn('XZ')
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
@ -235,6 +237,8 @@ startSketchOn('XZ')
 | 
			
		||||
},
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
	// the face id the sketch is on
 | 
			
		||||
	faceId: uuid,
 | 
			
		||||
	// The id of the face.
 | 
			
		||||
	id: uuid,
 | 
			
		||||
	// The original sketch group id of the object we are sketching on.
 | 
			
		||||
 | 
			
		||||
@ -20,6 +20,8 @@ const commonPoints = {
 | 
			
		||||
  startAt: '[9.06, -12.22]',
 | 
			
		||||
  num1: 9.14,
 | 
			
		||||
  num2: 18.2,
 | 
			
		||||
  // num1: 9.64,
 | 
			
		||||
  // num2: 19.19,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
test.beforeEach(async ({ context, page }) => {
 | 
			
		||||
@ -76,6 +78,7 @@ test('Basic sketch', async ({ page }) => {
 | 
			
		||||
  await expect(page.locator('.cm-content')).toHaveText(
 | 
			
		||||
    `const part001 = startSketchOn('-XZ')`
 | 
			
		||||
  )
 | 
			
		||||
  await u.closeDebugPanel()
 | 
			
		||||
 | 
			
		||||
  await page.waitForTimeout(300) // TODO detect animation ending, or disable animation
 | 
			
		||||
 | 
			
		||||
@ -86,7 +89,6 @@ test('Basic sketch', async ({ page }) => {
 | 
			
		||||
  |> startProfileAt(${commonPoints.startAt}, %)`)
 | 
			
		||||
  await page.waitForTimeout(100)
 | 
			
		||||
 | 
			
		||||
  await u.closeDebugPanel()
 | 
			
		||||
  await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
 | 
			
		||||
  await page.waitForTimeout(100)
 | 
			
		||||
 | 
			
		||||
@ -625,7 +627,7 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
 | 
			
		||||
  const emptySpaceClick = () =>
 | 
			
		||||
    page.mouse.click(728, 343).then(() => page.waitForTimeout(100))
 | 
			
		||||
  const topHorzSegmentClick = () =>
 | 
			
		||||
    page.mouse.click(709, 289).then(() => page.waitForTimeout(100))
 | 
			
		||||
    page.mouse.click(709, 290).then(() => page.waitForTimeout(100))
 | 
			
		||||
  const bottomHorzSegmentClick = () =>
 | 
			
		||||
    page.mouse.click(767, 396).then(() => page.waitForTimeout(100))
 | 
			
		||||
 | 
			
		||||
@ -640,13 +642,12 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
 | 
			
		||||
  await page.waitForTimeout(700) // wait for animation
 | 
			
		||||
 | 
			
		||||
  const startXPx = 600
 | 
			
		||||
  await u.closeDebugPanel()
 | 
			
		||||
  await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
 | 
			
		||||
  await expect(page.locator('.cm-content'))
 | 
			
		||||
    .toHaveText(`const part001 = startSketchOn('-XZ')
 | 
			
		||||
  |> startProfileAt(${commonPoints.startAt}, %)`)
 | 
			
		||||
 | 
			
		||||
  await u.closeDebugPanel()
 | 
			
		||||
 | 
			
		||||
  await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
 | 
			
		||||
 | 
			
		||||
  await expect(page.locator('.cm-content'))
 | 
			
		||||
@ -727,13 +728,18 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
 | 
			
		||||
    await emptySpaceClick()
 | 
			
		||||
 | 
			
		||||
    // select segment in editor than another segment in scene and check there are two cursors
 | 
			
		||||
    await page.getByText(`  |> line([-${commonPoints.num2}, 0], %)`).click()
 | 
			
		||||
    await page.waitForTimeout(300)
 | 
			
		||||
    await page.keyboard.down('Shift')
 | 
			
		||||
    await expect(page.locator('.cm-cursor')).toHaveCount(1)
 | 
			
		||||
    // TODO change this back to shift click in the scene, not cmd click in the editor
 | 
			
		||||
    await bottomHorzSegmentClick()
 | 
			
		||||
    await page.keyboard.up('Shift')
 | 
			
		||||
 | 
			
		||||
    await expect(page.locator('.cm-cursor')).toHaveCount(1)
 | 
			
		||||
 | 
			
		||||
    await page.keyboard.down(process.platform === 'linux' ? 'Control' : 'Meta')
 | 
			
		||||
    await page.waitForTimeout(100)
 | 
			
		||||
    await page.getByText(`  |> line([-${commonPoints.num2}, 0], %)`).click()
 | 
			
		||||
 | 
			
		||||
    await expect(page.locator('.cm-cursor')).toHaveCount(2)
 | 
			
		||||
    await page.waitForTimeout(500)
 | 
			
		||||
    await page.keyboard.up(process.platform === 'linux' ? 'Control' : 'Meta')
 | 
			
		||||
 | 
			
		||||
    // clear selection by clicking on nothing
 | 
			
		||||
    await emptySpaceClick()
 | 
			
		||||
@ -918,13 +924,13 @@ test('Can add multiple sketches', async ({ page }) => {
 | 
			
		||||
  await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
 | 
			
		||||
 | 
			
		||||
  const startXPx = 600
 | 
			
		||||
  await u.closeDebugPanel()
 | 
			
		||||
  await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
 | 
			
		||||
  await expect(page.locator('.cm-content'))
 | 
			
		||||
    .toHaveText(`const part001 = startSketchOn('-XZ')
 | 
			
		||||
  |> startProfileAt(${commonPoints.startAt}, %)`)
 | 
			
		||||
  await page.waitForTimeout(100)
 | 
			
		||||
 | 
			
		||||
  await u.closeDebugPanel()
 | 
			
		||||
  await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
 | 
			
		||||
  await page.waitForTimeout(100)
 | 
			
		||||
 | 
			
		||||
@ -1372,10 +1378,129 @@ test('Snap to close works (at any scale)', async ({ page }) => {
 | 
			
		||||
  ) => `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}], %)
 | 
			
		||||
|> line([0, -${roundOff(scale * 175.36) + fudge}], %)
 | 
			
		||||
|> close(%)`
 | 
			
		||||
 | 
			
		||||
  await doSnapAtDifferentScales([0, 100, 100], codeTemplate(0.01, 0.01))
 | 
			
		||||
 | 
			
		||||
  await doSnapAtDifferentScales([0, 10000, 10000], codeTemplate())
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
test('Sketch on face', async ({ page, context }) => {
 | 
			
		||||
  const u = getUtils(page)
 | 
			
		||||
  await context.addInitScript(async () => {
 | 
			
		||||
    localStorage.setItem(
 | 
			
		||||
      'persistCode',
 | 
			
		||||
      `const part001 = startSketchOn('-XZ')
 | 
			
		||||
  |> startProfileAt([3.29, 7.86], %)
 | 
			
		||||
  |> line([2.48, 2.44], %)
 | 
			
		||||
  |> line([2.66, 1.17], %)
 | 
			
		||||
  |> line([3.75, 0.46], %)
 | 
			
		||||
  |> line([4.99, -0.46], %)
 | 
			
		||||
  |> line([3.3, -2.12], %)
 | 
			
		||||
  |> line([2.16, -3.33], %)
 | 
			
		||||
  |> line([0.85, -3.08], %)
 | 
			
		||||
  |> line([-0.18, -3.36], %)
 | 
			
		||||
  |> line([-3.86, -2.73], %)
 | 
			
		||||
  |> line([-17.67, 0.85], %)
 | 
			
		||||
  |> close(%)
 | 
			
		||||
  |> extrude(5 + 7, %)`
 | 
			
		||||
    )
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  await page.setViewportSize({ width: 1200, height: 500 })
 | 
			
		||||
  await page.goto('/')
 | 
			
		||||
  await u.waitForAuthSkipAppStart()
 | 
			
		||||
  await expect(
 | 
			
		||||
    page.getByRole('button', { name: 'Start Sketch' })
 | 
			
		||||
  ).not.toBeDisabled()
 | 
			
		||||
 | 
			
		||||
  await page.getByRole('button', { name: 'Start Sketch' }).click()
 | 
			
		||||
 | 
			
		||||
  let previousCodeContent = await page.locator('.cm-content').innerText()
 | 
			
		||||
 | 
			
		||||
  await page.mouse.click(793, 133)
 | 
			
		||||
 | 
			
		||||
  const firstClickPosition = [612, 238]
 | 
			
		||||
  const secondClickPosition = [661, 242]
 | 
			
		||||
  const thirdClickPosition = [609, 267]
 | 
			
		||||
 | 
			
		||||
  await page.waitForTimeout(300)
 | 
			
		||||
 | 
			
		||||
  await page.mouse.click(firstClickPosition[0], firstClickPosition[1])
 | 
			
		||||
  await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
 | 
			
		||||
  previousCodeContent = await page.locator('.cm-content').innerText()
 | 
			
		||||
 | 
			
		||||
  await page.mouse.click(secondClickPosition[0], secondClickPosition[1])
 | 
			
		||||
  await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
 | 
			
		||||
  previousCodeContent = await page.locator('.cm-content').innerText()
 | 
			
		||||
 | 
			
		||||
  await page.mouse.click(thirdClickPosition[0], thirdClickPosition[1])
 | 
			
		||||
  await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
 | 
			
		||||
  previousCodeContent = await page.locator('.cm-content').innerText()
 | 
			
		||||
 | 
			
		||||
  await page.mouse.click(firstClickPosition[0], firstClickPosition[1])
 | 
			
		||||
  await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
 | 
			
		||||
  previousCodeContent = await page.locator('.cm-content').innerText()
 | 
			
		||||
 | 
			
		||||
  await expect(page.locator('.cm-content'))
 | 
			
		||||
    .toContainText(`const part002 = startSketchOn(part001, 'seg01')
 | 
			
		||||
  |> startProfileAt([1.03, 1.03], %)
 | 
			
		||||
  |> line([4.18, -0.35], %)
 | 
			
		||||
  |> line([-4.44, -2.13], %)
 | 
			
		||||
  |> close(%)`)
 | 
			
		||||
 | 
			
		||||
  await u.openAndClearDebugPanel()
 | 
			
		||||
  await page.getByRole('button', { name: 'Exit Sketch' }).click()
 | 
			
		||||
  await u.expectCmdLog('[data-message-type="execution-done"]')
 | 
			
		||||
 | 
			
		||||
  await u.updateCamPosition([1049, 239, 686])
 | 
			
		||||
  await u.closeDebugPanel()
 | 
			
		||||
 | 
			
		||||
  await page.getByText('startProfileAt([1.03, 1.03], %)').click()
 | 
			
		||||
  await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible()
 | 
			
		||||
  await page.getByRole('button', { name: 'Edit Sketch' }).click()
 | 
			
		||||
  await page.waitForTimeout(200)
 | 
			
		||||
 | 
			
		||||
  const pointToDragFirst = [691, 237]
 | 
			
		||||
  await page.mouse.move(pointToDragFirst[0], pointToDragFirst[1])
 | 
			
		||||
  await page.mouse.down()
 | 
			
		||||
  await page.mouse.move(pointToDragFirst[0] - 20, pointToDragFirst[1], {
 | 
			
		||||
    steps: 5,
 | 
			
		||||
  })
 | 
			
		||||
  await page.mouse.up()
 | 
			
		||||
  await page.waitForTimeout(100)
 | 
			
		||||
  await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
 | 
			
		||||
  previousCodeContent = await page.locator('.cm-content').innerText()
 | 
			
		||||
 | 
			
		||||
  await expect(page.locator('.cm-content'))
 | 
			
		||||
    .toContainText(`const part002 = startSketchOn(part001, 'seg01')
 | 
			
		||||
|> startProfileAt([1.03, 1.03], %)
 | 
			
		||||
|> line([2.81, -0.33], %)
 | 
			
		||||
|> line([-4.44, -2.13], %)
 | 
			
		||||
|> close(%)`)
 | 
			
		||||
 | 
			
		||||
  // exit sketch
 | 
			
		||||
  await u.openAndClearDebugPanel()
 | 
			
		||||
  await page.getByRole('button', { name: 'Exit Sketch' }).click()
 | 
			
		||||
  await u.expectCmdLog('[data-message-type="execution-done"]')
 | 
			
		||||
 | 
			
		||||
  await page.getByText('startProfileAt([1.03, 1.03], %)').click()
 | 
			
		||||
 | 
			
		||||
  await expect(page.getByRole('button', { name: 'Extrude' })).not.toBeDisabled()
 | 
			
		||||
  await page.getByRole('button', { name: 'Extrude' }).click()
 | 
			
		||||
 | 
			
		||||
  await expect(page.getByTestId('command-bar')).toBeVisible()
 | 
			
		||||
 | 
			
		||||
  await page.keyboard.press('Enter')
 | 
			
		||||
  await expect(page.getByText('Confirm Extrude')).toBeVisible()
 | 
			
		||||
  await page.keyboard.press('Enter')
 | 
			
		||||
 | 
			
		||||
  await expect(page.locator('.cm-content'))
 | 
			
		||||
    .toContainText(`const part002 = startSketchOn(part001, 'seg01')
 | 
			
		||||
|> startProfileAt([1.03, 1.03], %)
 | 
			
		||||
|> line([2.81, -0.33], %)
 | 
			
		||||
|> line([-4.44, -2.13], %)
 | 
			
		||||
|> close(%)
 | 
			
		||||
|> extrude(5 + 7, %)`)
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
@ -612,7 +612,7 @@ test('Client side scene scale should match engine scale mm', async ({
 | 
			
		||||
  await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
 | 
			
		||||
  await expect(page.locator('.cm-content'))
 | 
			
		||||
    .toHaveText(`const part001 = startSketchOn('-XZ')
 | 
			
		||||
  |> startProfileAt([230.03, -310.33], %)`)
 | 
			
		||||
  |> startProfileAt([230.03, -310.32], %)`)
 | 
			
		||||
  await page.waitForTimeout(100)
 | 
			
		||||
 | 
			
		||||
  await u.closeDebugPanel()
 | 
			
		||||
@ -622,7 +622,7 @@ test('Client side scene scale should match engine scale mm', async ({
 | 
			
		||||
 | 
			
		||||
  await expect(page.locator('.cm-content'))
 | 
			
		||||
    .toHaveText(`const part001 = startSketchOn('-XZ')
 | 
			
		||||
  |> startProfileAt([230.03, -310.33], %)
 | 
			
		||||
  |> startProfileAt([230.03, -310.32], %)
 | 
			
		||||
  |> line([232.2, 0], %)`)
 | 
			
		||||
 | 
			
		||||
  await page.getByRole('button', { name: 'Tangential Arc' }).click()
 | 
			
		||||
@ -632,7 +632,7 @@ test('Client side scene scale should match engine scale mm', async ({
 | 
			
		||||
 | 
			
		||||
  await expect(page.locator('.cm-content'))
 | 
			
		||||
    .toHaveText(`const part001 = startSketchOn('-XZ')
 | 
			
		||||
  |> startProfileAt([230.03, -310.33], %)
 | 
			
		||||
  |> startProfileAt([230.03, -310.32], %)
 | 
			
		||||
  |> line([232.2, 0], %)
 | 
			
		||||
  |> tangentialArcTo([694.43, -78.12], %)`)
 | 
			
		||||
 | 
			
		||||
@ -658,3 +658,48 @@ test('Client side scene scale should match engine scale mm', async ({
 | 
			
		||||
    maxDiffPixels: 100,
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
test('Sketch on face with none z-up', async ({ page, context }) => {
 | 
			
		||||
  const u = getUtils(page)
 | 
			
		||||
  await context.addInitScript(async () => {
 | 
			
		||||
    localStorage.setItem(
 | 
			
		||||
      'persistCode',
 | 
			
		||||
      `const part001 = startSketchOn('-XZ')
 | 
			
		||||
  |> startProfileAt([1.4, 2.47], %)
 | 
			
		||||
  |> line({ to: [9.31, 10.55], tag: 'seg01' }, %)
 | 
			
		||||
  |> line([11.91, -10.42], %)
 | 
			
		||||
  |> close(%)
 | 
			
		||||
  |> extrude(5 + 7, %)
 | 
			
		||||
const part002 = startSketchOn(part001, 'seg01')
 | 
			
		||||
  |> startProfileAt([-2.89, 1.82], %)
 | 
			
		||||
  |> line([4.68, 3.05], %)
 | 
			
		||||
  |> line({ to: [0, -7.79], tag: 'seg02' }, %)
 | 
			
		||||
  |> close(%)
 | 
			
		||||
  |> extrude(5 + 7, %)
 | 
			
		||||
`
 | 
			
		||||
    )
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  await page.setViewportSize({ width: 1200, height: 500 })
 | 
			
		||||
  await page.goto('/')
 | 
			
		||||
  await u.waitForAuthSkipAppStart()
 | 
			
		||||
  await expect(
 | 
			
		||||
    page.getByRole('button', { name: 'Start Sketch' })
 | 
			
		||||
  ).not.toBeDisabled()
 | 
			
		||||
 | 
			
		||||
  await page.getByRole('button', { name: 'Start Sketch' }).click()
 | 
			
		||||
  let previousCodeContent = await page.locator('.cm-content').innerText()
 | 
			
		||||
 | 
			
		||||
  // click at 641, 135
 | 
			
		||||
  await page.mouse.click(641, 135)
 | 
			
		||||
  await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
 | 
			
		||||
  previousCodeContent = await page.locator('.cm-content').innerText()
 | 
			
		||||
 | 
			
		||||
  await page.waitForTimeout(300)
 | 
			
		||||
 | 
			
		||||
  await expect(page).toHaveScreenshot({
 | 
			
		||||
    maxDiffPixels: 100,
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  await page.waitForTimeout(200)
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 52 KiB  | 
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 54 KiB  | 
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 75 KiB  | 
@ -425,6 +425,7 @@ export class CameraControls {
 | 
			
		||||
    if (this.camera instanceof OrthographicCamera) return
 | 
			
		||||
    const { x: px, y: py, z: pz } = this.camera.position
 | 
			
		||||
    const { x: qx, y: qy, z: qz, w: qw } = this.camera.quaternion
 | 
			
		||||
    const oldCamUp = this.camera.up.clone()
 | 
			
		||||
    const aspect = window.innerWidth / window.innerHeight
 | 
			
		||||
    this.lastPerspectiveFov = this.camera.fov
 | 
			
		||||
    const { z_near, z_far } = calculateNearFarFromFOV(this.lastPerspectiveFov)
 | 
			
		||||
@ -436,7 +437,8 @@ export class CameraControls {
 | 
			
		||||
      z_near,
 | 
			
		||||
      z_far
 | 
			
		||||
    )
 | 
			
		||||
    this.camera.up.set(0, 0, 1)
 | 
			
		||||
 | 
			
		||||
    this.camera.up.copy(oldCamUp)
 | 
			
		||||
    this.camera.layers.enable(SKETCH_LAYER)
 | 
			
		||||
    if (DEBUG_SHOW_INTERSECTION_PLANE)
 | 
			
		||||
      this.camera.layers.enable(INTERSECTION_PLANE_LAYER)
 | 
			
		||||
@ -458,13 +460,14 @@ export class CameraControls {
 | 
			
		||||
  }
 | 
			
		||||
  private createPerspectiveCamera = () => {
 | 
			
		||||
    const { z_near, z_far } = calculateNearFarFromFOV(this.lastPerspectiveFov)
 | 
			
		||||
    const previousCamUp = this.camera.up.clone()
 | 
			
		||||
    this.camera = new PerspectiveCamera(
 | 
			
		||||
      this.lastPerspectiveFov,
 | 
			
		||||
      window.innerWidth / window.innerHeight,
 | 
			
		||||
      z_near,
 | 
			
		||||
      z_far
 | 
			
		||||
    )
 | 
			
		||||
    this.camera.up.set(0, 0, 1)
 | 
			
		||||
    this.camera.up.copy(previousCamUp)
 | 
			
		||||
    this.camera.layers.enable(SKETCH_LAYER)
 | 
			
		||||
    if (DEBUG_SHOW_INTERSECTION_PLANE)
 | 
			
		||||
      this.camera.layers.enable(INTERSECTION_PLANE_LAYER)
 | 
			
		||||
@ -618,7 +621,7 @@ export class CameraControls {
 | 
			
		||||
      didChange = true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.safeLookAtTarget()
 | 
			
		||||
    this.safeLookAtTarget(this.camera.up)
 | 
			
		||||
 | 
			
		||||
    // Update the camera's matrices
 | 
			
		||||
    this.camera.updateMatrixWorld()
 | 
			
		||||
@ -683,48 +686,48 @@ export class CameraControls {
 | 
			
		||||
    targetAngle = -Math.PI / 2,
 | 
			
		||||
    duration = 500
 | 
			
		||||
  ): Promise<void> {
 | 
			
		||||
    // should tween the camera so that it has an xPosition of 0, and forcing it's yPosition to be negative
 | 
			
		||||
    // zPosition should stay the same
 | 
			
		||||
    const xyRadius = Math.sqrt(
 | 
			
		||||
      (this.target.x - this.camera.position.x) ** 2 +
 | 
			
		||||
        (this.target.y - this.camera.position.y) ** 2
 | 
			
		||||
    )
 | 
			
		||||
    const xyAngle = Math.atan2(
 | 
			
		||||
      this.camera.position.y - this.target.y,
 | 
			
		||||
      this.camera.position.x - this.target.x
 | 
			
		||||
    )
 | 
			
		||||
    this._isCamMovingCallback(true, true)
 | 
			
		||||
    return new Promise((resolve) => {
 | 
			
		||||
      // should tween the camera so that it has an xPosition of 0, and forcing it's yPosition to be negative
 | 
			
		||||
      // zPosition should stay the same
 | 
			
		||||
      const xyRadius = Math.sqrt(
 | 
			
		||||
        (this.target.x - this.camera.position.x) ** 2 +
 | 
			
		||||
          (this.target.y - this.camera.position.y) ** 2
 | 
			
		||||
      )
 | 
			
		||||
      const xyAngle = Math.atan2(
 | 
			
		||||
        this.camera.position.y - this.target.y,
 | 
			
		||||
        this.camera.position.x - this.target.x
 | 
			
		||||
      )
 | 
			
		||||
      const camAtTime = (obj: { angle: number }) => {
 | 
			
		||||
        const x = xyRadius * Math.cos(obj.angle)
 | 
			
		||||
        const y = xyRadius * Math.sin(obj.angle)
 | 
			
		||||
        this.camera.position.set(
 | 
			
		||||
          this.target.x + x,
 | 
			
		||||
          this.target.y + y,
 | 
			
		||||
          this.camera.position.z
 | 
			
		||||
        )
 | 
			
		||||
        this.update()
 | 
			
		||||
        this.onCameraChange()
 | 
			
		||||
      }
 | 
			
		||||
      const onComplete = (obj: { angle: number }) => {
 | 
			
		||||
        camAtTime(obj)
 | 
			
		||||
        this._isCamMovingCallback(false, true)
 | 
			
		||||
 | 
			
		||||
        // resolve after a couple of frames
 | 
			
		||||
        requestAnimationFrame(() => {
 | 
			
		||||
          requestAnimationFrame(() => resolve())
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
      this._isCamMovingCallback(true, true)
 | 
			
		||||
 | 
			
		||||
      if (isReducedMotion()) {
 | 
			
		||||
        onComplete({ angle: targetAngle })
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      new TWEEN.Tween({ angle: xyAngle })
 | 
			
		||||
        .to({ angle: targetAngle }, duration)
 | 
			
		||||
        .onUpdate((obj) => {
 | 
			
		||||
          const x = xyRadius * Math.cos(obj.angle)
 | 
			
		||||
          const y = xyRadius * Math.sin(obj.angle)
 | 
			
		||||
          this.camera.position.set(
 | 
			
		||||
            this.target.x + x,
 | 
			
		||||
            this.target.y + y,
 | 
			
		||||
            this.camera.position.z
 | 
			
		||||
          )
 | 
			
		||||
          this.update()
 | 
			
		||||
          this.onCameraChange()
 | 
			
		||||
        })
 | 
			
		||||
        .onComplete((obj) => {
 | 
			
		||||
          const x = xyRadius * Math.cos(obj.angle)
 | 
			
		||||
          const y = xyRadius * Math.sin(obj.angle)
 | 
			
		||||
          this.camera.position.set(
 | 
			
		||||
            this.target.x + x,
 | 
			
		||||
            this.target.y + y,
 | 
			
		||||
            this.camera.position.z
 | 
			
		||||
          )
 | 
			
		||||
          this.update()
 | 
			
		||||
          this.onCameraChange()
 | 
			
		||||
          this._isCamMovingCallback(false, true)
 | 
			
		||||
 | 
			
		||||
          // resolve after a couple of frames
 | 
			
		||||
          requestAnimationFrame(() => {
 | 
			
		||||
            requestAnimationFrame(() => resolve())
 | 
			
		||||
          })
 | 
			
		||||
        })
 | 
			
		||||
        .onUpdate(camAtTime)
 | 
			
		||||
        .onComplete(onComplete)
 | 
			
		||||
        .start()
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
@ -778,6 +781,8 @@ export class CameraControls {
 | 
			
		||||
          targetQuaternion,
 | 
			
		||||
          animationProgress
 | 
			
		||||
        )
 | 
			
		||||
        const up = new Vector3(0, 0, 1).applyQuaternion(currentQ)
 | 
			
		||||
        this.camera.up.copy(up)
 | 
			
		||||
        const currentTarget = tempVec.lerpVectors(
 | 
			
		||||
          initialTarget,
 | 
			
		||||
          targetPosition,
 | 
			
		||||
@ -802,7 +807,7 @@ export class CameraControls {
 | 
			
		||||
 | 
			
		||||
      const onComplete = async () => {
 | 
			
		||||
        if (isReducedMotion() && toOrthographic) {
 | 
			
		||||
          cameraAtTime(0.99)
 | 
			
		||||
          cameraAtTime(0.9999)
 | 
			
		||||
          this.useOrthographicCamera()
 | 
			
		||||
        } else if (toOrthographic) {
 | 
			
		||||
          await this.animateToOrthographic()
 | 
			
		||||
@ -863,37 +868,40 @@ export class CameraControls {
 | 
			
		||||
 | 
			
		||||
      animateFovChange() // Start the animation
 | 
			
		||||
    })
 | 
			
		||||
  animateToPerspective = () =>
 | 
			
		||||
  animateToPerspective = (targetCamUp = new Vector3(0, 0, 1)) =>
 | 
			
		||||
    new Promise((resolve) => {
 | 
			
		||||
      if (this.syncDirection === 'engineToClient')
 | 
			
		||||
      if (this.syncDirection === 'engineToClient') {
 | 
			
		||||
        console.warn(
 | 
			
		||||
          'animate To Perspective not design to work with engineToClient syncDirection.'
 | 
			
		||||
        )
 | 
			
		||||
      }
 | 
			
		||||
      this.isFovAnimationInProgress = true
 | 
			
		||||
      // Immediately set the camera to perspective with a very low FOV
 | 
			
		||||
      const targetFov = this.fovBeforeOrtho // Target FOV for perspective
 | 
			
		||||
      this.lastPerspectiveFov = 4
 | 
			
		||||
      let currentFov = 4
 | 
			
		||||
      this.camera.updateProjectionMatrix()
 | 
			
		||||
      const fovAnimationStep = (targetFov - currentFov) / FRAMES_TO_ANIMATE_IN
 | 
			
		||||
      const initialCameraUp = this.camera.up.clone()
 | 
			
		||||
      this.usePerspectiveCamera()
 | 
			
		||||
      const tempVec = new Vector3()
 | 
			
		||||
 | 
			
		||||
      const animateFovChange = () => {
 | 
			
		||||
        if (this.camera instanceof OrthographicCamera) return
 | 
			
		||||
        if (this.camera.fov < targetFov) {
 | 
			
		||||
          // Increase the FOV
 | 
			
		||||
          currentFov = Math.min(currentFov + fovAnimationStep, targetFov)
 | 
			
		||||
          // this.camera.fov = currentFov
 | 
			
		||||
          this.camera.updateProjectionMatrix()
 | 
			
		||||
          this.dollyZoom(currentFov)
 | 
			
		||||
          requestAnimationFrame(animateFovChange) // Continue the animation
 | 
			
		||||
        } else {
 | 
			
		||||
          // Set the flag to false as the FOV animation is complete
 | 
			
		||||
          this.isFovAnimationInProgress = false
 | 
			
		||||
          resolve(true)
 | 
			
		||||
        }
 | 
			
		||||
      const cameraAtTime = (t: number) => {
 | 
			
		||||
        currentFov =
 | 
			
		||||
          this.lastPerspectiveFov + (targetFov - this.lastPerspectiveFov) * t
 | 
			
		||||
        const currentUp = tempVec.lerpVectors(initialCameraUp, targetCamUp, t)
 | 
			
		||||
        this.camera.up.copy(currentUp)
 | 
			
		||||
        this.dollyZoom(currentFov)
 | 
			
		||||
      }
 | 
			
		||||
      animateFovChange() // Start the animation
 | 
			
		||||
 | 
			
		||||
      const onComplete = () => {
 | 
			
		||||
        this.isFovAnimationInProgress = false
 | 
			
		||||
        resolve(true)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      new TWEEN.Tween({ t: 0 })
 | 
			
		||||
        .to({ t: 1 }, isReducedMotion() ? 50 : FRAMES_TO_ANIMATE_IN * 16) // Assuming 60fps, hence 16ms per frame
 | 
			
		||||
        .easing(TWEEN.Easing.Quadratic.InOut)
 | 
			
		||||
        .onUpdate(({ t }) => cameraAtTime(t))
 | 
			
		||||
        .onComplete(onComplete)
 | 
			
		||||
        .start()
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
  reactCameraPropertiesCallback: (a: ReactCameraProperties) => void = () => {}
 | 
			
		||||
 | 
			
		||||
@ -40,3 +40,12 @@ export function isQuaternionVertical(q: Quaternion) {
 | 
			
		||||
  // no x or y components means it's vertical
 | 
			
		||||
  return compareVec2Epsilon2([v.x, v.y], [0, 0])
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function quaternionFromUpNForward(up: Vector3, forward: Vector3) {
 | 
			
		||||
  const dummyCam = new PerspectiveCamera()
 | 
			
		||||
  dummyCam.up.copy(up)
 | 
			
		||||
  dummyCam.position.copy(forward)
 | 
			
		||||
  dummyCam.lookAt(0, 0, 0)
 | 
			
		||||
  dummyCam.updateMatrix()
 | 
			
		||||
  return dummyCam.quaternion.clone()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
@ -37,8 +37,10 @@ export const ZOOM_MAGIC_NUMBER = 63.5
 | 
			
		||||
 | 
			
		||||
export const INTERSECTION_PLANE_LAYER = 1
 | 
			
		||||
export const SKETCH_LAYER = 2
 | 
			
		||||
export const DEBUG_SHOW_INTERSECTION_PLANE = false
 | 
			
		||||
export const DEBUG_SHOW_BOTH_SCENES = false
 | 
			
		||||
 | 
			
		||||
// redundant types so that it can be changed temporarily but CI will catch the wrong type
 | 
			
		||||
export const DEBUG_SHOW_INTERSECTION_PLANE: false = false
 | 
			
		||||
export const DEBUG_SHOW_BOTH_SCENES: false = false
 | 
			
		||||
 | 
			
		||||
export const RAYCASTABLE_PLANE = 'raycastable-plane'
 | 
			
		||||
export const DEFAULT_PLANES = 'default-planes'
 | 
			
		||||
@ -97,13 +99,13 @@ class SceneInfra {
 | 
			
		||||
  _baseUnitMultiplier = 1
 | 
			
		||||
  onDragCallback: (arg: OnDragCallbackArgs) => void = () => {}
 | 
			
		||||
  onMoveCallback: (arg: OnMoveCallbackArgs) => void = () => {}
 | 
			
		||||
  onClickCallback: (arg?: OnClickCallbackArgs) => void = () => {}
 | 
			
		||||
  onClickCallback: (arg: OnClickCallbackArgs) => void = () => {}
 | 
			
		||||
  onMouseEnter: (arg: OnMouseEnterLeaveArgs) => void = () => {}
 | 
			
		||||
  onMouseLeave: (arg: OnMouseEnterLeaveArgs) => void = () => {}
 | 
			
		||||
  setCallbacks = (callbacks: {
 | 
			
		||||
    onDrag?: (arg: OnDragCallbackArgs) => void
 | 
			
		||||
    onMove?: (arg: OnMoveCallbackArgs) => void
 | 
			
		||||
    onClick?: (arg?: OnClickCallbackArgs) => void
 | 
			
		||||
    onClick?: (arg: OnClickCallbackArgs) => void
 | 
			
		||||
    onMouseEnter?: (arg: OnMouseEnterLeaveArgs) => void
 | 
			
		||||
    onMouseLeave?: (arg: OnMouseEnterLeaveArgs) => void
 | 
			
		||||
  }) => {
 | 
			
		||||
@ -272,16 +274,19 @@ class SceneInfra {
 | 
			
		||||
    let transformedPoint = intersectPoint.clone()
 | 
			
		||||
    if (transformedPoint) {
 | 
			
		||||
      transformedPoint.applyQuaternion(inversePlaneQuaternion)
 | 
			
		||||
      transformedPoint?.sub(
 | 
			
		||||
        new Vector3(...planePosition).applyQuaternion(inversePlaneQuaternion)
 | 
			
		||||
      )
 | 
			
		||||
    }
 | 
			
		||||
    const twoD = new Vector2(
 | 
			
		||||
      // I think the intersection plane doesn't get scale when nearly everything else does, maybe that should change
 | 
			
		||||
      transformedPoint.x / this._baseUnitMultiplier,
 | 
			
		||||
      transformedPoint.y / this._baseUnitMultiplier
 | 
			
		||||
    ) // z should be 0
 | 
			
		||||
    const planePositionCorrected = new Vector3(
 | 
			
		||||
      ...planePosition
 | 
			
		||||
    ).applyQuaternion(inversePlaneQuaternion)
 | 
			
		||||
    twoD.sub(new Vector2(...planePositionCorrected))
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      twoD: new Vector2(
 | 
			
		||||
        transformedPoint.x / this._baseUnitMultiplier,
 | 
			
		||||
        transformedPoint.y / this._baseUnitMultiplier
 | 
			
		||||
      ), // z should be 0
 | 
			
		||||
      twoD,
 | 
			
		||||
      threeD: intersectPoint.divideScalar(this._baseUnitMultiplier),
 | 
			
		||||
      intersection: planeIntersects[0],
 | 
			
		||||
    }
 | 
			
		||||
@ -464,7 +469,7 @@ class SceneInfra {
 | 
			
		||||
          intersects,
 | 
			
		||||
        })
 | 
			
		||||
      } else {
 | 
			
		||||
        this.onClickCallback()
 | 
			
		||||
        this.onClickCallback({ mouseEvent, intersects })
 | 
			
		||||
      }
 | 
			
		||||
      // Clear the selected state whether it was dragged or not
 | 
			
		||||
      this.selected = null
 | 
			
		||||
@ -478,7 +483,7 @@ class SceneInfra {
 | 
			
		||||
        intersects,
 | 
			
		||||
      })
 | 
			
		||||
    } else {
 | 
			
		||||
      this.onClickCallback()
 | 
			
		||||
      this.onClickCallback({ mouseEvent, intersects })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  showDefaultPlanes() {
 | 
			
		||||
 | 
			
		||||
@ -38,7 +38,26 @@ describe('processMemory', () => {
 | 
			
		||||
      myVar: 5,
 | 
			
		||||
      myFn: undefined,
 | 
			
		||||
      otherVar: 3,
 | 
			
		||||
      theExtrude: [],
 | 
			
		||||
      theExtrude: [
 | 
			
		||||
        {
 | 
			
		||||
          type: 'extrudePlane',
 | 
			
		||||
          position: [0, 0, 0],
 | 
			
		||||
          rotation: [0, 0, 0, 1],
 | 
			
		||||
          faceId: expect.any(String),
 | 
			
		||||
          name: '',
 | 
			
		||||
          id: expect.any(String),
 | 
			
		||||
          sourceRange: [170, 194],
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          type: 'extrudePlane',
 | 
			
		||||
          position: [0, 0, 0],
 | 
			
		||||
          rotation: [0, 0, 0, 1],
 | 
			
		||||
          faceId: expect.any(String),
 | 
			
		||||
          name: '',
 | 
			
		||||
          id: expect.any(String),
 | 
			
		||||
          sourceRange: [202, 230],
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
      theSketch: [
 | 
			
		||||
        { type: 'ToPoint', to: [-3.35, 0.17], from: [0, 0], name: '' },
 | 
			
		||||
        { type: 'ToPoint', to: [0.98, 5.16], from: [-3.35, 0.17], name: '' },
 | 
			
		||||
 | 
			
		||||
@ -23,9 +23,9 @@ import { applyConstraintAngleLength } from './Toolbar/setAngleLength'
 | 
			
		||||
import { pathMapToSelections } from 'lang/util'
 | 
			
		||||
import { useStore } from 'useStore'
 | 
			
		||||
import {
 | 
			
		||||
  Selections,
 | 
			
		||||
  canExtrudeSelection,
 | 
			
		||||
  handleSelectionBatch,
 | 
			
		||||
  handleSelectionWithShift,
 | 
			
		||||
  isSelectionLastLine,
 | 
			
		||||
  isSketchPipe,
 | 
			
		||||
} from 'lib/selections'
 | 
			
		||||
@ -34,14 +34,20 @@ import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
 | 
			
		||||
import useStateMachineCommands from 'hooks/useStateMachineCommands'
 | 
			
		||||
import { modelingMachineConfig } from 'lib/commandBarConfigs/modelingCommandConfig'
 | 
			
		||||
import { sceneInfra } from 'clientSideScene/sceneInfra'
 | 
			
		||||
import { getSketchQuaternion } from 'clientSideScene/sceneEntities'
 | 
			
		||||
import { startSketchOnDefault } from 'lang/modifyAst'
 | 
			
		||||
import { Program } from 'lang/wasm'
 | 
			
		||||
import { isSingleCursorInPipe } from 'lang/queryAst'
 | 
			
		||||
import {
 | 
			
		||||
  getSketchQuaternion,
 | 
			
		||||
  getSketchOrientationDetails,
 | 
			
		||||
} from 'clientSideScene/sceneEntities'
 | 
			
		||||
import { sketchOnExtrudedFace, startSketchOnDefault } from 'lang/modifyAst'
 | 
			
		||||
import { Program, parse } from 'lang/wasm'
 | 
			
		||||
import { getNodePathFromSourceRange, isSingleCursorInPipe } from 'lang/queryAst'
 | 
			
		||||
import { TEST } from 'env'
 | 
			
		||||
import { exportFromEngine } from 'lib/exportFromEngine'
 | 
			
		||||
import { Models } from '@kittycad/lib/dist/types/src'
 | 
			
		||||
import toast from 'react-hot-toast'
 | 
			
		||||
import { EditorSelection } from '@uiw/react-codemirror'
 | 
			
		||||
import { Vector3 } from 'three'
 | 
			
		||||
import { quaternionFromUpNForward } from 'clientSideScene/helpers'
 | 
			
		||||
 | 
			
		||||
type MachineContext<T extends AnyStateMachine> = {
 | 
			
		||||
  state: StateFrom<T>
 | 
			
		||||
@ -69,9 +75,15 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
  const streamRef = useRef<HTMLDivElement>(null)
 | 
			
		||||
  useSetupEngineManager(streamRef, token)
 | 
			
		||||
 | 
			
		||||
  const { isShiftDown, editorView } = useStore((s) => ({
 | 
			
		||||
  const {
 | 
			
		||||
    isShiftDown,
 | 
			
		||||
    editorView,
 | 
			
		||||
    setLastCodeMirrorSelectionUpdatedFromScene,
 | 
			
		||||
  } = useStore((s) => ({
 | 
			
		||||
    isShiftDown: s.isShiftDown,
 | 
			
		||||
    editorView: s.editorView,
 | 
			
		||||
    setLastCodeMirrorSelectionUpdatedFromScene:
 | 
			
		||||
      s.setLastCodeMirrorSelectionUpdatedFromScene,
 | 
			
		||||
  }))
 | 
			
		||||
 | 
			
		||||
  // Settings machine setup
 | 
			
		||||
@ -92,92 +104,98 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
    {
 | 
			
		||||
      actions: {
 | 
			
		||||
        'sketch exit execute': () => {
 | 
			
		||||
          kclManager.executeAst()
 | 
			
		||||
          try {
 | 
			
		||||
            kclManager.executeAst(parse(kclManager.code))
 | 
			
		||||
          } catch (e) {
 | 
			
		||||
            kclManager.executeAst()
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        'Set selection': assign(({ selectionRanges }, event) => {
 | 
			
		||||
          if (event.type !== 'Set selection') return {} // this was needed for ts after adding 'Set selection' action to on done modal events
 | 
			
		||||
          const setSelections = event.data
 | 
			
		||||
          if (!editorView) return {}
 | 
			
		||||
          if (setSelections.selectionType === 'mirrorCodeMirrorSelections')
 | 
			
		||||
            return { selectionRanges: setSelections.selection }
 | 
			
		||||
          else if (setSelections.selectionType === 'otherSelection') {
 | 
			
		||||
            const {
 | 
			
		||||
              codeMirrorSelection,
 | 
			
		||||
              selectionRangeTypeMap,
 | 
			
		||||
              otherSelections,
 | 
			
		||||
            } = handleSelectionWithShift({
 | 
			
		||||
              otherSelection: setSelections.selection,
 | 
			
		||||
              currentSelections: selectionRanges,
 | 
			
		||||
              isShiftDown,
 | 
			
		||||
            })
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
              editorView.dispatch({
 | 
			
		||||
                selection: codeMirrorSelection,
 | 
			
		||||
              })
 | 
			
		||||
            })
 | 
			
		||||
            return {
 | 
			
		||||
              selectionRangeTypeMap,
 | 
			
		||||
              selectionRanges: {
 | 
			
		||||
                codeBasedSelections: selectionRanges.codeBasedSelections,
 | 
			
		||||
                otherSelections,
 | 
			
		||||
              },
 | 
			
		||||
            }
 | 
			
		||||
          } else if (setSelections.selectionType === 'singleCodeCursor') {
 | 
			
		||||
            // This DOES NOT set the `selectionRanges` in xstate context
 | 
			
		||||
            // instead it updates/dispatches to the editor, which in turn updates the xstate context
 | 
			
		||||
            // I've found this the best way to deal with the editor without causing an infinite loop
 | 
			
		||||
            // and really we want the editor to be in charge of cursor positions and for `selectionRanges` mirror it
 | 
			
		||||
            // because we want to respect the user manually placing the cursor too.
 | 
			
		||||
 | 
			
		||||
            // for more details on how selections see `src/lib/selections.ts`.
 | 
			
		||||
 | 
			
		||||
            const {
 | 
			
		||||
              codeMirrorSelection,
 | 
			
		||||
              selectionRangeTypeMap,
 | 
			
		||||
              otherSelections,
 | 
			
		||||
            } = handleSelectionWithShift({
 | 
			
		||||
              codeSelection: setSelections.selection,
 | 
			
		||||
              currentSelections: selectionRanges,
 | 
			
		||||
              isShiftDown,
 | 
			
		||||
            })
 | 
			
		||||
            if (codeMirrorSelection) {
 | 
			
		||||
              setTimeout(() => {
 | 
			
		||||
                editorView.dispatch({
 | 
			
		||||
                  selection: codeMirrorSelection,
 | 
			
		||||
                })
 | 
			
		||||
              })
 | 
			
		||||
            }
 | 
			
		||||
            if (!setSelections.selection) {
 | 
			
		||||
              return {
 | 
			
		||||
                selectionRangeTypeMap,
 | 
			
		||||
                selectionRanges: {
 | 
			
		||||
                  codeBasedSelections: selectionRanges.codeBasedSelections,
 | 
			
		||||
                  otherSelections,
 | 
			
		||||
                },
 | 
			
		||||
          const dispatchSelection = (selection?: EditorSelection) => {
 | 
			
		||||
            if (!selection) return // TODO less of hack for the below please
 | 
			
		||||
            setLastCodeMirrorSelectionUpdatedFromScene(Date.now())
 | 
			
		||||
            setTimeout(() => editorView.dispatch({ selection }))
 | 
			
		||||
          }
 | 
			
		||||
          let selections: Selections = {
 | 
			
		||||
            codeBasedSelections: [],
 | 
			
		||||
            otherSelections: [],
 | 
			
		||||
          }
 | 
			
		||||
          if (setSelections.selectionType === 'singleCodeCursor') {
 | 
			
		||||
            if (!setSelections.selection && isShiftDown) {
 | 
			
		||||
            } else if (!setSelections.selection && !isShiftDown) {
 | 
			
		||||
              selections = {
 | 
			
		||||
                codeBasedSelections: [],
 | 
			
		||||
                otherSelections: [],
 | 
			
		||||
              }
 | 
			
		||||
            } else if (setSelections.selection && !isShiftDown) {
 | 
			
		||||
              selections = {
 | 
			
		||||
                codeBasedSelections: [setSelections.selection],
 | 
			
		||||
                otherSelections: [],
 | 
			
		||||
              }
 | 
			
		||||
            } else if (setSelections.selection && isShiftDown) {
 | 
			
		||||
              selections = {
 | 
			
		||||
                codeBasedSelections: [
 | 
			
		||||
                  ...selectionRanges.codeBasedSelections,
 | 
			
		||||
                  setSelections.selection,
 | 
			
		||||
                ],
 | 
			
		||||
                otherSelections: selectionRanges.otherSelections,
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const {
 | 
			
		||||
              engineEvents,
 | 
			
		||||
              codeMirrorSelection,
 | 
			
		||||
              updateSceneObjectColors,
 | 
			
		||||
            } = handleSelectionBatch({
 | 
			
		||||
              selections,
 | 
			
		||||
            })
 | 
			
		||||
            codeMirrorSelection && dispatchSelection(codeMirrorSelection)
 | 
			
		||||
            engineEvents &&
 | 
			
		||||
              engineEvents.forEach((event) =>
 | 
			
		||||
                engineCommandManager.sendSceneCommand(event)
 | 
			
		||||
              )
 | 
			
		||||
            updateSceneObjectColors()
 | 
			
		||||
            return {
 | 
			
		||||
              selectionRangeTypeMap,
 | 
			
		||||
              selectionRanges: {
 | 
			
		||||
                codeBasedSelections: selectionRanges.codeBasedSelections,
 | 
			
		||||
                otherSelections,
 | 
			
		||||
              },
 | 
			
		||||
              selectionRanges: selections,
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          // This DOES NOT set the `selectionRanges` in xstate context
 | 
			
		||||
          // same as comment above
 | 
			
		||||
          const { codeMirrorSelection, selectionRangeTypeMap } =
 | 
			
		||||
            handleSelectionBatch({
 | 
			
		||||
              selections: setSelections.selection,
 | 
			
		||||
            })
 | 
			
		||||
          if (codeMirrorSelection) {
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
              editorView.dispatch({
 | 
			
		||||
                selection: codeMirrorSelection,
 | 
			
		||||
              })
 | 
			
		||||
            })
 | 
			
		||||
 | 
			
		||||
          if (setSelections.selectionType === 'mirrorCodeMirrorSelections') {
 | 
			
		||||
            return {
 | 
			
		||||
              selectionRanges: setSelections.selection,
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          return { selectionRangeTypeMap }
 | 
			
		||||
 | 
			
		||||
          if (setSelections.selectionType === 'otherSelection') {
 | 
			
		||||
            if (isShiftDown) {
 | 
			
		||||
              selections = {
 | 
			
		||||
                codeBasedSelections: selectionRanges.codeBasedSelections,
 | 
			
		||||
                otherSelections: [setSelections.selection],
 | 
			
		||||
              }
 | 
			
		||||
            } else {
 | 
			
		||||
              selections = {
 | 
			
		||||
                codeBasedSelections: [],
 | 
			
		||||
                otherSelections: [setSelections.selection],
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
            const { engineEvents, updateSceneObjectColors } =
 | 
			
		||||
              handleSelectionBatch({
 | 
			
		||||
                selections,
 | 
			
		||||
              })
 | 
			
		||||
            engineEvents &&
 | 
			
		||||
              engineEvents.forEach((event) =>
 | 
			
		||||
                engineCommandManager.sendSceneCommand(event)
 | 
			
		||||
              )
 | 
			
		||||
            updateSceneObjectColors()
 | 
			
		||||
            return {
 | 
			
		||||
              selectionRanges: selections,
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          return {}
 | 
			
		||||
        }),
 | 
			
		||||
        'Engine export': (_, event) => {
 | 
			
		||||
          if (event.type !== 'Export' || TEST) return
 | 
			
		||||
@ -255,10 +273,10 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
          kclManager.kclErrors.length === 0 && kclManager.ast.body.length > 0,
 | 
			
		||||
      },
 | 
			
		||||
      services: {
 | 
			
		||||
        'AST-undo-startSketchOn': async ({ sketchPathToNode }) => {
 | 
			
		||||
          if (!sketchPathToNode) return
 | 
			
		||||
        'AST-undo-startSketchOn': async ({ sketchDetails }) => {
 | 
			
		||||
          if (!sketchDetails) return
 | 
			
		||||
          const newAst: Program = JSON.parse(JSON.stringify(kclManager.ast))
 | 
			
		||||
          const varDecIndex = sketchPathToNode[1][0]
 | 
			
		||||
          const varDecIndex = sketchDetails.sketchPathToNode[1][0]
 | 
			
		||||
          // remove body item at varDecIndex
 | 
			
		||||
          newAst.body = newAst.body.filter((_, i) => i !== varDecIndex)
 | 
			
		||||
          await kclManager.executeAstMock(newAst, { updates: 'code' })
 | 
			
		||||
@ -267,28 +285,69 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
            onDrag: () => {},
 | 
			
		||||
          })
 | 
			
		||||
        },
 | 
			
		||||
        'animate-to-face': async (_, { data: { plane, normal } }) => {
 | 
			
		||||
        'animate-to-face': async (_, { data }) => {
 | 
			
		||||
          if (data.type === 'extrudeFace') {
 | 
			
		||||
            const { modifiedAst, pathToNode: pathToNewSketchNode } =
 | 
			
		||||
              sketchOnExtrudedFace(
 | 
			
		||||
                kclManager.ast,
 | 
			
		||||
                data.extrudeSegmentPathToNode,
 | 
			
		||||
                kclManager.programMemory,
 | 
			
		||||
                data.cap
 | 
			
		||||
              )
 | 
			
		||||
            await kclManager.executeAstMock(modifiedAst, { updates: 'code' })
 | 
			
		||||
 | 
			
		||||
            const forward = new Vector3(...data.zAxis)
 | 
			
		||||
            const up = new Vector3(...data.yAxis)
 | 
			
		||||
 | 
			
		||||
            let target = new Vector3(...data.position).multiplyScalar(
 | 
			
		||||
              sceneInfra._baseUnitMultiplier
 | 
			
		||||
            )
 | 
			
		||||
            const quaternion = quaternionFromUpNForward(up, forward)
 | 
			
		||||
            await sceneInfra.camControls.tweenCameraToQuaternion(
 | 
			
		||||
              quaternion,
 | 
			
		||||
              target
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            return {
 | 
			
		||||
              sketchPathToNode: pathToNewSketchNode,
 | 
			
		||||
              zAxis: data.zAxis,
 | 
			
		||||
              yAxis: data.yAxis,
 | 
			
		||||
              origin: data.position,
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          const { modifiedAst, pathToNode } = startSketchOnDefault(
 | 
			
		||||
            kclManager.ast,
 | 
			
		||||
            plane
 | 
			
		||||
            data.plane
 | 
			
		||||
          )
 | 
			
		||||
          await kclManager.updateAst(modifiedAst, false)
 | 
			
		||||
          const quaternion = getSketchQuaternion(pathToNode, normal)
 | 
			
		||||
          await sceneInfra.camControls.tweenCameraToQuaternion(quaternion)
 | 
			
		||||
          const quat = await getSketchQuaternion(pathToNode, data.zAxis)
 | 
			
		||||
          await sceneInfra.camControls.tweenCameraToQuaternion(quat)
 | 
			
		||||
          return {
 | 
			
		||||
            sketchPathToNode: pathToNode,
 | 
			
		||||
            sketchNormalBackUp: normal,
 | 
			
		||||
            zAxis: data.zAxis,
 | 
			
		||||
            yAxis: data.yAxis,
 | 
			
		||||
            origin: [0, 0, 0],
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        'animate-to-sketch': async ({
 | 
			
		||||
          sketchPathToNode,
 | 
			
		||||
          sketchNormalBackUp,
 | 
			
		||||
        }) => {
 | 
			
		||||
          const quaternion = getSketchQuaternion(
 | 
			
		||||
            sketchPathToNode || [],
 | 
			
		||||
            sketchNormalBackUp
 | 
			
		||||
        'animate-to-sketch': async ({ selectionRanges }) => {
 | 
			
		||||
          const sourceRange = selectionRanges.codeBasedSelections[0].range
 | 
			
		||||
          const sketchPathToNode = getNodePathFromSourceRange(
 | 
			
		||||
            kclManager.ast,
 | 
			
		||||
            sourceRange
 | 
			
		||||
          )
 | 
			
		||||
          await sceneInfra.camControls.tweenCameraToQuaternion(quaternion)
 | 
			
		||||
          const info = await getSketchOrientationDetails(sketchPathToNode || [])
 | 
			
		||||
          await sceneInfra.camControls.tweenCameraToQuaternion(
 | 
			
		||||
            info.quat,
 | 
			
		||||
            new Vector3(...info.sketchDetails.origin)
 | 
			
		||||
          )
 | 
			
		||||
          return {
 | 
			
		||||
            sketchPathToNode: sketchPathToNode || [],
 | 
			
		||||
            zAxis: info.sketchDetails.zAxis || null,
 | 
			
		||||
            yAxis: info.sketchDetails.yAxis || null,
 | 
			
		||||
            origin: info.sketchDetails.origin.map(
 | 
			
		||||
              (a) => a / sceneInfra._baseUnitMultiplier
 | 
			
		||||
            ) as [number, number, number],
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        'Get horizontal info': async ({
 | 
			
		||||
          selectionRanges,
 | 
			
		||||
 | 
			
		||||
@ -1,15 +1,14 @@
 | 
			
		||||
import { MouseEventHandler, useEffect, useRef, useState } from 'react'
 | 
			
		||||
import { v4 as uuidv4 } from 'uuid'
 | 
			
		||||
import { useStore } from '../useStore'
 | 
			
		||||
import { getNormalisedCoordinates } from '../lib/utils'
 | 
			
		||||
import Loading from './Loading'
 | 
			
		||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
 | 
			
		||||
import { Models } from '@kittycad/lib'
 | 
			
		||||
import { engineCommandManager } from '../lang/std/engineConnection'
 | 
			
		||||
import { useModelingContext } from 'hooks/useModelingContext'
 | 
			
		||||
import { useKclContext } from 'lang/KclSingleton'
 | 
			
		||||
import { ClientSideScene } from 'clientSideScene/ClientSideSceneComp'
 | 
			
		||||
import { NetworkHealthState, useNetworkStatus } from './NetworkHealthIndicator'
 | 
			
		||||
import { butName } from 'lib/cameraControls'
 | 
			
		||||
import { sendSelectEventToEngine } from 'lib/selections'
 | 
			
		||||
 | 
			
		||||
export const Stream = ({ className = '' }: { className?: string }) => {
 | 
			
		||||
  const [isLoading, setIsLoading] = useState(true)
 | 
			
		||||
@ -60,50 +59,14 @@ export const Stream = ({ className = '' }: { className?: string }) => {
 | 
			
		||||
    setClickCoords({ x, y })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleMouseUp: MouseEventHandler<HTMLDivElement> = ({
 | 
			
		||||
    clientX,
 | 
			
		||||
    clientY,
 | 
			
		||||
    ctrlKey,
 | 
			
		||||
  }) => {
 | 
			
		||||
  const handleMouseUp: MouseEventHandler<HTMLDivElement> = (e) => {
 | 
			
		||||
    if (!videoRef.current) return
 | 
			
		||||
    setButtonDownInStream(undefined)
 | 
			
		||||
    if (state.matches('Sketch')) return
 | 
			
		||||
    if (state.matches('Sketch no face')) return
 | 
			
		||||
    const { x, y } = getNormalisedCoordinates({
 | 
			
		||||
      clientX,
 | 
			
		||||
      clientY,
 | 
			
		||||
      el: videoRef.current,
 | 
			
		||||
      ...streamDimensions,
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    const newCmdId = uuidv4()
 | 
			
		||||
    const interaction = ctrlKey ? 'pan' : 'rotate'
 | 
			
		||||
 | 
			
		||||
    const command: Models['WebSocketRequest_type'] = {
 | 
			
		||||
      type: 'modeling_cmd_req',
 | 
			
		||||
      cmd: {
 | 
			
		||||
        type: 'camera_drag_end',
 | 
			
		||||
        interaction,
 | 
			
		||||
        window: { x, y },
 | 
			
		||||
      },
 | 
			
		||||
      cmd_id: newCmdId,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!didDragInStream) {
 | 
			
		||||
      command.cmd = {
 | 
			
		||||
        type: 'select_with_point',
 | 
			
		||||
        selected_at_window: { x, y },
 | 
			
		||||
        selection_type: 'add',
 | 
			
		||||
      }
 | 
			
		||||
      engineCommandManager.sendSceneCommand(command)
 | 
			
		||||
    } else if (didDragInStream) {
 | 
			
		||||
      command.cmd = {
 | 
			
		||||
        type: 'handle_mouse_drag_end',
 | 
			
		||||
        window: { x, y },
 | 
			
		||||
      }
 | 
			
		||||
      void engineCommandManager.sendSceneCommand(command)
 | 
			
		||||
    } else {
 | 
			
		||||
      engineCommandManager.sendSceneCommand(command)
 | 
			
		||||
    if (!didDragInStream && butName(e).left) {
 | 
			
		||||
      sendSelectEventToEngine(e, videoRef.current, streamDimensions)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setDidDragInStream(false)
 | 
			
		||||
@ -143,6 +106,7 @@ export const Stream = ({ className = '' }: { className?: string }) => {
 | 
			
		||||
        className={`w-full cursor-pointer h-full ${isExecuting && 'blur-md'}`}
 | 
			
		||||
        disablePictureInPicture
 | 
			
		||||
        style={{ transitionDuration: '200ms', transitionProperty: 'filter' }}
 | 
			
		||||
        id="video-stream"
 | 
			
		||||
      />
 | 
			
		||||
      <ClientSideScene cameraControls={settings.context?.cameraControls} />
 | 
			
		||||
      {!isNetworkOkay && !isLoading && (
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,7 @@ import ReactCodeMirror, {
 | 
			
		||||
  Extension,
 | 
			
		||||
  ViewUpdate,
 | 
			
		||||
  keymap,
 | 
			
		||||
  SelectionRange,
 | 
			
		||||
} from '@uiw/react-codemirror'
 | 
			
		||||
import { TEST } from 'env'
 | 
			
		||||
import { useCommandsContext } from 'hooks/useCommandsContext'
 | 
			
		||||
@ -75,7 +76,7 @@ export const TextEditor = ({
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  const {
 | 
			
		||||
    context: { selectionRanges, selectionRangeTypeMap },
 | 
			
		||||
    context: { selectionRanges },
 | 
			
		||||
    send,
 | 
			
		||||
    state,
 | 
			
		||||
  } = useModelingContext()
 | 
			
		||||
@ -91,10 +92,27 @@ export const TextEditor = ({
 | 
			
		||||
    if (isNetworkOkay) kclManager.setCodeAndExecute(newCode)
 | 
			
		||||
    else kclManager.setCode(newCode)
 | 
			
		||||
  } //, []);
 | 
			
		||||
  const lastSelection = useRef('')
 | 
			
		||||
  const onUpdate = (viewUpdate: ViewUpdate) => {
 | 
			
		||||
    if (!editorView) {
 | 
			
		||||
      setEditorView(viewUpdate.view)
 | 
			
		||||
    }
 | 
			
		||||
    const selString = stringifyRanges(
 | 
			
		||||
      viewUpdate?.state?.selection?.ranges || []
 | 
			
		||||
    )
 | 
			
		||||
    if (selString === lastSelection.current) {
 | 
			
		||||
      // onUpdate is noisy and is fired a lot by extensions
 | 
			
		||||
      // since we're only interested in selections changes we can ignore most of these.
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    lastSelection.current = selString
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
      // TODO find a less lazy way of getting the last
 | 
			
		||||
      Date.now() - useStore.getState().lastCodeMirrorSelectionUpdatedFromScene <
 | 
			
		||||
      150
 | 
			
		||||
    )
 | 
			
		||||
      return // update triggered by scene selection
 | 
			
		||||
    if (sceneInfra.selected) return // mid drag
 | 
			
		||||
    const ignoreEvents: ModelingMachineEvent['type'][] = [
 | 
			
		||||
      'Equip Line tool',
 | 
			
		||||
@ -104,7 +122,6 @@ export const TextEditor = ({
 | 
			
		||||
    const eventInfo = processCodeMirrorRanges({
 | 
			
		||||
      codeMirrorRanges: viewUpdate.state.selection.ranges,
 | 
			
		||||
      selectionRanges,
 | 
			
		||||
      selectionRangeTypeMap,
 | 
			
		||||
      isShiftDown,
 | 
			
		||||
    })
 | 
			
		||||
    if (!eventInfo) return
 | 
			
		||||
@ -226,3 +243,7 @@ export const TextEditor = ({
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function stringifyRanges(ranges: readonly SelectionRange[]): string {
 | 
			
		||||
  return ranges.map(({ to, from }) => `${to}->${from}`).join('&')
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
import { executeAst, executeCode } from 'useStore'
 | 
			
		||||
import { Selections } from 'lib/selections'
 | 
			
		||||
import { KCLError } from './errors'
 | 
			
		||||
import { v4 as uuidv4 } from 'uuid'
 | 
			
		||||
import {
 | 
			
		||||
  EngineCommandManager,
 | 
			
		||||
  engineCommandManager,
 | 
			
		||||
@ -14,6 +15,8 @@ import {
 | 
			
		||||
  Program,
 | 
			
		||||
  ProgramMemory,
 | 
			
		||||
  recast,
 | 
			
		||||
  SketchGroup,
 | 
			
		||||
  ExtrudeGroup,
 | 
			
		||||
} from 'lang/wasm'
 | 
			
		||||
import { bracket } from 'lib/exampleKcl'
 | 
			
		||||
import { createContext, useContext, useEffect, useState } from 'react'
 | 
			
		||||
@ -235,7 +238,6 @@ class KclManager {
 | 
			
		||||
    updateCode = false,
 | 
			
		||||
    executionId?: number
 | 
			
		||||
  ) {
 | 
			
		||||
    console.trace('executeAst')
 | 
			
		||||
    const currentExecutionId = executionId || Date.now()
 | 
			
		||||
    this._cancelTokens.set(currentExecutionId, false)
 | 
			
		||||
 | 
			
		||||
@ -245,6 +247,7 @@ class KclManager {
 | 
			
		||||
      ast,
 | 
			
		||||
      engineCommandManager: this.engineCommandManager,
 | 
			
		||||
    })
 | 
			
		||||
    enterEditMode(programMemory)
 | 
			
		||||
    this.isExecuting = false
 | 
			
		||||
    // Check the cancellation token for this execution before applying side effects
 | 
			
		||||
    if (this._cancelTokens.get(currentExecutionId)) {
 | 
			
		||||
@ -333,6 +336,7 @@ class KclManager {
 | 
			
		||||
    }
 | 
			
		||||
    if (!result.isChange) return
 | 
			
		||||
    const { logs, errors, programMemory, ast } = result
 | 
			
		||||
    enterEditMode(programMemory)
 | 
			
		||||
    this.logs = logs
 | 
			
		||||
    this.kclErrors = errors
 | 
			
		||||
    this.programMemory = programMemory
 | 
			
		||||
@ -520,3 +524,18 @@ function safteLSSetItem(key: string, value: string) {
 | 
			
		||||
  if (typeof window === 'undefined') return
 | 
			
		||||
  localStorage?.setItem(key, value)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function enterEditMode(programMemory: ProgramMemory) {
 | 
			
		||||
  const firstSketchOrExtrudeGroup = Object.values(programMemory.root).find(
 | 
			
		||||
    (node) => node.type === 'ExtrudeGroup' || node.type === 'SketchGroup'
 | 
			
		||||
  ) as SketchGroup | ExtrudeGroup
 | 
			
		||||
  firstSketchOrExtrudeGroup &&
 | 
			
		||||
    engineCommandManager.sendSceneCommand({
 | 
			
		||||
      type: 'modeling_cmd_req',
 | 
			
		||||
      cmd_id: uuidv4(),
 | 
			
		||||
      cmd: {
 | 
			
		||||
        type: 'edit_mode_enter',
 | 
			
		||||
        target: firstSketchOrExtrudeGroup.id,
 | 
			
		||||
      },
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -74,16 +74,56 @@ const mySketch001 = startSketchOn('XY')
 | 
			
		||||
    expect(sketch001).toEqual({
 | 
			
		||||
      type: 'ExtrudeGroup',
 | 
			
		||||
      id: expect.any(String),
 | 
			
		||||
      value: [],
 | 
			
		||||
      value: [
 | 
			
		||||
        {
 | 
			
		||||
          type: 'extrudePlane',
 | 
			
		||||
          position: [0, 0, 0],
 | 
			
		||||
          rotation: [0, 0, 0, 1],
 | 
			
		||||
          faceId: expect.any(String),
 | 
			
		||||
          name: '',
 | 
			
		||||
          id: expect.any(String),
 | 
			
		||||
          sourceRange: [77, 102],
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          type: 'extrudePlane',
 | 
			
		||||
          position: [0, 0, 0],
 | 
			
		||||
          rotation: [0, 0, 0, 1],
 | 
			
		||||
          faceId: expect.any(String),
 | 
			
		||||
          name: '',
 | 
			
		||||
          id: expect.any(String),
 | 
			
		||||
          sourceRange: [108, 132],
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
      sketchGroupValues: [
 | 
			
		||||
        {
 | 
			
		||||
          type: 'ToPoint',
 | 
			
		||||
          from: [0, 0],
 | 
			
		||||
          to: [-1.59, -1.54],
 | 
			
		||||
          name: '',
 | 
			
		||||
          __geoMeta: {
 | 
			
		||||
            id: expect.any(String),
 | 
			
		||||
            sourceRange: [77, 102],
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          type: 'ToPoint',
 | 
			
		||||
          from: [-1.59, -1.54],
 | 
			
		||||
          to: [0.46, -5.82],
 | 
			
		||||
          name: '',
 | 
			
		||||
          __geoMeta: {
 | 
			
		||||
            id: expect.any(String),
 | 
			
		||||
            sourceRange: [108, 132],
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
      height: 2,
 | 
			
		||||
      position: [0, 0, 0],
 | 
			
		||||
      rotation: [0, 0, 0, 1],
 | 
			
		||||
      endCapId: null,
 | 
			
		||||
      startCapId: null,
 | 
			
		||||
      sketchGroupValues: expect.any(Array),
 | 
			
		||||
      xAxis: { x: 1, y: 0, z: 0 },
 | 
			
		||||
      yAxis: { x: 0, y: 1, z: 0 },
 | 
			
		||||
      zAxis: { x: 0, y: 0, z: 1 },
 | 
			
		||||
      startCapId: expect.any(String),
 | 
			
		||||
      endCapId: expect.any(String),
 | 
			
		||||
      __meta: [{ sourceRange: [46, 71] }],
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
@ -117,32 +157,149 @@ const sk2 = startSketchOn('XY')
 | 
			
		||||
      {
 | 
			
		||||
        type: 'ExtrudeGroup',
 | 
			
		||||
        id: expect.any(String),
 | 
			
		||||
        value: [],
 | 
			
		||||
        value: [
 | 
			
		||||
          {
 | 
			
		||||
            type: 'extrudePlane',
 | 
			
		||||
            position: [0, 0, 0],
 | 
			
		||||
            rotation: [0, 0, 0, 1],
 | 
			
		||||
            faceId: expect.any(String),
 | 
			
		||||
            name: '',
 | 
			
		||||
            id: expect.any(String),
 | 
			
		||||
            sourceRange: [69, 89],
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            type: 'extrudePlane',
 | 
			
		||||
            position: [0, 0, 0],
 | 
			
		||||
            rotation: [0, 0, 0, 1],
 | 
			
		||||
            faceId: expect.any(String),
 | 
			
		||||
            name: 'p',
 | 
			
		||||
            id: expect.any(String),
 | 
			
		||||
            sourceRange: [95, 118],
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            type: 'extrudePlane',
 | 
			
		||||
            position: [0, 0, 0],
 | 
			
		||||
            rotation: [0, 0, 0, 1],
 | 
			
		||||
            faceId: expect.any(String),
 | 
			
		||||
            name: '',
 | 
			
		||||
            id: expect.any(String),
 | 
			
		||||
            sourceRange: [124, 143],
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
        sketchGroupValues: [
 | 
			
		||||
          {
 | 
			
		||||
            type: 'ToPoint',
 | 
			
		||||
            from: [0, 0],
 | 
			
		||||
            to: [-2.5, 0],
 | 
			
		||||
            name: '',
 | 
			
		||||
            __geoMeta: {
 | 
			
		||||
              id: expect.any(String),
 | 
			
		||||
              sourceRange: [69, 89],
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            type: 'ToPoint',
 | 
			
		||||
            from: [-2.5, 0],
 | 
			
		||||
            to: [0, 10],
 | 
			
		||||
            name: 'p',
 | 
			
		||||
            __geoMeta: {
 | 
			
		||||
              id: expect.any(String),
 | 
			
		||||
              sourceRange: [95, 118],
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            type: 'ToPoint',
 | 
			
		||||
            from: [0, 10],
 | 
			
		||||
            to: [2.5, 0],
 | 
			
		||||
            name: '',
 | 
			
		||||
            __geoMeta: {
 | 
			
		||||
              id: expect.any(String),
 | 
			
		||||
              sourceRange: [124, 143],
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
        height: 2,
 | 
			
		||||
        position: [0, 0, 0],
 | 
			
		||||
        rotation: [0, 0, 0, 1],
 | 
			
		||||
        endCapId: null,
 | 
			
		||||
        startCapId: null,
 | 
			
		||||
        sketchGroupValues: expect.any(Array),
 | 
			
		||||
        xAxis: { x: 1, y: 0, z: 0 },
 | 
			
		||||
        yAxis: { x: 0, y: 1, z: 0 },
 | 
			
		||||
        zAxis: { x: 0, y: 0, z: 1 },
 | 
			
		||||
        startCapId: expect.any(String),
 | 
			
		||||
        endCapId: expect.any(String),
 | 
			
		||||
        __meta: [{ sourceRange: [38, 63] }],
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        type: 'ExtrudeGroup',
 | 
			
		||||
        id: expect.any(String),
 | 
			
		||||
        value: [],
 | 
			
		||||
        value: [
 | 
			
		||||
          {
 | 
			
		||||
            type: 'extrudePlane',
 | 
			
		||||
            position: [0, 0, 0],
 | 
			
		||||
            rotation: [0, 0, 0, 1],
 | 
			
		||||
            faceId: expect.any(String),
 | 
			
		||||
            name: '',
 | 
			
		||||
            id: expect.any(String),
 | 
			
		||||
            sourceRange: [374, 394],
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            type: 'extrudePlane',
 | 
			
		||||
            position: [0, 0, 0],
 | 
			
		||||
            rotation: [0, 0, 0, 1],
 | 
			
		||||
            faceId: expect.any(String),
 | 
			
		||||
            name: 'p',
 | 
			
		||||
            id: expect.any(String),
 | 
			
		||||
            sourceRange: [400, 422],
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            type: 'extrudePlane',
 | 
			
		||||
            position: [0, 0, 0],
 | 
			
		||||
            rotation: [0, 0, 0, 1],
 | 
			
		||||
            faceId: expect.any(String),
 | 
			
		||||
            name: '',
 | 
			
		||||
            id: expect.any(String),
 | 
			
		||||
            sourceRange: [428, 447],
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
        sketchGroupValues: [
 | 
			
		||||
          {
 | 
			
		||||
            type: 'ToPoint',
 | 
			
		||||
            from: [0, 0],
 | 
			
		||||
            to: [-2.5, 0],
 | 
			
		||||
            name: '',
 | 
			
		||||
            __geoMeta: {
 | 
			
		||||
              id: expect.any(String),
 | 
			
		||||
              sourceRange: [374, 394],
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            type: 'ToPoint',
 | 
			
		||||
            from: [-2.5, 0],
 | 
			
		||||
            to: [0, 3],
 | 
			
		||||
            name: 'p',
 | 
			
		||||
            __geoMeta: {
 | 
			
		||||
              id: expect.any(String),
 | 
			
		||||
              sourceRange: [400, 422],
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            type: 'ToPoint',
 | 
			
		||||
            from: [0, 3],
 | 
			
		||||
            to: [2.5, 0],
 | 
			
		||||
            name: '',
 | 
			
		||||
            __geoMeta: {
 | 
			
		||||
              id: expect.any(String),
 | 
			
		||||
              sourceRange: [428, 447],
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
        height: 2,
 | 
			
		||||
        position: [0, 0, 0],
 | 
			
		||||
        rotation: [0, 0, 0, 1],
 | 
			
		||||
 | 
			
		||||
        endCapId: null,
 | 
			
		||||
        startCapId: null,
 | 
			
		||||
        sketchGroupValues: expect.any(Array),
 | 
			
		||||
        xAxis: { x: 1, y: 0, z: 0 },
 | 
			
		||||
        yAxis: { x: 0, y: 1, z: 0 },
 | 
			
		||||
        zAxis: { x: 0, y: 0, z: 1 },
 | 
			
		||||
        startCapId: expect.any(String),
 | 
			
		||||
        endCapId: expect.any(String),
 | 
			
		||||
        __meta: [{ sourceRange: [343, 368] }],
 | 
			
		||||
      },
 | 
			
		||||
    ])
 | 
			
		||||
 | 
			
		||||
@ -12,8 +12,10 @@ import {
 | 
			
		||||
  addSketchTo,
 | 
			
		||||
  giveSketchFnCallTag,
 | 
			
		||||
  moveValueIntoNewVariable,
 | 
			
		||||
  sketchOnExtrudedFace,
 | 
			
		||||
} from './modifyAst'
 | 
			
		||||
import { enginelessExecutor } from '../lib/testHelpers'
 | 
			
		||||
import { getNodePathFromSourceRange } from './queryAst'
 | 
			
		||||
 | 
			
		||||
beforeAll(() => initPromise)
 | 
			
		||||
 | 
			
		||||
@ -274,3 +276,89 @@ const yo2 = hmm([identifierGuy + 5])`
 | 
			
		||||
    expect(newCode).toContain(`const yo2 = hmm([newVar])`)
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
describe('testing sketchOnExtrudedFace', () => {
 | 
			
		||||
  test('it should be able to extrude on regular segments', async () => {
 | 
			
		||||
    const code = `const part001 = startSketchOn('-XZ')
 | 
			
		||||
  |> startProfileAt([3.58, 2.06], %)
 | 
			
		||||
  |> line([9.7, 9.19], %)
 | 
			
		||||
  |> line([8.62, -9.57], %)
 | 
			
		||||
  |> close(%)
 | 
			
		||||
  |> extrude(5 + 7, %)`
 | 
			
		||||
    const ast = parse(code)
 | 
			
		||||
    const programMemory = await enginelessExecutor(ast)
 | 
			
		||||
    const snippet = `line([9.7, 9.19], %)`
 | 
			
		||||
    const range: [number, number] = [
 | 
			
		||||
      code.indexOf(snippet),
 | 
			
		||||
      code.indexOf(snippet) + snippet.length,
 | 
			
		||||
    ]
 | 
			
		||||
    const pathToNode = getNodePathFromSourceRange(ast, range)
 | 
			
		||||
 | 
			
		||||
    const { modifiedAst } = sketchOnExtrudedFace(ast, pathToNode, programMemory)
 | 
			
		||||
    const newCode = recast(modifiedAst)
 | 
			
		||||
    expect(newCode).toContain(`const part001 = startSketchOn('-XZ')
 | 
			
		||||
  |> startProfileAt([3.58, 2.06], %)
 | 
			
		||||
  |> line([9.7, 9.19], %, 'seg01')
 | 
			
		||||
  |> line([8.62, -9.57], %)
 | 
			
		||||
  |> close(%)
 | 
			
		||||
  |> extrude(5 + 7, %)
 | 
			
		||||
const part002 = startSketchOn(part001, 'seg01')`)
 | 
			
		||||
  })
 | 
			
		||||
  test('it should be able to extrude on close segments', async () => {
 | 
			
		||||
    const code = `const part001 = startSketchOn('-XZ')
 | 
			
		||||
  |> startProfileAt([3.58, 2.06], %)
 | 
			
		||||
  |> line([9.7, 9.19], %)
 | 
			
		||||
  |> line([8.62, -9.57], %)
 | 
			
		||||
  |> close(%)
 | 
			
		||||
  |> extrude(5 + 7, %)`
 | 
			
		||||
    const ast = parse(code)
 | 
			
		||||
    const programMemory = await enginelessExecutor(ast)
 | 
			
		||||
    const snippet = `close(%)`
 | 
			
		||||
    const range: [number, number] = [
 | 
			
		||||
      code.indexOf(snippet),
 | 
			
		||||
      code.indexOf(snippet) + snippet.length,
 | 
			
		||||
    ]
 | 
			
		||||
    const pathToNode = getNodePathFromSourceRange(ast, range)
 | 
			
		||||
 | 
			
		||||
    const { modifiedAst } = sketchOnExtrudedFace(ast, pathToNode, programMemory)
 | 
			
		||||
    const newCode = recast(modifiedAst)
 | 
			
		||||
    expect(newCode).toContain(`const part001 = startSketchOn('-XZ')
 | 
			
		||||
  |> startProfileAt([3.58, 2.06], %)
 | 
			
		||||
  |> line([9.7, 9.19], %)
 | 
			
		||||
  |> line([8.62, -9.57], %)
 | 
			
		||||
  |> close(%, 'seg01')
 | 
			
		||||
  |> extrude(5 + 7, %)
 | 
			
		||||
const part002 = startSketchOn(part001, 'seg01')`)
 | 
			
		||||
  })
 | 
			
		||||
  test('it should be able to extrude on start-end caps', async () => {
 | 
			
		||||
    const code = `const part001 = startSketchOn('-XZ')
 | 
			
		||||
  |> startProfileAt([3.58, 2.06], %)
 | 
			
		||||
  |> line([9.7, 9.19], %)
 | 
			
		||||
  |> line([8.62, -9.57], %)
 | 
			
		||||
  |> close(%)
 | 
			
		||||
  |> extrude(5 + 7, %)`
 | 
			
		||||
    const ast = parse(code)
 | 
			
		||||
    const programMemory = await enginelessExecutor(ast)
 | 
			
		||||
    const snippet = `startProfileAt([3.58, 2.06], %)`
 | 
			
		||||
    const range: [number, number] = [
 | 
			
		||||
      code.indexOf(snippet),
 | 
			
		||||
      code.indexOf(snippet) + snippet.length,
 | 
			
		||||
    ]
 | 
			
		||||
    const pathToNode = getNodePathFromSourceRange(ast, range)
 | 
			
		||||
 | 
			
		||||
    const { modifiedAst } = sketchOnExtrudedFace(
 | 
			
		||||
      ast,
 | 
			
		||||
      pathToNode,
 | 
			
		||||
      programMemory,
 | 
			
		||||
      'end'
 | 
			
		||||
    )
 | 
			
		||||
    const newCode = recast(modifiedAst)
 | 
			
		||||
    expect(newCode).toContain(`const part001 = startSketchOn('-XZ')
 | 
			
		||||
  |> startProfileAt([3.58, 2.06], %)
 | 
			
		||||
  |> line([9.7, 9.19], %)
 | 
			
		||||
  |> line([8.62, -9.57], %)
 | 
			
		||||
  |> close(%)
 | 
			
		||||
  |> extrude(5 + 7, %)
 | 
			
		||||
const part002 = startSketchOn(part001, 'END')`)
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,3 @@
 | 
			
		||||
import { ToolTip } from '../useStore'
 | 
			
		||||
import { Selection } from 'lib/selections'
 | 
			
		||||
import {
 | 
			
		||||
  Program,
 | 
			
		||||
@ -64,7 +63,6 @@ export function addStartProfileAt(
 | 
			
		||||
  pathToNode: PathToNode,
 | 
			
		||||
  at: [number, number]
 | 
			
		||||
): { modifiedAst: Program; pathToNode: PathToNode } {
 | 
			
		||||
  console.log('addStartProfileAt called')
 | 
			
		||||
  const variableDeclaration = getNodeFromPath<VariableDeclaration>(
 | 
			
		||||
    node,
 | 
			
		||||
    pathToNode,
 | 
			
		||||
@ -317,17 +315,17 @@ export function extrudeSketch(
 | 
			
		||||
export function sketchOnExtrudedFace(
 | 
			
		||||
  node: Program,
 | 
			
		||||
  pathToNode: PathToNode,
 | 
			
		||||
  programMemory: ProgramMemory
 | 
			
		||||
  programMemory: ProgramMemory,
 | 
			
		||||
  cap: 'none' | 'start' | 'end' = 'none'
 | 
			
		||||
): { modifiedAst: Program; pathToNode: PathToNode } {
 | 
			
		||||
  let _node = { ...node }
 | 
			
		||||
  const newSketchName = findUniqueName(node, 'part')
 | 
			
		||||
  const { node: oldSketchNode, shallowPath: pathToOldSketch } =
 | 
			
		||||
    getNodeFromPath<VariableDeclarator>(
 | 
			
		||||
      _node,
 | 
			
		||||
      pathToNode,
 | 
			
		||||
      'VariableDeclarator',
 | 
			
		||||
      true
 | 
			
		||||
    )
 | 
			
		||||
  const { node: oldSketchNode } = getNodeFromPath<VariableDeclarator>(
 | 
			
		||||
    _node,
 | 
			
		||||
    pathToNode,
 | 
			
		||||
    'VariableDeclarator',
 | 
			
		||||
    true
 | 
			
		||||
  )
 | 
			
		||||
  const oldSketchName = oldSketchNode.id.name
 | 
			
		||||
  const { node: expression } = getNodeFromPath<CallExpression>(
 | 
			
		||||
    _node,
 | 
			
		||||
@ -335,42 +333,44 @@ export function sketchOnExtrudedFace(
 | 
			
		||||
    'CallExpression'
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const { modifiedAst, tag } = addTagForSketchOnFace(
 | 
			
		||||
    {
 | 
			
		||||
      previousProgramMemory: programMemory,
 | 
			
		||||
      pathToNode,
 | 
			
		||||
      node: _node,
 | 
			
		||||
    },
 | 
			
		||||
    expression.callee.name
 | 
			
		||||
  )
 | 
			
		||||
  _node = modifiedAst
 | 
			
		||||
  let _tag = ''
 | 
			
		||||
  if (cap === 'none') {
 | 
			
		||||
    const { modifiedAst, tag } = addTagForSketchOnFace(
 | 
			
		||||
      {
 | 
			
		||||
        previousProgramMemory: programMemory,
 | 
			
		||||
        pathToNode,
 | 
			
		||||
        node: _node,
 | 
			
		||||
      },
 | 
			
		||||
      expression.callee.name
 | 
			
		||||
    )
 | 
			
		||||
    _tag = tag
 | 
			
		||||
    _node = modifiedAst
 | 
			
		||||
  } else {
 | 
			
		||||
    _tag = cap.toUpperCase()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const newSketch = createVariableDeclaration(
 | 
			
		||||
    newSketchName,
 | 
			
		||||
    createPipeExpression([
 | 
			
		||||
      createCallExpressionStdLib('startSketchAt', [
 | 
			
		||||
        createArrayExpression([createLiteral(0), createLiteral(0)]),
 | 
			
		||||
      ]),
 | 
			
		||||
      createCallExpressionStdLib('lineTo', [
 | 
			
		||||
        createArrayExpression([createLiteral(1), createLiteral(1)]),
 | 
			
		||||
        createPipeSubstitution(),
 | 
			
		||||
      ]),
 | 
			
		||||
      createCallExpression('transform', [
 | 
			
		||||
        createCallExpressionStdLib('getExtrudeWallTransform', [
 | 
			
		||||
          createLiteral(tag),
 | 
			
		||||
          createIdentifier(oldSketchName),
 | 
			
		||||
        ]),
 | 
			
		||||
        createPipeSubstitution(),
 | 
			
		||||
      ]),
 | 
			
		||||
    createCallExpressionStdLib('startSketchOn', [
 | 
			
		||||
      createIdentifier(oldSketchName),
 | 
			
		||||
      createLiteral(_tag),
 | 
			
		||||
    ]),
 | 
			
		||||
    'const'
 | 
			
		||||
  )
 | 
			
		||||
  const expressionIndex = getLastIndex(pathToOldSketch)
 | 
			
		||||
 | 
			
		||||
  const expressionIndex = pathToNode[1][0] as number
 | 
			
		||||
  _node.body.splice(expressionIndex + 1, 0, newSketch)
 | 
			
		||||
  const newpathToNode: PathToNode = [
 | 
			
		||||
    ['body', ''],
 | 
			
		||||
    [expressionIndex + 1, 'index'],
 | 
			
		||||
    ['declarations', 'VariableDeclaration'],
 | 
			
		||||
    [0, 'index'],
 | 
			
		||||
    ['init', 'VariableDeclarator'],
 | 
			
		||||
  ]
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    modifiedAst: _node,
 | 
			
		||||
    pathToNode: [...pathToNode.slice(0, -1), [expressionIndex, 'index']],
 | 
			
		||||
    pathToNode: newpathToNode,
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -150,7 +150,7 @@ log(5, myVar)
 | 
			
		||||
    const recasted = recast(ast)
 | 
			
		||||
    expect(recasted.trim()).toBe(code.trim())
 | 
			
		||||
  })
 | 
			
		||||
  it('recast long object exectution', () => {
 | 
			
		||||
  it('recast long object execution', () => {
 | 
			
		||||
    const code = `const three = 3
 | 
			
		||||
const yo = {
 | 
			
		||||
  aStr: 'str',
 | 
			
		||||
@ -163,7 +163,7 @@ const yo = {
 | 
			
		||||
    const recasted = recast(ast)
 | 
			
		||||
    expect(recasted).toBe(code)
 | 
			
		||||
  })
 | 
			
		||||
  it('recast short object exectution', () => {
 | 
			
		||||
  it('recast short object execution', () => {
 | 
			
		||||
    const code = `const yo = { key: 'val' }
 | 
			
		||||
`
 | 
			
		||||
    const { ast } = code2ast(code)
 | 
			
		||||
 | 
			
		||||
@ -13,6 +13,10 @@ interface CommandInfo {
 | 
			
		||||
  range: SourceRange
 | 
			
		||||
  pathToNode: PathToNode
 | 
			
		||||
  parentId?: string
 | 
			
		||||
  additionalData?: {
 | 
			
		||||
    type: 'cap'
 | 
			
		||||
    info: 'start' | 'end'
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type WebSocketResponse = Models['OkWebSocketResponseData_type']
 | 
			
		||||
@ -1069,14 +1073,42 @@ export class EngineCommandManager {
 | 
			
		||||
      } as const
 | 
			
		||||
      this.artifactMap[id] = artifact
 | 
			
		||||
      if (
 | 
			
		||||
        command.commandType === 'entity_linear_pattern' ||
 | 
			
		||||
        command.commandType === 'entity_circular_pattern'
 | 
			
		||||
        (command.commandType === 'entity_linear_pattern' &&
 | 
			
		||||
          modelingResponse.type === 'entity_linear_pattern') ||
 | 
			
		||||
        (command.commandType === 'entity_circular_pattern' &&
 | 
			
		||||
          modelingResponse.type === 'entity_circular_pattern')
 | 
			
		||||
      ) {
 | 
			
		||||
        const entities = (modelingResponse as any)?.data?.entity_ids
 | 
			
		||||
        const entities = modelingResponse.data.entity_ids
 | 
			
		||||
        entities?.forEach((entity: string) => {
 | 
			
		||||
          this.artifactMap[entity] = artifact
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
      if (
 | 
			
		||||
        command?.commandType === 'solid3d_get_extrusion_face_info' &&
 | 
			
		||||
        modelingResponse.type === 'solid3d_get_extrusion_face_info'
 | 
			
		||||
      ) {
 | 
			
		||||
        console.log('modelingResposne', modelingResponse)
 | 
			
		||||
        const parent = this.artifactMap[command?.parentId || '']
 | 
			
		||||
        modelingResponse.data.faces.forEach((face) => {
 | 
			
		||||
          if (face.cap !== 'none' && face.face_id && parent) {
 | 
			
		||||
            this.artifactMap[face.face_id] = {
 | 
			
		||||
              ...parent,
 | 
			
		||||
              commandType: 'solid3d_get_extrusion_face_info',
 | 
			
		||||
              additionalData: {
 | 
			
		||||
                type: 'cap',
 | 
			
		||||
                info: face.cap === 'bottom' ? 'start' : 'end',
 | 
			
		||||
              },
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          const curveArtifact = this.artifactMap[face?.curve_id || '']
 | 
			
		||||
          if (curveArtifact && face?.face_id) {
 | 
			
		||||
            this.artifactMap[face.face_id] = {
 | 
			
		||||
              ...curveArtifact,
 | 
			
		||||
              commandType: 'solid3d_get_extrusion_face_info',
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
      resolve({
 | 
			
		||||
        id,
 | 
			
		||||
        commandType: command.commandType,
 | 
			
		||||
@ -1388,12 +1420,6 @@ export class EngineCommandManager {
 | 
			
		||||
    const promise = new Promise((_resolve, reject) => {
 | 
			
		||||
      resolve = _resolve
 | 
			
		||||
    })
 | 
			
		||||
    const getParentId = (): string | undefined => {
 | 
			
		||||
      if (command.type === 'extend_path') {
 | 
			
		||||
        return command.path
 | 
			
		||||
      }
 | 
			
		||||
      // TODO handle other commands that have a parent
 | 
			
		||||
    }
 | 
			
		||||
    const pathToNode = ast
 | 
			
		||||
      ? getNodePathFromSourceRange(ast, range || [0, 0])
 | 
			
		||||
      : []
 | 
			
		||||
@ -1402,7 +1428,6 @@ export class EngineCommandManager {
 | 
			
		||||
      pathToNode,
 | 
			
		||||
      type: 'pending',
 | 
			
		||||
      commandType: command.type,
 | 
			
		||||
      parentId: getParentId(),
 | 
			
		||||
      promise,
 | 
			
		||||
      resolve,
 | 
			
		||||
    }
 | 
			
		||||
@ -1419,10 +1444,14 @@ export class EngineCommandManager {
 | 
			
		||||
      resolve = _resolve
 | 
			
		||||
    })
 | 
			
		||||
    const getParentId = (): string | undefined => {
 | 
			
		||||
      if (command.type === 'extend_path') {
 | 
			
		||||
        return command.path
 | 
			
		||||
      if (command.type === 'extend_path') return command.path
 | 
			
		||||
      if (command.type === 'solid3d_get_extrusion_face_info') {
 | 
			
		||||
        const edgeArtifact = this.artifactMap[command.edge_id]
 | 
			
		||||
        // edges's parent id is to the original "start_path" artifact
 | 
			
		||||
        if (edgeArtifact?.parentId) return edgeArtifact.parentId
 | 
			
		||||
      }
 | 
			
		||||
      // TODO handle other commands that have a parent
 | 
			
		||||
      if (command.type === 'close_path') return command.path_id
 | 
			
		||||
      // handle other commands that have a parent here
 | 
			
		||||
    }
 | 
			
		||||
    const pathToNode = ast
 | 
			
		||||
      ? getNodePathFromSourceRange(ast, range || [0, 0])
 | 
			
		||||
 | 
			
		||||
@ -1155,11 +1155,14 @@ export function addTagForSketchOnFace(
 | 
			
		||||
  a: ModifyAstBase,
 | 
			
		||||
  expressionName: string
 | 
			
		||||
) {
 | 
			
		||||
  if (expressionName === 'close') {
 | 
			
		||||
    return addTag(1)(a)
 | 
			
		||||
  }
 | 
			
		||||
  if (expressionName in sketchLineHelperMap) {
 | 
			
		||||
    const { addTag } = sketchLineHelperMap[expressionName]
 | 
			
		||||
    return addTag(a)
 | 
			
		||||
  }
 | 
			
		||||
  throw new Error('not a sketch line helper')
 | 
			
		||||
  throw new Error(`"${expressionName}" is not a sketch line helper`)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function isAngleLiteral(lineArugement: Value): boolean {
 | 
			
		||||
@ -1174,7 +1177,7 @@ function isAngleLiteral(lineArugement: Value): boolean {
 | 
			
		||||
 | 
			
		||||
type addTagFn = (a: ModifyAstBase) => { modifiedAst: Program; tag: string }
 | 
			
		||||
 | 
			
		||||
function addTag(): addTagFn {
 | 
			
		||||
function addTag(tagIndex = 2): addTagFn {
 | 
			
		||||
  return ({ node, pathToNode }) => {
 | 
			
		||||
    const _node = { ...node }
 | 
			
		||||
    const { node: primaryCallExp } = getNodeFromPath<CallExpression>(
 | 
			
		||||
@ -1184,12 +1187,12 @@ function addTag(): addTagFn {
 | 
			
		||||
    )
 | 
			
		||||
    // Tag is always 3rd expression now, using arg index feels brittle
 | 
			
		||||
    // but we can come up with a better way to identify tag later.
 | 
			
		||||
    const thirdArg = primaryCallExp.arguments?.[2]
 | 
			
		||||
    const thirdArg = primaryCallExp.arguments?.[tagIndex]
 | 
			
		||||
    const tagLiteral =
 | 
			
		||||
      thirdArg || (createLiteral(findUniqueName(_node, 'seg', 2)) as Literal)
 | 
			
		||||
    const isTagExisting = !!thirdArg
 | 
			
		||||
    if (!isTagExisting) {
 | 
			
		||||
      primaryCallExp.arguments[2] = tagLiteral
 | 
			
		||||
      primaryCallExp.arguments[tagIndex] = tagLiteral
 | 
			
		||||
    }
 | 
			
		||||
    if ('value' in tagLiteral) {
 | 
			
		||||
      // Now TypeScript knows tagLiteral has a value property
 | 
			
		||||
 | 
			
		||||
@ -66,6 +66,7 @@ export type { Position } from '../wasm-lib/kcl/bindings/Position'
 | 
			
		||||
export type { Rotation } from '../wasm-lib/kcl/bindings/Rotation'
 | 
			
		||||
export type { Path } from '../wasm-lib/kcl/bindings/Path'
 | 
			
		||||
export type { SketchGroup } from '../wasm-lib/kcl/bindings/SketchGroup'
 | 
			
		||||
export type { ExtrudeGroup } from '../wasm-lib/kcl/bindings/ExtrudeGroup'
 | 
			
		||||
export type { MemoryItem } from '../wasm-lib/kcl/bindings/MemoryItem'
 | 
			
		||||
export type { ExtrudeSurface } from '../wasm-lib/kcl/bindings/ExtrudeSurface'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -39,7 +39,7 @@ export interface MouseGuard {
 | 
			
		||||
  rotate: MouseGuardHandler
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const butName = (e: React.MouseEvent) => ({
 | 
			
		||||
export const butName = (e: React.MouseEvent) => ({
 | 
			
		||||
  middle: !!(e.buttons & 4) || e.button === 1,
 | 
			
		||||
  right: !!(e.buttons & 2) || e.button === 2,
 | 
			
		||||
  left: !!(e.buttons & 1) || e.button === 0,
 | 
			
		||||
 | 
			
		||||
@ -117,7 +117,7 @@ export const modelingMachineConfig: CommandSetConfig<
 | 
			
		||||
    args: {
 | 
			
		||||
      selection: {
 | 
			
		||||
        inputType: 'selection',
 | 
			
		||||
        selectionTypes: ['face'],
 | 
			
		||||
        selectionTypes: ['extrude-wall', 'start-cap', 'end-cap'],
 | 
			
		||||
        multiple: false, // TODO: multiple selection
 | 
			
		||||
        required: true,
 | 
			
		||||
        skip: true,
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,7 @@ import { v4 as uuidv4 } from 'uuid'
 | 
			
		||||
import { EditorSelection } from '@codemirror/state'
 | 
			
		||||
import { kclManager } from 'lang/KclSingleton'
 | 
			
		||||
import { SelectionRange } from '@uiw/react-codemirror'
 | 
			
		||||
import { isOverlap } from 'lib/utils'
 | 
			
		||||
import { getNormalisedCoordinates, isOverlap } from 'lib/utils'
 | 
			
		||||
import { isCursorInSketchCommandRange } from 'lang/util'
 | 
			
		||||
import { Program } from 'lang/wasm'
 | 
			
		||||
import {
 | 
			
		||||
@ -22,70 +22,12 @@ import {
 | 
			
		||||
  getParentGroup,
 | 
			
		||||
  PROFILE_START,
 | 
			
		||||
} from 'clientSideScene/sceneEntities'
 | 
			
		||||
import { Mesh } from 'three'
 | 
			
		||||
import { Mesh, Object3D, Object3DEventMap } from 'three'
 | 
			
		||||
import { AXIS_GROUP, X_AXIS } from 'clientSideScene/sceneInfra'
 | 
			
		||||
 | 
			
		||||
export const X_AXIS_UUID = 'ad792545-7fd3-482a-a602-a93924e3055b'
 | 
			
		||||
export const Y_AXIS_UUID = '680fd157-266f-4b8a-984f-cdf46b8bdf01'
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
How selections work is complex due to the nature that we rely on the engine
 | 
			
		||||
to tell what has been selected after we send a click command. But than the
 | 
			
		||||
app needs these selections to be based on cursors, therefore the app must
 | 
			
		||||
be in control of selections. On top of that because we need to set cursor
 | 
			
		||||
positions in code-mirror for selections, both from app logic, and still
 | 
			
		||||
allow the user to add multiple cursors like a normal editor, it's best to
 | 
			
		||||
let code mirror control cursor positions and associate those source ranges
 | 
			
		||||
with entity ids from code-mirror events later.
 | 
			
		||||
 | 
			
		||||
So it's a lot of back and forth. conceptually the back and forth is:
 | 
			
		||||
 | 
			
		||||
1) we send a click command to the engine
 | 
			
		||||
2) the engine sends back ids of entities that were clicked
 | 
			
		||||
3) we associate that source ranges with those ids
 | 
			
		||||
4) we set the codemirror selection based on those source ranges (taking
 | 
			
		||||
  into account if the user is holding shift to add to current selections
 | 
			
		||||
  or not). we also create and remember a SelectionRangeTypeMap
 | 
			
		||||
5) Code mirror fires a an event that cursors have changed, we loop through
 | 
			
		||||
  these ranges and associate them with entity ids again with the ArtifactMap,
 | 
			
		||||
  but also we can pick up selection types using the SelectionRangeTypeMap
 | 
			
		||||
6) we clear all previous selections in the engine and set the new ones
 | 
			
		||||
 | 
			
		||||
The above is less likely to get stale but below is some more details,
 | 
			
		||||
because this wonders all over the code-base, I've tried to centeralise it
 | 
			
		||||
by putting relevant utils in this file. All of the functions below are
 | 
			
		||||
pure with the exception of getEventForSelectWithPoint which makes a call
 | 
			
		||||
to the engine, but it's a query call (not mutation) so I'm okay with this.
 | 
			
		||||
Actual side effects that change cursors or tell the engine what's selected
 | 
			
		||||
are still done throughout the in their relevant parts in the codebase.
 | 
			
		||||
 | 
			
		||||
In detail:
 | 
			
		||||
 | 
			
		||||
1) Click commands are mostly sent in stream.tsx search for
 | 
			
		||||
  "select_with_point"
 | 
			
		||||
2) The handler for when the engine sends back entity ids calls
 | 
			
		||||
  getEventForSelectWithPoint, it fires an XState event to update our
 | 
			
		||||
  selections is xstate context
 | 
			
		||||
3 and 4) The XState handler for the above uses handleSelectionBatch and
 | 
			
		||||
  handleSelectionWithShift to update the selections in xstate context as
 | 
			
		||||
  well as returning our SelectionRangeTypeMap and a codeMirror specific
 | 
			
		||||
  event to be dispatched.
 | 
			
		||||
5) The codeMirror handler for changes to the cursor uses
 | 
			
		||||
  processCodeMirrorRanges to associate the ranges back with their original
 | 
			
		||||
  types and the entity ids (the id can vary depending on the type, as
 | 
			
		||||
  there's only one source range for a given segment, but depending on if
 | 
			
		||||
  the user selected the segment directly or the vertex, the id will be
 | 
			
		||||
  different)
 | 
			
		||||
6) We take all of the ids and create events for the engine with
 | 
			
		||||
  resetAndSetEngineEntitySelectionCmds
 | 
			
		||||
 | 
			
		||||
An important note is that if a user changes the cursor directly themselves
 | 
			
		||||
then they skip directly to step 5, And these selections get a type of
 | 
			
		||||
"default".
 | 
			
		||||
 | 
			
		||||
There are a few more nuances than this, but best to find them in the code.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
export type Axis = 'y-axis' | 'x-axis' | 'z-axis'
 | 
			
		||||
 | 
			
		||||
export type Selection = {
 | 
			
		||||
@ -93,7 +35,9 @@ export type Selection = {
 | 
			
		||||
    | 'default'
 | 
			
		||||
    | 'line-end'
 | 
			
		||||
    | 'line-mid'
 | 
			
		||||
    | 'face'
 | 
			
		||||
    | 'extrude-wall'
 | 
			
		||||
    | 'start-cap'
 | 
			
		||||
    | 'end-cap'
 | 
			
		||||
    | 'point'
 | 
			
		||||
    | 'edge'
 | 
			
		||||
    | 'line'
 | 
			
		||||
@ -106,15 +50,6 @@ export type Selections = {
 | 
			
		||||
  codeBasedSelections: Selection[]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface SelectionRangeTypeMap {
 | 
			
		||||
  [key: number]: Selection['type']
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface RangeAndId {
 | 
			
		||||
  id: string
 | 
			
		||||
  range: SourceRange
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function getEventForSelectWithPoint(
 | 
			
		||||
  {
 | 
			
		||||
    data,
 | 
			
		||||
@ -139,8 +74,32 @@ export async function getEventForSelectWithPoint(
 | 
			
		||||
      },
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  const sourceRange = engineCommandManager.artifactMap[data.entity_id]?.range
 | 
			
		||||
  if (engineCommandManager.artifactMap[data.entity_id]) {
 | 
			
		||||
  const _artifact = engineCommandManager.artifactMap[data.entity_id]
 | 
			
		||||
  const sourceRange = _artifact?.range
 | 
			
		||||
  if (_artifact) {
 | 
			
		||||
    if (_artifact.commandType === 'solid3d_get_extrusion_face_info') {
 | 
			
		||||
      if (_artifact?.additionalData)
 | 
			
		||||
        return {
 | 
			
		||||
          type: 'Set selection',
 | 
			
		||||
          data: {
 | 
			
		||||
            selectionType: 'singleCodeCursor',
 | 
			
		||||
            selection: {
 | 
			
		||||
              range: sourceRange,
 | 
			
		||||
              type:
 | 
			
		||||
                _artifact?.additionalData.info === 'end'
 | 
			
		||||
                  ? 'end-cap'
 | 
			
		||||
                  : 'start-cap',
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
        }
 | 
			
		||||
      return {
 | 
			
		||||
        type: 'Set selection',
 | 
			
		||||
        data: {
 | 
			
		||||
          selectionType: 'singleCodeCursor',
 | 
			
		||||
          selection: { range: sourceRange, type: 'extrude-wall' },
 | 
			
		||||
        },
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return {
 | 
			
		||||
      type: 'Set selection',
 | 
			
		||||
      data: {
 | 
			
		||||
@ -148,46 +107,17 @@ export async function getEventForSelectWithPoint(
 | 
			
		||||
        selection: { range: sourceRange, type: 'default' },
 | 
			
		||||
      },
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (!sketchEnginePathId) return null
 | 
			
		||||
  // selected a vertex
 | 
			
		||||
  const res = await engineCommandManager.sendSceneCommand({
 | 
			
		||||
    type: 'modeling_cmd_req',
 | 
			
		||||
    cmd_id: uuidv4(),
 | 
			
		||||
    cmd: {
 | 
			
		||||
      type: 'path_get_curve_uuids_for_vertices',
 | 
			
		||||
      vertex_ids: [data.entity_id],
 | 
			
		||||
      path_id: sketchEnginePathId,
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
  const curveIds = res?.data?.data?.curve_ids
 | 
			
		||||
  const ranges: RangeAndId[] = curveIds
 | 
			
		||||
    .map(
 | 
			
		||||
      (id: string): RangeAndId => ({
 | 
			
		||||
        id,
 | 
			
		||||
        range: engineCommandManager.artifactMap[id].range,
 | 
			
		||||
      })
 | 
			
		||||
    )
 | 
			
		||||
    .sort((a: RangeAndId, b: RangeAndId) => a.range[0] - b.range[0])
 | 
			
		||||
  // default to the head of the curve selected
 | 
			
		||||
  const _sourceRange = ranges?.[0].range
 | 
			
		||||
  const artifact = engineCommandManager.artifactMap[ranges?.[0]?.id]
 | 
			
		||||
  if (artifact.type === 'result') {
 | 
			
		||||
    artifact.headVertexId = data.entity_id
 | 
			
		||||
  }
 | 
			
		||||
  return {
 | 
			
		||||
    type: 'Set selection',
 | 
			
		||||
    data: {
 | 
			
		||||
      selectionType: 'singleCodeCursor',
 | 
			
		||||
      // line-end is used to indicate that headVertexId should be sent to the engine as "selected"
 | 
			
		||||
      // not the whole curve
 | 
			
		||||
      selection: { range: _sourceRange, type: 'line-end' },
 | 
			
		||||
    },
 | 
			
		||||
  } else {
 | 
			
		||||
    // if we don't recognise the entity, select nothing
 | 
			
		||||
    return {
 | 
			
		||||
      type: 'Set selection',
 | 
			
		||||
      data: { selectionType: 'singleCodeCursor' },
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getEventForSegmentSelection(
 | 
			
		||||
  obj: any
 | 
			
		||||
  obj: Object3D<Object3DEventMap>
 | 
			
		||||
): ModelingMachineEvent | null {
 | 
			
		||||
  const group = getParentGroup(obj, [
 | 
			
		||||
    STRAIGHT_SEGMENT,
 | 
			
		||||
@ -231,107 +161,54 @@ export function handleSelectionBatch({
 | 
			
		||||
}: {
 | 
			
		||||
  selections: Selections
 | 
			
		||||
}): {
 | 
			
		||||
  selectionRangeTypeMap: SelectionRangeTypeMap
 | 
			
		||||
  codeMirrorSelection?: EditorSelection
 | 
			
		||||
  engineEvents: Models['WebSocketRequest_type'][]
 | 
			
		||||
  codeMirrorSelection: EditorSelection
 | 
			
		||||
  otherSelections: Axis[]
 | 
			
		||||
  updateSceneObjectColors: () => void
 | 
			
		||||
} {
 | 
			
		||||
  const ranges: ReturnType<typeof EditorSelection.cursor>[] = []
 | 
			
		||||
  const selectionRangeTypeMap: SelectionRangeTypeMap = {}
 | 
			
		||||
  const engineEvents: Models['WebSocketRequest_type'][] =
 | 
			
		||||
    resetAndSetEngineEntitySelectionCmds(
 | 
			
		||||
      codeToIdSelections(selections.codeBasedSelections)
 | 
			
		||||
    )
 | 
			
		||||
  selections.codeBasedSelections.forEach(({ range, type }) => {
 | 
			
		||||
    if (range?.[1]) {
 | 
			
		||||
      ranges.push(EditorSelection.cursor(range[1]))
 | 
			
		||||
      selectionRangeTypeMap[range[1]] = type
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
  if (ranges.length)
 | 
			
		||||
    return {
 | 
			
		||||
      selectionRangeTypeMap,
 | 
			
		||||
      engineEvents,
 | 
			
		||||
      codeMirrorSelection: EditorSelection.create(
 | 
			
		||||
        ranges,
 | 
			
		||||
        selections.codeBasedSelections.length - 1
 | 
			
		||||
      ),
 | 
			
		||||
      otherSelections: selections.otherSelections,
 | 
			
		||||
      updateSceneObjectColors: () =>
 | 
			
		||||
        updateSceneObjectColors(selections.codeBasedSelections),
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    selectionRangeTypeMap,
 | 
			
		||||
    codeMirrorSelection: EditorSelection.create(
 | 
			
		||||
      [EditorSelection.cursor(kclManager.code.length)],
 | 
			
		||||
      0
 | 
			
		||||
    ),
 | 
			
		||||
    engineEvents,
 | 
			
		||||
    otherSelections: selections.otherSelections,
 | 
			
		||||
    updateSceneObjectColors: () =>
 | 
			
		||||
      updateSceneObjectColors(selections.codeBasedSelections),
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function handleSelectionWithShift({
 | 
			
		||||
  codeSelection,
 | 
			
		||||
  otherSelection,
 | 
			
		||||
  currentSelections,
 | 
			
		||||
  isShiftDown,
 | 
			
		||||
}: {
 | 
			
		||||
  codeSelection?: Selection
 | 
			
		||||
  otherSelection?: Axis
 | 
			
		||||
  currentSelections: Selections
 | 
			
		||||
  isShiftDown: boolean
 | 
			
		||||
}): {
 | 
			
		||||
  selectionRangeTypeMap: SelectionRangeTypeMap
 | 
			
		||||
  otherSelections: Axis[]
 | 
			
		||||
  codeMirrorSelection?: EditorSelection
 | 
			
		||||
} {
 | 
			
		||||
  const code = kclManager.code
 | 
			
		||||
  if (codeSelection && otherSelection) {
 | 
			
		||||
    throw new Error('cannot have both code and other selection')
 | 
			
		||||
  }
 | 
			
		||||
  if (!codeSelection && !otherSelection) {
 | 
			
		||||
    return handleSelectionBatch({
 | 
			
		||||
      selections: {
 | 
			
		||||
        otherSelections: [],
 | 
			
		||||
        codeBasedSelections: [
 | 
			
		||||
          {
 | 
			
		||||
            range: [0, code.length ? code.length : 0],
 | 
			
		||||
            type: 'default',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
  if (otherSelection) {
 | 
			
		||||
    return handleSelectionBatch({
 | 
			
		||||
      selections: {
 | 
			
		||||
        codeBasedSelections: isShiftDown
 | 
			
		||||
          ? currentSelections.codeBasedSelections
 | 
			
		||||
          : [
 | 
			
		||||
              {
 | 
			
		||||
                range: [0, code.length ? code.length : 0],
 | 
			
		||||
                type: 'default',
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
        otherSelections: [otherSelection],
 | 
			
		||||
      },
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
  const isEndOfFileDumbySelection =
 | 
			
		||||
    currentSelections.codeBasedSelections.length === 1 &&
 | 
			
		||||
    currentSelections.codeBasedSelections[0].range[0] === kclManager.code.length
 | 
			
		||||
  const newCodeBasedSelections = !isShiftDown
 | 
			
		||||
    ? [codeSelection!]
 | 
			
		||||
    : isEndOfFileDumbySelection
 | 
			
		||||
    ? [codeSelection!]
 | 
			
		||||
    : [...currentSelections.codeBasedSelections, codeSelection!]
 | 
			
		||||
  const selections: Selections = {
 | 
			
		||||
    otherSelections: isShiftDown ? currentSelections.otherSelections : [],
 | 
			
		||||
    codeBasedSelections: newCodeBasedSelections,
 | 
			
		||||
  }
 | 
			
		||||
  return handleSelectionBatch({ selections })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type SelectionToEngine = { type: Selection['type']; id: string }
 | 
			
		||||
 | 
			
		||||
export function processCodeMirrorRanges({
 | 
			
		||||
  codeMirrorRanges,
 | 
			
		||||
  selectionRanges,
 | 
			
		||||
  selectionRangeTypeMap,
 | 
			
		||||
  isShiftDown,
 | 
			
		||||
}: {
 | 
			
		||||
  codeMirrorRanges: readonly SelectionRange[]
 | 
			
		||||
  selectionRanges: Selections
 | 
			
		||||
  selectionRangeTypeMap: SelectionRangeTypeMap
 | 
			
		||||
  isShiftDown: boolean
 | 
			
		||||
}): null | {
 | 
			
		||||
  modelingEvent: ModelingMachineEvent
 | 
			
		||||
@ -349,41 +226,13 @@ export function processCodeMirrorRanges({
 | 
			
		||||
  if (!isChange) return null
 | 
			
		||||
  const codeBasedSelections: Selections['codeBasedSelections'] =
 | 
			
		||||
    codeMirrorRanges.map(({ from, to }) => {
 | 
			
		||||
      if (selectionRangeTypeMap[to]) {
 | 
			
		||||
        return {
 | 
			
		||||
          type: selectionRangeTypeMap[to],
 | 
			
		||||
          range: [from, to],
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      return {
 | 
			
		||||
        type: 'default',
 | 
			
		||||
        range: [from, to],
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  const idBasedSelections: SelectionToEngine[] = codeBasedSelections
 | 
			
		||||
    .flatMap(({ type, range }): null | SelectionToEngine[] => {
 | 
			
		||||
      // TODO #868: loops over all artifacts will become inefficient at a large scale
 | 
			
		||||
      const entriesWithOverlap = Object.entries(
 | 
			
		||||
        engineCommandManager.artifactMap || {}
 | 
			
		||||
      ).filter(([_, artifact]) => {
 | 
			
		||||
        return artifact.range && isOverlap(artifact.range, range)
 | 
			
		||||
          ? artifact
 | 
			
		||||
          : false
 | 
			
		||||
      })
 | 
			
		||||
      if (entriesWithOverlap.length) {
 | 
			
		||||
        return entriesWithOverlap.map(([id, artifact]) => ({
 | 
			
		||||
          type,
 | 
			
		||||
          id:
 | 
			
		||||
            type === 'line-end' &&
 | 
			
		||||
            artifact.type === 'result' &&
 | 
			
		||||
            artifact.headVertexId
 | 
			
		||||
              ? artifact.headVertexId
 | 
			
		||||
              : id,
 | 
			
		||||
        }))
 | 
			
		||||
      }
 | 
			
		||||
      return null
 | 
			
		||||
    })
 | 
			
		||||
    .filter(Boolean) as any
 | 
			
		||||
  const idBasedSelections: SelectionToEngine[] =
 | 
			
		||||
    codeToIdSelections(codeBasedSelections)
 | 
			
		||||
 | 
			
		||||
  if (!selectionRanges) return null
 | 
			
		||||
  updateSceneObjectColors(codeBasedSelections)
 | 
			
		||||
@ -486,24 +335,21 @@ export type CommonASTNode = {
 | 
			
		||||
  ast: Program
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function buildCommonNodeFromSelection(
 | 
			
		||||
  selectionRanges: Selections,
 | 
			
		||||
  i: number
 | 
			
		||||
) {
 | 
			
		||||
function buildCommonNodeFromSelection(selectionRanges: Selections, i: number) {
 | 
			
		||||
  return {
 | 
			
		||||
    selection: selectionRanges.codeBasedSelections[i],
 | 
			
		||||
    ast: kclManager.ast,
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function nodeHasExtrude(node: CommonASTNode) {
 | 
			
		||||
function nodeHasExtrude(node: CommonASTNode) {
 | 
			
		||||
  return doesPipeHaveCallExp({
 | 
			
		||||
    calleeName: 'extrude',
 | 
			
		||||
    ...node,
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function nodeHasClose(node: CommonASTNode) {
 | 
			
		||||
function nodeHasClose(node: CommonASTNode) {
 | 
			
		||||
  return doesPipeHaveCallExp({
 | 
			
		||||
    calleeName: 'close',
 | 
			
		||||
    ...node,
 | 
			
		||||
@ -521,7 +367,7 @@ export function canExtrudeSelection(selection: Selections) {
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function canExtrudeSelectionItem(selection: Selections, i: number) {
 | 
			
		||||
function canExtrudeSelectionItem(selection: Selections, i: number) {
 | 
			
		||||
  const commonNode = buildCommonNodeFromSelection(selection, i)
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
@ -547,7 +393,7 @@ export function getSelectionType(
 | 
			
		||||
  return selection.codeBasedSelections
 | 
			
		||||
    .map((s, i) => {
 | 
			
		||||
      if (canExtrudeSelectionItem(selection, i)) {
 | 
			
		||||
        return ['face', 1] as ResolvedSelectionType // This is implicitly determining what a face is, which is bad
 | 
			
		||||
        return ['extrude-wall', 1] as ResolvedSelectionType // This is implicitly determining what a face is, which is bad
 | 
			
		||||
      } else {
 | 
			
		||||
        return ['other', 1] as ResolvedSelectionType
 | 
			
		||||
      }
 | 
			
		||||
@ -590,3 +436,100 @@ export function canSubmitSelectionArg(
 | 
			
		||||
    })
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function codeToIdSelections(
 | 
			
		||||
  codeBasedSelections: Selection[]
 | 
			
		||||
): SelectionToEngine[] {
 | 
			
		||||
  return codeBasedSelections
 | 
			
		||||
    .flatMap(({ type, range, ...rest }): null | SelectionToEngine[] => {
 | 
			
		||||
      // TODO #868: loops over all artifacts will become inefficient at a large scale
 | 
			
		||||
      const entriesWithOverlap = Object.entries(
 | 
			
		||||
        engineCommandManager.artifactMap || {}
 | 
			
		||||
      )
 | 
			
		||||
        .map(([id, artifact]) => {
 | 
			
		||||
          return artifact.range && isOverlap(artifact.range, range)
 | 
			
		||||
            ? {
 | 
			
		||||
                artifact,
 | 
			
		||||
                selection: { type, range, ...rest },
 | 
			
		||||
                id,
 | 
			
		||||
              }
 | 
			
		||||
            : false
 | 
			
		||||
        })
 | 
			
		||||
        .filter(Boolean)
 | 
			
		||||
      let bestCandidate
 | 
			
		||||
      entriesWithOverlap.forEach((entry) => {
 | 
			
		||||
        if (!entry) return
 | 
			
		||||
        if (
 | 
			
		||||
          type === 'default' &&
 | 
			
		||||
          entry.artifact.commandType === 'extend_path'
 | 
			
		||||
        ) {
 | 
			
		||||
          bestCandidate = entry
 | 
			
		||||
          return
 | 
			
		||||
        }
 | 
			
		||||
        if (
 | 
			
		||||
          type === 'start-cap' &&
 | 
			
		||||
          entry.artifact.commandType === 'solid3d_get_extrusion_face_info' &&
 | 
			
		||||
          entry?.artifact?.additionalData?.info === 'start'
 | 
			
		||||
        ) {
 | 
			
		||||
          bestCandidate = entry
 | 
			
		||||
          return
 | 
			
		||||
        }
 | 
			
		||||
        if (
 | 
			
		||||
          type === 'end-cap' &&
 | 
			
		||||
          entry.artifact.commandType === 'solid3d_get_extrusion_face_info' &&
 | 
			
		||||
          entry?.artifact?.additionalData?.info === 'end'
 | 
			
		||||
        ) {
 | 
			
		||||
          bestCandidate = entry
 | 
			
		||||
          return
 | 
			
		||||
        }
 | 
			
		||||
        if (
 | 
			
		||||
          type === 'extrude-wall' &&
 | 
			
		||||
          entry.artifact.commandType === 'solid3d_get_extrusion_face_info'
 | 
			
		||||
        ) {
 | 
			
		||||
          bestCandidate = entry
 | 
			
		||||
          return
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      if (bestCandidate) {
 | 
			
		||||
        const _bestCandidate = bestCandidate as {
 | 
			
		||||
          artifact: any
 | 
			
		||||
          selection: any
 | 
			
		||||
          id: string
 | 
			
		||||
        }
 | 
			
		||||
        return [
 | 
			
		||||
          {
 | 
			
		||||
            type,
 | 
			
		||||
            id: _bestCandidate.id,
 | 
			
		||||
          },
 | 
			
		||||
        ]
 | 
			
		||||
      }
 | 
			
		||||
      return null
 | 
			
		||||
    })
 | 
			
		||||
    .filter(Boolean) as any
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function sendSelectEventToEngine(
 | 
			
		||||
  e: MouseEvent | React.MouseEvent<HTMLDivElement, MouseEvent>,
 | 
			
		||||
  el: HTMLVideoElement,
 | 
			
		||||
  streamDimensions: { streamWidth: number; streamHeight: number }
 | 
			
		||||
) {
 | 
			
		||||
  const { x, y } = getNormalisedCoordinates({
 | 
			
		||||
    clientX: e.clientX,
 | 
			
		||||
    clientY: e.clientY,
 | 
			
		||||
    el,
 | 
			
		||||
    ...streamDimensions,
 | 
			
		||||
  })
 | 
			
		||||
  const result: Promise<Models['SelectWithPoint_type']> = engineCommandManager
 | 
			
		||||
    .sendSceneCommand({
 | 
			
		||||
      type: 'modeling_cmd_req',
 | 
			
		||||
      cmd: {
 | 
			
		||||
        type: 'select_with_point',
 | 
			
		||||
        selected_at_window: { x, y },
 | 
			
		||||
        selection_type: 'add',
 | 
			
		||||
      },
 | 
			
		||||
      cmd_id: uuidv4(),
 | 
			
		||||
    })
 | 
			
		||||
    .then((res) => res.data.data)
 | 
			
		||||
  return result
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,13 +1,6 @@
 | 
			
		||||
import { PathToNode, VariableDeclarator } from 'lang/wasm'
 | 
			
		||||
import { engineCommandManager } from 'lang/std/engineConnection'
 | 
			
		||||
import {
 | 
			
		||||
  Axis,
 | 
			
		||||
  Selection,
 | 
			
		||||
  SelectionRangeTypeMap,
 | 
			
		||||
  Selections,
 | 
			
		||||
} from 'lib/selections'
 | 
			
		||||
import { Axis, Selection, Selections } from 'lib/selections'
 | 
			
		||||
import { assign, createMachine } from 'xstate'
 | 
			
		||||
import { isCursorInSketchCommandRange } from 'lang/util'
 | 
			
		||||
import { getNodePathFromSourceRange } from 'lang/queryAst'
 | 
			
		||||
import { kclManager } from 'lang/KclSingleton'
 | 
			
		||||
import {
 | 
			
		||||
@ -26,7 +19,6 @@ import {
 | 
			
		||||
} from 'components/Toolbar/EqualLength'
 | 
			
		||||
import { addStartProfileAt, extrudeSketch } from 'lang/modifyAst'
 | 
			
		||||
import { getNodeFromPath } from '../lang/queryAst'
 | 
			
		||||
import { CallExpression, PipeExpression } from '../lang/wasm'
 | 
			
		||||
import {
 | 
			
		||||
  applyConstraintEqualAngle,
 | 
			
		||||
  equalAngleInfo,
 | 
			
		||||
@ -45,10 +37,10 @@ import { ModelingCommandSchema } from 'lib/commandBarConfigs/modelingCommandConf
 | 
			
		||||
import {
 | 
			
		||||
  DefaultPlaneStr,
 | 
			
		||||
  sceneEntitiesManager,
 | 
			
		||||
  quaternionFromSketchGroup,
 | 
			
		||||
  sketchGroupFromPathToNode,
 | 
			
		||||
} from 'clientSideScene/sceneEntities'
 | 
			
		||||
import { sceneInfra } from 'clientSideScene/sceneInfra'
 | 
			
		||||
import { Vector3 } from 'three'
 | 
			
		||||
import { quaternionFromUpNForward } from 'clientSideScene/helpers'
 | 
			
		||||
 | 
			
		||||
export const MODELING_PERSIST_KEY = 'MODELING_PERSIST_KEY'
 | 
			
		||||
 | 
			
		||||
@ -70,6 +62,13 @@ export type SetSelections =
 | 
			
		||||
      selection: Selections
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
export interface SketchDetails {
 | 
			
		||||
  sketchPathToNode: PathToNode
 | 
			
		||||
  zAxis: [number, number, number]
 | 
			
		||||
  yAxis: [number, number, number]
 | 
			
		||||
  origin: [number, number, number]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type ModelingMachineEvent =
 | 
			
		||||
  | {
 | 
			
		||||
      type: 'Enter sketch'
 | 
			
		||||
@ -77,9 +76,24 @@ export type ModelingMachineEvent =
 | 
			
		||||
        forceNewSketch?: boolean
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  | { type: 'Sketch On Face' }
 | 
			
		||||
  | {
 | 
			
		||||
      type: 'Select default plane'
 | 
			
		||||
      data: { plane: DefaultPlaneStr; normal: [number, number, number] }
 | 
			
		||||
      data: {
 | 
			
		||||
        zAxis: [number, number, number]
 | 
			
		||||
        yAxis: [number, number, number]
 | 
			
		||||
      } & (
 | 
			
		||||
        | {
 | 
			
		||||
            type: 'defaultPlane'
 | 
			
		||||
            plane: DefaultPlaneStr
 | 
			
		||||
          }
 | 
			
		||||
        | {
 | 
			
		||||
            type: 'extrudeFace'
 | 
			
		||||
            position: [number, number, number]
 | 
			
		||||
            extrudeSegmentPathToNode: PathToNode
 | 
			
		||||
            cap: 'start' | 'end' | 'none'
 | 
			
		||||
          }
 | 
			
		||||
      )
 | 
			
		||||
    }
 | 
			
		||||
  | { type: 'Set selection'; data: SetSelections }
 | 
			
		||||
  | { type: 'Sketch no face' }
 | 
			
		||||
@ -109,18 +123,15 @@ export type ModelingMachineEvent =
 | 
			
		||||
  | { type: 'Equip Line tool' }
 | 
			
		||||
  | { type: 'Equip tangential arc to' }
 | 
			
		||||
  | {
 | 
			
		||||
      type: 'done.invoke.animate-to-face'
 | 
			
		||||
      data: {
 | 
			
		||||
        sketchPathToNode: PathToNode
 | 
			
		||||
        sketchNormalBackUp: [number, number, number] | null
 | 
			
		||||
      }
 | 
			
		||||
      type: 'done.invoke.animate-to-face' | 'done.invoke.animate-to-sketch'
 | 
			
		||||
      data: SketchDetails
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
export type MoveDesc = { line: number; snippet: string }
 | 
			
		||||
 | 
			
		||||
export const modelingMachine = createMachine(
 | 
			
		||||
  {
 | 
			
		||||
    /** @xstate-layout N4IgpgJg5mDOIC5QFkD2EwBsCWA7KAxAMICGuAxlgNoAMAuoqAA6qzYAu2qujIAHogC0AdgCsAZgB04gEyjhADnEA2GgoUAWJQBoQAT0QBGGuICckmoZkbTM42YWKAvk91oMOfAQDKYdgAJYLDByTm5aBiQQFjYwniiBBFtpGhlxDRphGg1ZURlhXQMEcRLDSVF5UwV84XEK8Rc3dCw8KElsCEwwHz9A4NCuXAjeGI5B3kTBLWFJDWV5hVSZZTrlBUKjS1FZiUrDeWVDasaQdxb8ds7ugFFcdjAAJ0CAaz9yAAthqNG4iaF07bCNLiRYlGiZUyZDbFYQzfIaLSQ5Sw0TGZQnM6eNodLoEW73J6wV7sD5UQyRZisMbcP4IQRrGiSdRmfIqUxrCrQlbKcoQ4RHDTWUx5DHNLGXXHXPjsB4AVwwX0psXGCSEFThkNEymFGlECkM0MM+0ZMgUa1MwuypgRhlFHlaEpufBYD3YiuiVN+qrpdg0klMwkFaU0ermwuhwlUkiBhxUyKUazt5za3mJH2IZEomFTb0+9BGnpVoESRrNkmMga0ahMplkhqOZVL+sM4ksCj1SfFOZJ70k3Y+AEkrj0AkEugNwvnvoWad7DIGyuqOe2rPNDRCLIKaKJa6JBXrRJ2Hf3eyeh7jkCRXn0oABbMB3fwAN0enHIJEw7p+Rf4RhkpkbapNEjTRUgKfQjA0FsUnBOQJHyJQNCPC4Tz7NN3nPbpL2vII7wfAJ3lQB5sAAL24dgPy-Gd4mLP9A0kVQzW3I18mRGRDXEI1yl1LJDBWYwKhkZCU3QtDc0w4huFgGUSDwfxCOIsi7g-fwIGwaTMzAKjlVnWiECsPdNzSGgqnSAD1GhNJ2QsCpRHBXUAJbYSxJ7FzB2HIgpJkuSX1dbB30wVT1IoigtKnJVqRo399OWBR-TyFdlCgtRBUs1lymsAVYRjPdnNQs8PK8h5ZNwfwAEEACFvH8AANbTItpKx21mcQxEMGxOP-NJLJ1eL41gndagaVxTjFY9RIK3FPNwaTirkyrqoATXqr09Ka7ZrDsZQ5H2ZQtXYiDijUOKgVsvi1ErPKJvQiTptmkr-DIKAuhWn8S3SKQQRBU15H1YRTEsuy4QPbKtX+gMrtzNyMMKmbvNKrp8HYPMKQ9HSove1rpD21QANkTjREsw4Tu1KD-oRBNhEh1zJu6O74f8JhHiZ3A1PIWVMBIJ41I00LXt06KrFNWYLU6jIjlUZRLP1P1+SqAMgQRDQxGpj5oduoqHoU0jyI-TA9EenAoCGcK0YaudlhkJlQ3bAEMhWQ1UR5f9Q1hfItRKVXTxu2H7p819-L1g2P2wY3+YxujzByU1-qdsxUUNbbtj1AMqjWf9HGGpp7RQ67xN9hnYFwEgmH8dhUFq8PGs4qRlhBNQA2qMxLOyK2ZDkeZ5m3Rx2699WC7m0qi5LsuK+W03vwF97oMyfjYVa4wNEs9qZj3Hud3nOYcj72nJLhwf-DAABHWUVMRqBkari3a3KFf-qyPIJHAop51bFqTHBbccdMHefamzW5JMC5nragE9qLV3yBYdu-EdzpE3oaWsMwTCcUOBaTIahDwjUxONKGu96YHweGAW8qAXz+HIAAu4sAr5rWsHFQ41h9qaH2DoQ6RxOIWD4rYbIYIzRCSwWNXOuC-7dAAEpgEEGAPgIRZT3GoYLOoUgWyGFrKkdIYgE6HXUFbdIqRkSdwTLafhOcRJCPzpKE+2BS4ABk8BgFHqgT8YD0aNQUNZCQKgbDCnSBZTRdReSK3bnuPaRpf5mJuBY0uIUYB3GwCpLm5BR5yMSFtOKIIPZ8V+go+slhoxrFUJkhESU+5lQAO6yQIkRHWylAo8xCpQfweAABmqACAQG4GAdouAnyoFeJIGA7BBDayUhRTAggmmoCSYgYyfpCkZADGYBW4hLJQXMO2U0aROEdypkY5M0NJClPKfJSpwyVK1M0g03AzSCCPAeERSQTBObsGaQ8W8fS-CDOObrUZ4zJkIGMtsI0NoWy6lNAGSy31Nw0FUCYPIkZDHZ12ahA5HBnwBwCkFXm9TxmtPaZ07pvT+mCF8m+D8YzLkTKcebPSGyrYtkUPYZEyj8hpSUEyMQcg7ClCBMUspKLiWBxqcFc52Kbl3IeSQJ5RFXmEv5QFMlzTfk0qZNuMQAExCaDSoKcsONVFaOZDyw5C1aoXKuW03AHS8D4o6YSkgAAjWAgg+DyopajSeEc-koNyTIWCcg6jrEOv+ba5YMhQSSrIbatQDUoqNTVE1LTRUPHuY8550r3l2odU6n5lLVrRQ2TMHIIIlA7m3FoHqfFpB6j2pxbIgYf47K7KJZFAQjWLTjTi81eKenWrTfawQehnWKs9XUeQFRbCIN1NCVsyIRauLsnqVsuV604Nck28qVV-CtpFQ8W5ibxWSpeW8gZ6a+0DuzW9KZnEkGsVassFYobJ18T9FC9k20do3qjQEJ6XQ21motV0rth7BBfvEVm114DvR5pSEaOocgLQaKKHBHkKi7JGgqGaWEH7Hr4G-VundSaJUpsA8B09YHnEQc4idbITUrAN3nIDec5YWzyFSHO5Y2yEUNqhqu8+yMf24stQBwlPH3gkYLGR6lBMWpWTJluZlAaOQWBWJelQ84rCYeE22hN+H92poGcJ0T05xO5s9cYGF+o1jKwOghpKVt5i0OtGIFE6Il2CJXbygITMHgszZhzLmGK6l2Oxb+ztBL3mee8-5XzDxBBnNCgZiKObkmW3ikoawr706EwDdUbYC85h7Tg76zD4WHw+c5tzIVoVNPbrFcmqVgHius0i2VmLFXKDxbNolqZyXhSpbmNA7LllsrSH5HlncwpCsuZMa5Gx5r7GYBHH0cccRFXeqjjRo4mQjh5CWQGgM5YesmVoYxPhHHl1qxm3Y8uDi8QRLLk9fCsTArxMSWeqeUzYR+n2GYcE6h5ZS0OgrFqrVHBZHZH3C7c3JADlwBwAgK2VBMjSRt60sGuT6kkMGWskJWr-TSOD2xkPoew7JKRqlub8hWzsqaR9NHtQRn+rMFYEhbBakhNufHs2ruYEkLgKVH4FtjhCMt177rrC1GkKafUvCMhan+0UJKMwFzKz1O1KCnEOeXdQA4yQAA5CuAAFVAeB2CwAIGVCAEBAgUVdIzI3dxFXzD9JqfLgpFhJS5NqFqqcsiKCUAoDXkO9f+EN8b03pBQqONJ51-SmhzAmGDKtrQep-VFF4iLVBpRTQrD7kT9gcOReNUsDyJPWQVDbkT4aYEjPIzLChboqCOeYd55J2JsnJZmJMkhJCOYshwTtUrwBaMts5irg6s507rm1YABV7sxLiQ8BJ5cBf9GF1H89fztrmG1EoLxh3-yV6gkyR93rLC6g7JNvZM-8APfn4vlp1xbsQ65w7hE79FA7jsttaE1oeRuzSMxqFOYdjUaYxPZWUVmCuDSV0E8AAeVwHbT-StX2W8Cn0EHALaUECgPYFgJNjXze30nxkZxMF1BMkyEWGfkQB3DihXDQzpU8UwQnymw+H8F538EaRIEoB6CW1UjAHYI5g805nNV+UEHanMDSRhSNEDAkGUS5DkH9BsEODmEsDgmAOwQuDIGwFvAlVaFHkZkEO6GCwE16Q0K0PuEEHLkEHYMoF+QqEAjIOyB9ShEOm2lSSsDBFBUDHUGchMO0PwF0PFXNWXyW0GEVSBEYw5R3EYkDGhDXnLFWDsHBBWBqG8Jh1MJ0PLkPj4GCh0KJFzAQJCw6R8LMIsNyJ7BsL2gYisFqAtHbjSFhGhCghywSkAN9BcKQhOF5wwHgCiDUKgFb2j0EEWDKFyEUDL3UC0B2yKEGJNC1EsFqCFGMDNGchxDAH6PX3pC0HKGRHmRDGAmhHUEXFbH1GkOMmUS9jWPwPZDihLxMHyQr0OiQQWGtEGlsHmAwwv3ymEQuPdRbAtAxySn1GB19UyxfiZTiNalSGqAAmhMwyGS+X82FXJW+JcTWFmDmUUH5BqFSHBWSABFbChW1HyHhRAMRUbXc1RT8nRViyxSRMMzbyMC0W1T7wUU3wNADTkEZFSCghlnmFsMwxjTjWRLnFcSkDUE6lrG2lMwBl23YV1HjFUHOj1BOxJM4zc0NXXU3VpIS3XyOGSAShyA21SkOinQ2jghHSqChMw2A0FLpOj11O2BWHshKCqDyFrEBjUCZGDAqFUBrzqHUwfAvneBtO1PwN1MZAyFWBVwlLXHkwR3+mUUcG2LlKzhVLO17FXQa1Kz82pMCy1I6x1PbF-3ZDslAgUJKEG3RzlnbAAnVDBw+NEify10wCFOpXsAsCGnbBsHUDWBiKjG5MEmQVtnH1TMn17EbO11zxbNzX2DKEyBKE7ItJ7OcOSD4jvR72721ADy5x5z52bNtPXyFHzXnn-GrFkCs0QDTwUOUUzzNFkC3KbN1wNztxNynOSV1FriUHan1HjxUEmIvKDWp2gyOEjC0GJN6L2UnP3NDO3C311EFAjJ3371YQbEYwOJbDbHP0YMv1n04Fv1HlfKmVDGjBjBUB+nBnXDyH+LNA5AfgMj7nQMgOt2wPQjgIIv0nNHKDqCTyZ1S2lKKCCUZyZTP3jC1C9hYIrisNWKgvdSmDkIkBWVUDqH+i-2cLkKgnsDnQmNNBSM0N8KgH8P0LYr4ikADG1CtHkFp1kJ5BMHUEjDXiONUIETaCKPSIrkkWyL8NKI+CMscAx1hCYlanZE5UnUjF5HsHIMswAnaKwpcr8IyICLsQAApyEmA9AABKNikQCQCXTJRiTQFhIoHhRTF9QJOlZU8C2K-S+K-Q-wZK1AVKtK2qlK9KzKsQT6OQUYvKiYydBESnHcawA0muYUFwFwIAA */
 | 
			
		||||
    /** @xstate-layout N4IgpgJg5mDOIC5QFkD2EwBsCWA7KAxAMICGuAxlgNoAMAuoqAA6qzYAu2qujIAHogC0AdgCsAZgB04gEyjhADnEA2GgoUAWJQBoQAT0QBGGuICckmoZkbTM42YWKAvk91oMOfAQDKYdgAJYLDByTm5aBiQQFjYwniiBBFtpGhlxDRphGg1ZURlhXQMEcRLDSVF5UwV84XEK8Rc3dCw8KElsCEwwHz9A4NCuXAjeGI5B3kTBLWFJDWV5hVSZZTrlBUKjS1FZiUrDeWVDasaQdxb8ds7ugFFcdjAAJ0CAaz9yAAthqNG4iaFlmZWZSmZRaKyGQzKYQaDYIGSmDSzdKyDSGcQ0KrAk5nTxtDpdAi3e5PWCvdgfKiGSLMVhjbh-BCCaw0WaQrZqdGLAr6RAZGSSWzCZSiIEKNFi5TY5q4y4E658dgPACuGC+NNi4wSQgqM3hNFEwNEGlEYthENELJkCjWplMFpsGlRUo8rVlNz4LAe7DV0Vpvy1jKtCkkVuZMjsDpNsOEwjKUIkcxNqWhomd5za3jJH2IZEomEzb0+9BGfs1oESEOtkmM0K0ahMplkZqOZUrYrRlgUJrTMoL5Pekj7HwAklcegEgl0BuFi99S-SA4ZoWUdWsTSLlsozZlzNkZPrG0a5F2e66hwPz6OCcgSK8+lAALZgO7+ABuj045BImB9PzL-CMeFW2qTQhU0ZMzVRKR0RoVJ6nyJQNFPC5z0HLN3ivbobzvIJH2fAJ3lQB5sAAL24dhv1-ed4nLQDoUkVRrX1CF8ihGQzXECFymNLJIU4-U8mQjN0LQwtMOIbhYEVEg8H8QjiLIu5v38CBsCk3MwCojUF1ohArCNCwNDSDFENMI51h5OEzGUCwKgtbJRDMtEhNE-tXJHMciEk6TZPfL1sC-TAVLUiiKE02d1TpGiAL05ZgztK0RVBYxNHYyzw1qcprCODQY2EZYjRc1DL087yHhk3B-AAQQAIW8fwAA0tKihkrC7JExEMGxOPhNJYXhAy7ShRY4NMWoGlcU5pTPESSoJLzcCk8rZNq+qAE1mv9XS2u2aw7GUOR9mFA7YU5YN8rstlHEdIrZvQ8SFqWir-DIKAuk2-8K2RaQlBA+QxWEUx+otXV1zyg1AbG27C3cjDSsWnzKq6fB2CLalfW06KvvEGYVANDE0TSfZ+sOc7gVRQHHSUKFobcubukexH-CYR4WdwVTyCVTASCeVT1LCj6dJiqwrVmW0eoyI5VC3dKxURWMqjG-LHVy1NJpxGaYfpiSEeWyr5NI8jv0wPQXpwKAhgijGWsXZZ+S7TQu3SI0aBWM0kpDO1QPysQVgmpoXRQu6xPhp7fI-ALjdN79sAtwWsbo8wcitQGkrMEUzQO7YTTGzFqiqWpaY+WGHrK57YFwEgmH8dhUEa+PWs4qRlnERYFDG6ozH6vcQzkeZ5n1RxwyLi97tDpmK6rmu642q2-yFr60QsIVOJjHHjBhdKupmI0h8cpc5hyEeS-HvX-DAABHJVlORqBUYb23G3KbfAayPIJG5Iol3RJETFg-VjqmGPtrRmZ8mA82NtQOe1FG75AsOGFY+xGwU03l-RsMwTCcUOLaTIag1YB3TLDE+80y6yQeGAB8qB3z+HIKQu4sAH7bWsMGQ41gDRWlRCacQZolBlEsMCfcOROQHWAWPAkAAlMAggwB8BCEqe4jDhZ1CkGiVQ+kdQGlhCCGyjk14misGZbIoiQ5yivtgauAAZPAYBp6oB-NAzGrV246JKKCW0CZ1CwiUNnTIe05CJghMY-s4lrhmOrqFGAdxsDKR5uQaeijEj7WDK3A0WD-rKObJYSQYFVDpMdKCY+VUADuMkCJEUNkpIKfNQqUH8HgAAZqgAgEBuBgHaLgV8qBXiSBgOwQQBtFIUUwIIBpqAEmIGMoifJGQxpmCVtw9KqJzBdlDFg2wENCklI4HJcpgzlLVI0nU3AjSCCPAeERSQTBubsEaQ8B8PS-D9N2UbYZozxlWX1NWLqqI0TGitGNfqrd+TZFdrBOorFDCbNKW+COgVgr81qaM5prT2mdO6b0wQflPzfhGccsZDiba6SJvyNEih7BQkMIKfqVopCOHfnYUo+UoXbKxZHKpIVDlIrORcq5JAblEXuRi1lgVcWNPecSyQix5D7zEJoalRlqzHVSK3ECShmUBFWo1I5JyWm4DaXgNFbSMUkAAEawEEHwUV+L0bzwTlZSE2SDqwXDLsCyRR4QHWrBkVEoJZAHULuraaQcYbFOhZqhq2qmncoeJc65tzBWPNNeay1byCVbRikTGYQj1B1FMPqLQ-UzI2TqNaCQG9oRAMDYHYSIatkarqv4NakbkV6tRV0o1iazWCD0Fa8VWDpAaMcvCHGxpTquyzYrC0XD83quqg2ptXKHjnJjby-ldyHl9KTd23tabPoTNXvAle+V5jpEhKdSEiJXbaJdaSmQs7XpdGbbq-VHT20bsEA+qRqabUwIDJmlIEJwWOTtIYYGBoBTogtOaE0UJhD3vwI+xdy7Y18vje+z9O6f2OL-Zxc62Q2pWDUGZT+EyRSAjRPIOC+oASztvqjJ9KKDVvoxXR94mGSzYaJZxbYyczAoP3PkEm7VXaMpUEuKwtHnx33eM26NKG10Jr6ax9jc5OMZv7cYEw+i1i5TSu60E-J5jMIRGIGMBpZ0sweGzDmXMebwpqTYpFz623oseZZ6zAVbMPEEAcsKKnIrpsSXbAUXDrAHTinIfq1RtjrzmMKdxch-ZTWrUQ0N2z3PPhs9zXmHKwqyaXTyuNAr30ZfZp57LPncuUH89bQLEzgtexROFtYkX0p5WkLGOLwH35JY1sGtyVi9W2MwOOPoU44jiv3EnQjRxMhHDyAs91Y1qxewxMwxid6q2ENQoNmxtc7GEjCTXV6+FolBVifE3dC8JkxkRPsMwsF1CKxlkUJWSIcaOCyCCY+u3huSGHLgDgBBJsqElSk2bCI5BA0smsMoaR24PZxoDNIP3rF-YB0DykWHCUZvyPyC0VoL2EeBNGQGswVgSHWcCWC+DkvbZEr9-bmBJC4AFd+Ubk4QgTau3a6wmVZDqClsyA0L3ECghmMuVWcsoKQq272BnaOmeSAAHJ1wAAqoDwOwWABAqoQAgIECiXpmaa7uOK+YiI80GkckZRYoJYTzCTmYV+mQc0KFR0NpXqv-Aa61zr0gYV7HY7q3pTQu5ZBAoxFoE0brEC8TFtg0oVoVjHwx+wYHPPWr8NmF2LIKhqNR7NGkKQcwhTLFBcsJ0cvNZuTT8DqkHGccVmYpKvNea5iyFgl1IvZlslOzmECbqkpq-9eLgAFRO1EmJDw4m1w5-0bnwe91wgOuYYESg7TolsPCIvqJJUXv3JYY03YR81rchP-Ap3p+z6aaEpU5j-CM9QHY83jpf6KEchaE6lkEQ2RjOGOoLIV2UvY+JUdmOudSL0c8AAeVwBbRfUNUkCqm8DH0EDAJaUEEgPYBgMtiX2uz0hKGDDyFqCckdDqDUFhCTAFGwTUFylbhKDg1P1hn8FZ38HqRIEoB6HGxUjAHYK5gCF5T1XeSZC4iOARCRy0ChBsFEAd2WH3zSByEhiFBsBHhYLrnYM4N8AnAX0GHeSFC+R0wkDmWyEOFhAdAgxKDsCUAGhXhcjIGwAfD5VaGnmZm5j1XgJczaXsMcPuEEFrkEA0PCjwLtQqGAldxBSWEyFkOSSsBKFSHbmhHUDsMBx8OcNrlcLIG6C0LGy510Mzz-Xyi+TkCh0YmhEoK0GrFWDsFghWBqGSIcKcPwBcJkRCmcNJELA8KY26W8L5SkX8PaP7HeSzngX9SqH2EUL00QFwwYihFbhBCe1yndxOFZwwHgCiD6ygEbxD0EEWDKFyEUHz3UC0EWyEEWElT3ByE03UCyCQiYPxDAC2OX0EFBCIKhFmU0BNFyi8TFHKE5Hu1iMsErQIXl0LEePwPmJz0o3zzyELxhwYjtDAnZBXm+yYOKjEQeNUybyMAyGbjGlylsA+MBljz0kpTu1RGMj9jz16yDTP2LjSzKQUheXs05TxTBLtSlmDEdAxEUFjBqFSEBWSGdhgkd3yFl2BJrzpLrRhX8jhV80RVZMxJD3MhJVUEsGUVX1A3SjkBZFSFRDlnmFCNnXDUjTZKcSfg5AMRUGqIxELU4h2GGjUVyWOFRJEnpLnXWhNMVOXzEPxw4Vwz3DHWFF7jsjECqHzngzekcwVIC29Pbm2BWFgiESqDyEbGBjUElXhwqFUDLzqEkxRhk1GVNMXHbhZBxOtzFEbH7hJlB0BkpUcDeONBTxdNrWhVKyyzszlKjMaSLO2i7D-xBAtHAhsD9n6jJWyTrP3h1BRPFNHwHCfzsR7IzXsGXkIKNDDLWEoNUBDFREPnX0dGnLpxBIG0V2f2ZzT0XMSX2D4XGi7BsHUA3Jh2SEhBWHyQezmCBMPIlLnJPLsRZzZ0wAvImRsCzTXj1EWFkEmIQHj2HMpST2tFkA9z21PJV3V1N210ArhGNGbl4TBBMDSDdksjLxDHbDRCOGULFFT0B3YAwuMEcgYmNCMhxI3270skrFbHMkA07BPxnNpIHAv0iU4Gv2ngwuPHlmPStNz1tG3DyBDDWAWEow3Fpw2KIXQIgKN2wPQlgJoptF+N3lSVC2hyKCNBsgH1sGP2GnM2bP7DULYI4IxJjPwKZBKAYhNAEyMgM35MsjoJDHYRBBqBWEbHqNSKaPSMEPstq29JWAFCFHbwROJ1kJshMGuOFGOLUEYJ4skB6M4BCrrhaKkjaPQhouW19mFCyAjE4lsCixZAHhLOZGtCtBcBcCAA */
 | 
			
		||||
    id: 'Modeling',
 | 
			
		||||
 | 
			
		||||
    tsTypes: {} as import('./modelingMachine.typegen').Typegen0,
 | 
			
		||||
@ -135,11 +146,14 @@ export const modelingMachine = createMachine(
 | 
			
		||||
        otherSelections: [],
 | 
			
		||||
        codeBasedSelections: [],
 | 
			
		||||
      } as Selections,
 | 
			
		||||
      selectionRangeTypeMap: {} as SelectionRangeTypeMap,
 | 
			
		||||
      sketchPathToNode: null as PathToNode | null, // maybe too specific, and we should have a generic pathToNode, but being specific seems less risky when I'm not sure
 | 
			
		||||
      sketchEnginePathId: '' as string,
 | 
			
		||||
      sketchDetails: {
 | 
			
		||||
        sketchPathToNode: [],
 | 
			
		||||
        zAxis: [0, 0, 1],
 | 
			
		||||
        yAxis: [0, 1, 0],
 | 
			
		||||
        origin: [0, 0, 0],
 | 
			
		||||
      } as null | SketchDetails,
 | 
			
		||||
      sketchPlaneId: '' as string,
 | 
			
		||||
      sketchNormalBackUp: null as null | [number, number, number],
 | 
			
		||||
      sketchEnginePathId: '' as string,
 | 
			
		||||
      moveDescs: [] as MoveDesc[],
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
@ -160,7 +174,6 @@ export const modelingMachine = createMachine(
 | 
			
		||||
            {
 | 
			
		||||
              target: 'animating to existing sketch',
 | 
			
		||||
              cond: 'Selection is on face',
 | 
			
		||||
              actions: ['set sketch metadata'],
 | 
			
		||||
            },
 | 
			
		||||
            'Sketch no face',
 | 
			
		||||
          ],
 | 
			
		||||
@ -504,6 +517,11 @@ export const modelingMachine = createMachine(
 | 
			
		||||
            target: 'animating to plane',
 | 
			
		||||
            actions: ['reset sketch metadata'],
 | 
			
		||||
          },
 | 
			
		||||
 | 
			
		||||
          'Set selection': {
 | 
			
		||||
            target: 'Sketch no face',
 | 
			
		||||
            internal: true,
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
@ -532,15 +550,15 @@ export const modelingMachine = createMachine(
 | 
			
		||||
          {
 | 
			
		||||
            src: 'animate-to-sketch',
 | 
			
		||||
            id: 'animate-to-sketch',
 | 
			
		||||
            onDone: 'Sketch',
 | 
			
		||||
            onDone: {
 | 
			
		||||
              target: 'Sketch',
 | 
			
		||||
              actions: 'set new sketch metadata',
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
 | 
			
		||||
        entry: 'clientToEngine cam sync direction',
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      'animating to plane (copy)': {},
 | 
			
		||||
      'animating to plane (copy) (copy)': {},
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    initial: 'idle',
 | 
			
		||||
@ -562,13 +580,13 @@ export const modelingMachine = createMachine(
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    guards: {
 | 
			
		||||
      'is editing existing sketch': ({ sketchPathToNode }) => {
 | 
			
		||||
      'is editing existing sketch': ({ sketchDetails }) => {
 | 
			
		||||
        // should check that the variable declaration is a pipeExpression
 | 
			
		||||
        // and that the pipeExpression contains a "startProfileAt" callExpression
 | 
			
		||||
        if (!sketchPathToNode) return false
 | 
			
		||||
        if (!sketchDetails?.sketchPathToNode) return false
 | 
			
		||||
        const variableDeclaration = getNodeFromPath<VariableDeclarator>(
 | 
			
		||||
          kclManager.ast,
 | 
			
		||||
          sketchPathToNode,
 | 
			
		||||
          sketchDetails.sketchPathToNode,
 | 
			
		||||
          'VariableDeclarator'
 | 
			
		||||
        ).node
 | 
			
		||||
        if (variableDeclaration.type !== 'VariableDeclarator') return false
 | 
			
		||||
@ -621,128 +639,154 @@ export const modelingMachine = createMachine(
 | 
			
		||||
    },
 | 
			
		||||
    // end guards
 | 
			
		||||
    actions: {
 | 
			
		||||
      'set sketchMetadata from pathToNode': assign(({ sketchPathToNode }) => {
 | 
			
		||||
        if (!sketchPathToNode) return {}
 | 
			
		||||
        return getSketchMetadataFromPathToNode(sketchPathToNode)
 | 
			
		||||
      'set sketchMetadata from pathToNode': assign(({ sketchDetails }) => {
 | 
			
		||||
        if (!sketchDetails?.sketchPathToNode || !sketchDetails) return {}
 | 
			
		||||
        return {
 | 
			
		||||
          sketchDetails: {
 | 
			
		||||
            ...sketchDetails,
 | 
			
		||||
            sketchPathToNode: sketchDetails.sketchPathToNode,
 | 
			
		||||
          },
 | 
			
		||||
        }
 | 
			
		||||
      }),
 | 
			
		||||
      'hide default planes': () => {
 | 
			
		||||
        sceneInfra.removeDefaultPlanes()
 | 
			
		||||
        kclManager.hidePlanes()
 | 
			
		||||
      },
 | 
			
		||||
      'reset sketch metadata': assign({
 | 
			
		||||
        sketchPathToNode: null,
 | 
			
		||||
        sketchDetails: null,
 | 
			
		||||
        sketchEnginePathId: '',
 | 
			
		||||
        sketchPlaneId: '',
 | 
			
		||||
      }),
 | 
			
		||||
      'set sketch metadata': assign(({ selectionRanges }) => {
 | 
			
		||||
        const sourceRange = selectionRanges.codeBasedSelections[0].range
 | 
			
		||||
        const sketchPathToNode = getNodePathFromSourceRange(
 | 
			
		||||
          kclManager.ast,
 | 
			
		||||
          sourceRange
 | 
			
		||||
        )
 | 
			
		||||
        return getSketchMetadataFromPathToNode(
 | 
			
		||||
          sketchPathToNode,
 | 
			
		||||
          selectionRanges
 | 
			
		||||
        )
 | 
			
		||||
      }),
 | 
			
		||||
      'set new sketch metadata': assign((_, { data }) => data),
 | 
			
		||||
      'set new sketch metadata': assign((_, { data }) => ({
 | 
			
		||||
        sketchDetails: data,
 | 
			
		||||
      })),
 | 
			
		||||
      // TODO implement source ranges for all of these constraints
 | 
			
		||||
      // need to make the async like the modal constraints
 | 
			
		||||
      'Make selection horizontal': ({ selectionRanges, sketchPathToNode }) => {
 | 
			
		||||
      'Make selection horizontal': ({ selectionRanges, sketchDetails }) => {
 | 
			
		||||
        const { modifiedAst } = applyConstraintHorzVert(
 | 
			
		||||
          selectionRanges,
 | 
			
		||||
          'horizontal',
 | 
			
		||||
          kclManager.ast,
 | 
			
		||||
          kclManager.programMemory
 | 
			
		||||
        )
 | 
			
		||||
        if (!sketchDetails) return
 | 
			
		||||
        sceneEntitiesManager.updateAstAndRejigSketch(
 | 
			
		||||
          sketchPathToNode || [],
 | 
			
		||||
          modifiedAst
 | 
			
		||||
          sketchDetails.sketchPathToNode,
 | 
			
		||||
          modifiedAst,
 | 
			
		||||
          sketchDetails.zAxis,
 | 
			
		||||
          sketchDetails.yAxis,
 | 
			
		||||
          sketchDetails.origin
 | 
			
		||||
        )
 | 
			
		||||
      },
 | 
			
		||||
      'Make selection vertical': ({ selectionRanges, sketchPathToNode }) => {
 | 
			
		||||
      'Make selection vertical': ({ selectionRanges, sketchDetails }) => {
 | 
			
		||||
        const { modifiedAst } = applyConstraintHorzVert(
 | 
			
		||||
          selectionRanges,
 | 
			
		||||
          'vertical',
 | 
			
		||||
          kclManager.ast,
 | 
			
		||||
          kclManager.programMemory
 | 
			
		||||
        )
 | 
			
		||||
        if (!sketchDetails) return
 | 
			
		||||
        sceneEntitiesManager.updateAstAndRejigSketch(
 | 
			
		||||
          sketchPathToNode || [],
 | 
			
		||||
          modifiedAst
 | 
			
		||||
          sketchDetails.sketchPathToNode || [],
 | 
			
		||||
          modifiedAst,
 | 
			
		||||
          sketchDetails.zAxis,
 | 
			
		||||
          sketchDetails.yAxis,
 | 
			
		||||
          sketchDetails.origin
 | 
			
		||||
        )
 | 
			
		||||
      },
 | 
			
		||||
      'Constrain horizontally align': ({
 | 
			
		||||
        selectionRanges,
 | 
			
		||||
        sketchPathToNode,
 | 
			
		||||
      }) => {
 | 
			
		||||
      'Constrain horizontally align': ({ selectionRanges, sketchDetails }) => {
 | 
			
		||||
        const { modifiedAst } = applyConstraintHorzVertAlign({
 | 
			
		||||
          selectionRanges,
 | 
			
		||||
          constraint: 'setVertDistance',
 | 
			
		||||
        })
 | 
			
		||||
        if (!sketchDetails) return
 | 
			
		||||
        sceneEntitiesManager.updateAstAndRejigSketch(
 | 
			
		||||
          sketchPathToNode || [],
 | 
			
		||||
          modifiedAst
 | 
			
		||||
          sketchDetails?.sketchPathToNode || [],
 | 
			
		||||
          modifiedAst,
 | 
			
		||||
          sketchDetails.zAxis,
 | 
			
		||||
          sketchDetails.yAxis,
 | 
			
		||||
          sketchDetails.origin
 | 
			
		||||
        )
 | 
			
		||||
      },
 | 
			
		||||
      'Constrain vertically align': ({ selectionRanges, sketchPathToNode }) => {
 | 
			
		||||
      'Constrain vertically align': ({ selectionRanges, sketchDetails }) => {
 | 
			
		||||
        const { modifiedAst } = applyConstraintHorzVertAlign({
 | 
			
		||||
          selectionRanges,
 | 
			
		||||
          constraint: 'setHorzDistance',
 | 
			
		||||
        })
 | 
			
		||||
        if (!sketchDetails) return
 | 
			
		||||
        sceneEntitiesManager.updateAstAndRejigSketch(
 | 
			
		||||
          sketchPathToNode || [],
 | 
			
		||||
          modifiedAst
 | 
			
		||||
          sketchDetails?.sketchPathToNode || [],
 | 
			
		||||
          modifiedAst,
 | 
			
		||||
          sketchDetails.zAxis,
 | 
			
		||||
          sketchDetails.yAxis,
 | 
			
		||||
          sketchDetails.origin
 | 
			
		||||
        )
 | 
			
		||||
      },
 | 
			
		||||
      'Constrain snap to X': ({ selectionRanges, sketchPathToNode }) => {
 | 
			
		||||
      'Constrain snap to X': ({ selectionRanges, sketchDetails }) => {
 | 
			
		||||
        const { modifiedAst } = applyConstraintAxisAlign({
 | 
			
		||||
          selectionRanges,
 | 
			
		||||
          constraint: 'snapToXAxis',
 | 
			
		||||
        })
 | 
			
		||||
        if (!sketchDetails) return
 | 
			
		||||
        sceneEntitiesManager.updateAstAndRejigSketch(
 | 
			
		||||
          sketchPathToNode || [],
 | 
			
		||||
          modifiedAst
 | 
			
		||||
          sketchDetails?.sketchPathToNode || [],
 | 
			
		||||
          modifiedAst,
 | 
			
		||||
          sketchDetails.zAxis,
 | 
			
		||||
          sketchDetails.yAxis,
 | 
			
		||||
          sketchDetails.origin
 | 
			
		||||
        )
 | 
			
		||||
      },
 | 
			
		||||
      'Constrain snap to Y': ({ selectionRanges, sketchPathToNode }) => {
 | 
			
		||||
      'Constrain snap to Y': ({ selectionRanges, sketchDetails }) => {
 | 
			
		||||
        const { modifiedAst } = applyConstraintAxisAlign({
 | 
			
		||||
          selectionRanges,
 | 
			
		||||
          constraint: 'snapToYAxis',
 | 
			
		||||
        })
 | 
			
		||||
        if (!sketchDetails) return
 | 
			
		||||
        sceneEntitiesManager.updateAstAndRejigSketch(
 | 
			
		||||
          sketchPathToNode || [],
 | 
			
		||||
          modifiedAst
 | 
			
		||||
          sketchDetails?.sketchPathToNode || [],
 | 
			
		||||
          modifiedAst,
 | 
			
		||||
          sketchDetails.zAxis,
 | 
			
		||||
          sketchDetails.yAxis,
 | 
			
		||||
          sketchDetails.origin
 | 
			
		||||
        )
 | 
			
		||||
      },
 | 
			
		||||
      'Constrain equal length': ({ selectionRanges, sketchPathToNode }) => {
 | 
			
		||||
      'Constrain equal length': ({ selectionRanges, sketchDetails }) => {
 | 
			
		||||
        const { modifiedAst } = applyConstraintEqualLength({
 | 
			
		||||
          selectionRanges,
 | 
			
		||||
        })
 | 
			
		||||
        if (!sketchDetails) return
 | 
			
		||||
        sceneEntitiesManager.updateAstAndRejigSketch(
 | 
			
		||||
          sketchPathToNode || [],
 | 
			
		||||
          modifiedAst
 | 
			
		||||
          sketchDetails?.sketchPathToNode || [],
 | 
			
		||||
          modifiedAst,
 | 
			
		||||
          sketchDetails.zAxis,
 | 
			
		||||
          sketchDetails.yAxis,
 | 
			
		||||
          sketchDetails.origin
 | 
			
		||||
        )
 | 
			
		||||
      },
 | 
			
		||||
      'Constrain parallel': ({ selectionRanges, sketchPathToNode }) => {
 | 
			
		||||
      'Constrain parallel': ({ selectionRanges, sketchDetails }) => {
 | 
			
		||||
        const { modifiedAst } = applyConstraintEqualAngle({
 | 
			
		||||
          selectionRanges,
 | 
			
		||||
        })
 | 
			
		||||
        if (!sketchDetails) return
 | 
			
		||||
        sceneEntitiesManager.updateAstAndRejigSketch(
 | 
			
		||||
          sketchPathToNode || [],
 | 
			
		||||
          modifiedAst
 | 
			
		||||
          sketchDetails?.sketchPathToNode || [],
 | 
			
		||||
          modifiedAst,
 | 
			
		||||
          sketchDetails.zAxis,
 | 
			
		||||
          sketchDetails.yAxis,
 | 
			
		||||
          sketchDetails.origin
 | 
			
		||||
        )
 | 
			
		||||
      },
 | 
			
		||||
      'Constrain remove constraints': ({
 | 
			
		||||
        selectionRanges,
 | 
			
		||||
        sketchPathToNode,
 | 
			
		||||
      }) => {
 | 
			
		||||
      'Constrain remove constraints': ({ selectionRanges, sketchDetails }) => {
 | 
			
		||||
        const { modifiedAst } = applyRemoveConstrainingValues({
 | 
			
		||||
          selectionRanges,
 | 
			
		||||
        })
 | 
			
		||||
        if (!sketchDetails) return
 | 
			
		||||
        sceneEntitiesManager.updateAstAndRejigSketch(
 | 
			
		||||
          sketchPathToNode || [],
 | 
			
		||||
          modifiedAst
 | 
			
		||||
          sketchDetails?.sketchPathToNode || [],
 | 
			
		||||
          modifiedAst,
 | 
			
		||||
          sketchDetails.zAxis,
 | 
			
		||||
          sketchDetails.yAxis,
 | 
			
		||||
          sketchDetails.origin
 | 
			
		||||
        )
 | 
			
		||||
      },
 | 
			
		||||
      'AST extrude': (_, event) => {
 | 
			
		||||
@ -754,7 +798,6 @@ export const modelingMachine = createMachine(
 | 
			
		||||
          distance.variableName &&
 | 
			
		||||
          distance.insertIndex !== undefined
 | 
			
		||||
        ) {
 | 
			
		||||
          console.log('adding variable!', distance)
 | 
			
		||||
          const newBody = [...ast.body]
 | 
			
		||||
          newBody.splice(
 | 
			
		||||
            distance.insertIndex,
 | 
			
		||||
@ -785,18 +828,25 @@ export const modelingMachine = createMachine(
 | 
			
		||||
          sceneInfra.modelingSend('Equip Line tool')
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      'setup client side sketch segments': ({ sketchPathToNode }, { type }) => {
 | 
			
		||||
      'setup client side sketch segments': ({ sketchDetails }) => {
 | 
			
		||||
        if (!sketchDetails) return
 | 
			
		||||
        if (Object.keys(sceneEntitiesManager.activeSegments).length > 0) {
 | 
			
		||||
          sceneEntitiesManager
 | 
			
		||||
            .tearDownSketch({ removeAxis: false })
 | 
			
		||||
            .then(() => {
 | 
			
		||||
              sceneEntitiesManager.setupSketch({
 | 
			
		||||
                sketchPathToNode: sketchPathToNode || [],
 | 
			
		||||
                sketchPathToNode: sketchDetails?.sketchPathToNode || [],
 | 
			
		||||
                forward: sketchDetails.zAxis,
 | 
			
		||||
                up: sketchDetails.yAxis,
 | 
			
		||||
                position: sketchDetails.origin,
 | 
			
		||||
              })
 | 
			
		||||
            })
 | 
			
		||||
        } else {
 | 
			
		||||
          sceneEntitiesManager.setupSketch({
 | 
			
		||||
            sketchPathToNode: sketchPathToNode || [],
 | 
			
		||||
            sketchPathToNode: sketchDetails?.sketchPathToNode || [],
 | 
			
		||||
            forward: sketchDetails.zAxis,
 | 
			
		||||
            up: sketchDetails.yAxis,
 | 
			
		||||
            position: sketchDetails.origin,
 | 
			
		||||
          })
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
@ -809,43 +859,60 @@ export const modelingMachine = createMachine(
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      'remove sketch grid': () => sceneEntitiesManager.removeSketchGrid(),
 | 
			
		||||
      'set up draft line': ({ sketchPathToNode }) => {
 | 
			
		||||
        sceneEntitiesManager.setUpDraftLine(sketchPathToNode || [])
 | 
			
		||||
      'set up draft line': ({ sketchDetails }) => {
 | 
			
		||||
        if (!sketchDetails) return
 | 
			
		||||
        sceneEntitiesManager.setUpDraftLine(
 | 
			
		||||
          sketchDetails.sketchPathToNode,
 | 
			
		||||
          sketchDetails.zAxis,
 | 
			
		||||
          sketchDetails.yAxis
 | 
			
		||||
        )
 | 
			
		||||
      },
 | 
			
		||||
      'set up draft arc': ({ sketchPathToNode }) => {
 | 
			
		||||
        sceneEntitiesManager.setUpDraftArc(sketchPathToNode || [])
 | 
			
		||||
      'set up draft arc': ({ sketchDetails }) => {
 | 
			
		||||
        if (!sketchDetails) return
 | 
			
		||||
        sceneEntitiesManager.setUpDraftArc(
 | 
			
		||||
          sketchDetails.sketchPathToNode,
 | 
			
		||||
          sketchDetails.zAxis,
 | 
			
		||||
          sketchDetails.yAxis
 | 
			
		||||
        )
 | 
			
		||||
      },
 | 
			
		||||
      'set up draft line without teardown': ({ sketchPathToNode }) =>
 | 
			
		||||
      'set up draft line without teardown': ({ sketchDetails }) =>
 | 
			
		||||
        sceneEntitiesManager.setupSketch({
 | 
			
		||||
          sketchPathToNode: sketchPathToNode || [],
 | 
			
		||||
          sketchPathToNode: sketchDetails?.sketchPathToNode || [],
 | 
			
		||||
          draftSegment: 'line',
 | 
			
		||||
          forward: sketchDetails?.zAxis || [0, 1, 0],
 | 
			
		||||
          up: sketchDetails?.yAxis || [0, 0, 1],
 | 
			
		||||
          position: sketchDetails?.origin,
 | 
			
		||||
        }),
 | 
			
		||||
      'show default planes': () => {
 | 
			
		||||
        sceneInfra.showDefaultPlanes()
 | 
			
		||||
        sceneEntitiesManager.setupDefaultPlaneHover()
 | 
			
		||||
        kclManager.showPlanes()
 | 
			
		||||
      },
 | 
			
		||||
      'setup noPoints onClick listener': ({ sketchPathToNode }) => {
 | 
			
		||||
      'setup noPoints onClick listener': ({ sketchDetails }) => {
 | 
			
		||||
        if (!sketchDetails) return
 | 
			
		||||
        sceneEntitiesManager.createIntersectionPlane()
 | 
			
		||||
        const sketchGroup = sketchGroupFromPathToNode({
 | 
			
		||||
          pathToNode: sketchPathToNode || [],
 | 
			
		||||
          ast: kclManager.ast,
 | 
			
		||||
          programMemory: kclManager.programMemory,
 | 
			
		||||
        })
 | 
			
		||||
        const quaternion = quaternionFromSketchGroup(sketchGroup)
 | 
			
		||||
        const quaternion = quaternionFromUpNForward(
 | 
			
		||||
          new Vector3(...sketchDetails.yAxis),
 | 
			
		||||
          new Vector3(...sketchDetails.zAxis)
 | 
			
		||||
        )
 | 
			
		||||
        sceneEntitiesManager.intersectionPlane &&
 | 
			
		||||
          sceneEntitiesManager.intersectionPlane.setRotationFromQuaternion(
 | 
			
		||||
            quaternion
 | 
			
		||||
          )
 | 
			
		||||
        sceneEntitiesManager.intersectionPlane &&
 | 
			
		||||
          sceneEntitiesManager.intersectionPlane.position.copy(
 | 
			
		||||
            new Vector3(...(sketchDetails?.origin || [0, 0, 0]))
 | 
			
		||||
          )
 | 
			
		||||
        sceneInfra.setCallbacks({
 | 
			
		||||
          onClick: async (args) => {
 | 
			
		||||
            if (!args) return
 | 
			
		||||
            if (args.mouseEvent.which !== 1) return
 | 
			
		||||
            const { intersectionPoint } = args
 | 
			
		||||
            if (!intersectionPoint?.twoD || !sketchPathToNode) return
 | 
			
		||||
            if (!intersectionPoint?.twoD || !sketchDetails?.sketchPathToNode)
 | 
			
		||||
              return
 | 
			
		||||
            const { modifiedAst } = addStartProfileAt(
 | 
			
		||||
              kclManager.ast,
 | 
			
		||||
              sketchPathToNode,
 | 
			
		||||
              sketchDetails.sketchPathToNode,
 | 
			
		||||
              [intersectionPoint.twoD.x, intersectionPoint.twoD.y]
 | 
			
		||||
            )
 | 
			
		||||
            await kclManager.updateAst(modifiedAst, false)
 | 
			
		||||
@ -854,8 +921,15 @@ export const modelingMachine = createMachine(
 | 
			
		||||
          },
 | 
			
		||||
        })
 | 
			
		||||
      },
 | 
			
		||||
      'add axis n grid': ({ sketchPathToNode }) =>
 | 
			
		||||
        sceneEntitiesManager.createSketchAxis(sketchPathToNode || []),
 | 
			
		||||
      'add axis n grid': ({ sketchDetails }) => {
 | 
			
		||||
        if (!sketchDetails) return
 | 
			
		||||
        sceneEntitiesManager.createSketchAxis(
 | 
			
		||||
          sketchDetails.sketchPathToNode || [],
 | 
			
		||||
          sketchDetails.zAxis,
 | 
			
		||||
          sketchDetails.yAxis,
 | 
			
		||||
          sketchDetails.origin
 | 
			
		||||
        )
 | 
			
		||||
      },
 | 
			
		||||
      'reset client scene mouse handlers': () => {
 | 
			
		||||
        // when not in sketch mode we don't need any mouse listeners
 | 
			
		||||
        // (note the orbit controls are always active though)
 | 
			
		||||
@ -871,44 +945,3 @@ export const modelingMachine = createMachine(
 | 
			
		||||
    // end actions
 | 
			
		||||
  }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
function getSketchMetadataFromPathToNode(
 | 
			
		||||
  pathToNode: PathToNode,
 | 
			
		||||
  selectionRanges?: Selections
 | 
			
		||||
) {
 | 
			
		||||
  const pipeExpression = getNodeFromPath<PipeExpression>(
 | 
			
		||||
    kclManager.ast,
 | 
			
		||||
    pathToNode,
 | 
			
		||||
    'PipeExpression'
 | 
			
		||||
  ).node
 | 
			
		||||
  if (pipeExpression.type !== 'PipeExpression') return {}
 | 
			
		||||
  const sketchCallExpression = pipeExpression.body.find(
 | 
			
		||||
    (e) => e.type === 'CallExpression' && e.callee.name === 'startSketchOn'
 | 
			
		||||
  ) as CallExpression
 | 
			
		||||
  if (!sketchCallExpression) return {}
 | 
			
		||||
 | 
			
		||||
  let sketchEnginePathId: string
 | 
			
		||||
  if (selectionRanges) {
 | 
			
		||||
    sketchEnginePathId =
 | 
			
		||||
      isCursorInSketchCommandRange(
 | 
			
		||||
        engineCommandManager.artifactMap,
 | 
			
		||||
        selectionRanges
 | 
			
		||||
      ) || ''
 | 
			
		||||
  } else {
 | 
			
		||||
    const _selectionRanges: Selections = {
 | 
			
		||||
      otherSelections: [],
 | 
			
		||||
      codeBasedSelections: [
 | 
			
		||||
        { range: [pipeExpression.start, pipeExpression.end], type: 'default' },
 | 
			
		||||
      ],
 | 
			
		||||
    }
 | 
			
		||||
    sketchEnginePathId =
 | 
			
		||||
      isCursorInSketchCommandRange(
 | 
			
		||||
        engineCommandManager.artifactMap,
 | 
			
		||||
        _selectionRanges
 | 
			
		||||
      ) || ''
 | 
			
		||||
  }
 | 
			
		||||
  return {
 | 
			
		||||
    sketchPathToNode: pathToNode,
 | 
			
		||||
    sketchEnginePathId,
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -93,6 +93,8 @@ export interface StoreState {
 | 
			
		||||
    path: string
 | 
			
		||||
  }[]
 | 
			
		||||
  setHomeMenuItems: (items: { name: string; path: string }[]) => void
 | 
			
		||||
  lastCodeMirrorSelectionUpdatedFromScene: number
 | 
			
		||||
  setLastCodeMirrorSelectionUpdatedFromScene: (time: number) => void
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const useStore = create<StoreState>()(
 | 
			
		||||
@ -156,6 +158,9 @@ export const useStore = create<StoreState>()(
 | 
			
		||||
        setHomeShowMenu: (showHomeMenu) => set({ showHomeMenu }),
 | 
			
		||||
        homeMenuItems: [],
 | 
			
		||||
        setHomeMenuItems: (homeMenuItems) => set({ homeMenuItems }),
 | 
			
		||||
        lastCodeMirrorSelectionUpdatedFromScene: Date.now(),
 | 
			
		||||
        setLastCodeMirrorSelectionUpdatedFromScene: (time) =>
 | 
			
		||||
          set({ lastCodeMirrorSelectionUpdatedFromScene: time }),
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
@ -242,6 +242,8 @@ pub struct Face {
 | 
			
		||||
    pub y_axis: Point3d,
 | 
			
		||||
    /// The z-axis (normal).
 | 
			
		||||
    pub z_axis: Point3d,
 | 
			
		||||
    /// the face id the sketch is on
 | 
			
		||||
    pub face_id: uuid::Uuid,
 | 
			
		||||
    #[serde(rename = "__meta")]
 | 
			
		||||
    pub meta: Vec<Metadata>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,7 @@
 | 
			
		||||
use anyhow::Result;
 | 
			
		||||
use derive_docs::stdlib;
 | 
			
		||||
use schemars::JsonSchema;
 | 
			
		||||
use uuid::Uuid;
 | 
			
		||||
 | 
			
		||||
use crate::{
 | 
			
		||||
    errors::{KclError, KclErrorDetails},
 | 
			
		||||
@ -113,8 +114,9 @@ async fn inner_extrude(length: f64, sketch_group: Box<SketchGroup>, args: Args)
 | 
			
		||||
 | 
			
		||||
    // Create a hashmap for quick id lookup
 | 
			
		||||
    let mut face_id_map = std::collections::HashMap::new();
 | 
			
		||||
    let mut start_cap_id = None;
 | 
			
		||||
    let mut end_cap_id = None;
 | 
			
		||||
    // creating fake ids for start and end caps is to make extrudes mock-execute safe
 | 
			
		||||
    let mut start_cap_id = Some(Uuid::new_v4());
 | 
			
		||||
    let mut end_cap_id = Some(Uuid::new_v4());
 | 
			
		||||
 | 
			
		||||
    for face_info in face_infos {
 | 
			
		||||
        match face_info.cap {
 | 
			
		||||
@ -160,6 +162,18 @@ async fn inner_extrude(length: f64, sketch_group: Box<SketchGroup>, args: Args)
 | 
			
		||||
                    new_value.push(extrude_surface);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            new_value.push(ExtrudeSurface::ExtrudePlane(crate::executor::ExtrudePlane {
 | 
			
		||||
                position: sketch_group.position, // TODO should be for the extrude surface
 | 
			
		||||
                rotation: sketch_group.rotation, // TODO should be for the extrude surface
 | 
			
		||||
                // pushing this values with a fake face_id to make extrudes mock-execute safe
 | 
			
		||||
                face_id: Uuid::new_v4(),
 | 
			
		||||
                name: path.get_base().name.clone(),
 | 
			
		||||
                geo_meta: GeoMeta {
 | 
			
		||||
                    id: path.get_base().geo_meta.id,
 | 
			
		||||
                    metadata: path.get_base().geo_meta.metadata.clone(),
 | 
			
		||||
                },
 | 
			
		||||
            }));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -955,6 +955,7 @@ async fn start_sketch_on_face(
 | 
			
		||||
        y_axis: extrude_group.y_axis,
 | 
			
		||||
        z_axis: extrude_group.z_axis,
 | 
			
		||||
        meta: vec![args.source_range.into()],
 | 
			
		||||
        face_id: extrude_plane_id,
 | 
			
		||||
    }))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user