Merge branch 'main' into pierremtb/issue1349
This commit is contained in:
		| @ -1,3 +1,3 @@ | ||||
| [codespell] | ||||
| ignore-words-list: crate,everytime,inout,co-ordinate | ||||
| ignore-words-list: crate,everytime,inout,co-ordinate,ot | ||||
| skip: **/target,node_modules,build,**/Cargo.lock,./src-tauri/gen/schemas | ||||
|  | ||||
							
								
								
									
										16
									
								
								docs/kcl/KNOWN-ISSUES.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								docs/kcl/KNOWN-ISSUES.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,16 @@ | ||||
| # Known Issues | ||||
|  | ||||
| The following are bugs that are not in modeling-app or kcl itself. These bugs | ||||
| once fixed in engine will just start working here with no language changes. | ||||
|  | ||||
| - **Sketch on Face**: If your sketch is outside the edges of the face (on which you | ||||
|     are sketching) you will get multiple models returned instead of one single | ||||
|     model for that sketch and its underlying 3D object. | ||||
|  | ||||
| - **Patterns**: If you try and pass a pattern to `hole` currently only the first | ||||
|     item in the pattern is being subtracted. This is an engine bug that is being | ||||
|     worked on. | ||||
|    | ||||
| - **Import**: Right now you can import a file, even if that file has brep data | ||||
|     you cannot edit it. You also cannot move or transform the imported objects at | ||||
|    all. In the future, after v1, the engine will account for this. | ||||
							
								
								
									
										1793
									
								
								docs/kcl/std.json
									
									
									
									
									
								
							
							
						
						
									
										1793
									
								
								docs/kcl/std.json
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										197
									
								
								docs/kcl/std.md
									
									
									
									
									
								
							
							
						
						
									
										197
									
								
								docs/kcl/std.md
									
									
									
									
									
								
							| @ -41,6 +41,7 @@ | ||||
| 	* [`log2`](#log2) | ||||
| 	* [`max`](#max) | ||||
| 	* [`min`](#min) | ||||
| 	* [`patternCircular`](#patternCircular) | ||||
| 	* [`patternLinear`](#patternLinear) | ||||
| 	* [`pi`](#pi) | ||||
| 	* [`pow`](#pow) | ||||
| @ -3998,6 +3999,194 @@ min(args: [number]) -> number | ||||
|  | ||||
|  | ||||
|  | ||||
| ### patternCircular | ||||
|  | ||||
| A Circular pattern. | ||||
|  | ||||
|  | ||||
|  | ||||
| ``` | ||||
| patternCircular(data: CircularPatternData, geometry: Geometry) -> Geometries | ||||
| ``` | ||||
|  | ||||
| #### Arguments | ||||
|  | ||||
| * `data`: `CircularPatternData` - Data for a circular pattern. | ||||
| ``` | ||||
| { | ||||
| 	// The arc angle (in degrees) to place the repetitions. Must be greater than 0. | ||||
| 	arcDegrees: number, | ||||
| 	// The axis around which to make the pattern. This is a 3D vector. | ||||
| 	axis: [number, number, number], | ||||
| 	// The center about which to make th pattern. This is a 3D vector. | ||||
| 	center: [number, number, number], | ||||
| 	// The number of repetitions. Must be greater than 0. This excludes the original entity. For example, if `repetitions` is 1, the original entity will be copied once. | ||||
| 	repetitions: number, | ||||
| 	// Whether or not to rotate the duplicates as they are copied. | ||||
| 	rotateDuplicates: string, | ||||
| } | ||||
| ``` | ||||
| * `geometry`: `Geometry` - A geometry. | ||||
| ``` | ||||
| { | ||||
| 	// The plane id or face id of the sketch group. | ||||
| 	entityId: uuid, | ||||
| 	// The id of the sketch group. | ||||
| 	id: uuid, | ||||
| 	// The position of the sketch group. | ||||
| 	position: [number, number, number], | ||||
| 	// The rotation of the sketch group base plane. | ||||
| 	rotation: [number, number, number, number], | ||||
| 	// The starting path. | ||||
| 	start: { | ||||
| 	// The from point. | ||||
| 	from: [number, number], | ||||
| 	// The name of the path. | ||||
| 	name: string, | ||||
| 	// The to point. | ||||
| 	to: [number, number], | ||||
| }, | ||||
| 	type: "SketchGroup", | ||||
| 	// The paths in the sketch group. | ||||
| 	value: [{ | ||||
| 	// The from point. | ||||
| 	from: [number, number], | ||||
| 	// The name of the path. | ||||
| 	name: string, | ||||
| 	// The to point. | ||||
| 	to: [number, number], | ||||
| 	type: "ToPoint", | ||||
| } | | ||||
| { | ||||
| 	// arc's direction | ||||
| 	ccw: string, | ||||
| 	// the arc's center | ||||
| 	center: [number, number], | ||||
| 	// The from point. | ||||
| 	from: [number, number], | ||||
| 	// The name of the path. | ||||
| 	name: string, | ||||
| 	// The to point. | ||||
| 	to: [number, number], | ||||
| 	type: "TangentialArcTo", | ||||
| } | | ||||
| { | ||||
| 	// The from point. | ||||
| 	from: [number, number], | ||||
| 	// The name of the path. | ||||
| 	name: string, | ||||
| 	// The to point. | ||||
| 	to: [number, number], | ||||
| 	type: "Horizontal", | ||||
| 	// The x coordinate. | ||||
| 	x: number, | ||||
| } | | ||||
| { | ||||
| 	// The from point. | ||||
| 	from: [number, number], | ||||
| 	// The name of the path. | ||||
| 	name: string, | ||||
| 	// The to point. | ||||
| 	to: [number, number], | ||||
| 	type: "AngledLineTo", | ||||
| 	// The x coordinate. | ||||
| 	x: number, | ||||
| 	// The y coordinate. | ||||
| 	y: number, | ||||
| } | | ||||
| { | ||||
| 	// The from point. | ||||
| 	from: [number, number], | ||||
| 	// The name of the path. | ||||
| 	name: string, | ||||
| 	// The to point. | ||||
| 	to: [number, number], | ||||
| 	type: "Base", | ||||
| }], | ||||
| 	// The x-axis of the sketch group base plane in the 3D space | ||||
| 	xAxis: { | ||||
| 	x: number, | ||||
| 	y: number, | ||||
| 	z: number, | ||||
| }, | ||||
| 	// The y-axis of the sketch group base plane in the 3D space | ||||
| 	yAxis: { | ||||
| 	x: number, | ||||
| 	y: number, | ||||
| 	z: number, | ||||
| }, | ||||
| 	// The z-axis of the sketch group base plane in the 3D space | ||||
| 	zAxis: { | ||||
| 	x: number, | ||||
| 	y: number, | ||||
| 	z: number, | ||||
| }, | ||||
| } | | ||||
| { | ||||
| 	// The id of the extrusion end cap | ||||
| 	endCapId: uuid, | ||||
| 	// The height of the extrude group. | ||||
| 	height: number, | ||||
| 	// The id of the extrude group. | ||||
| 	id: uuid, | ||||
| 	// The position of the extrude group. | ||||
| 	position: [number, number, number], | ||||
| 	// The rotation of the extrude group. | ||||
| 	rotation: [number, number, number, number], | ||||
| 	// The id of the extrusion start cap | ||||
| 	startCapId: uuid, | ||||
| 	type: "ExtrudeGroup", | ||||
| 	// The extrude surfaces. | ||||
| 	value: [{ | ||||
| 	// The face id for the extrude plane. | ||||
| 	faceId: uuid, | ||||
| 	// The id of the geometry. | ||||
| 	id: uuid, | ||||
| 	// The name. | ||||
| 	name: string, | ||||
| 	// The position. | ||||
| 	position: [number, number, number], | ||||
| 	// The rotation. | ||||
| 	rotation: [number, number, number, number], | ||||
| 	// The source range. | ||||
| 	sourceRange: [number, number], | ||||
| 	type: "extrudePlane", | ||||
| }], | ||||
| 	// The x-axis of the extrude group base plane in the 3D space | ||||
| 	xAxis: { | ||||
| 	x: number, | ||||
| 	y: number, | ||||
| 	z: number, | ||||
| }, | ||||
| 	// The y-axis of the extrude group base plane in the 3D space | ||||
| 	yAxis: { | ||||
| 	x: number, | ||||
| 	y: number, | ||||
| 	z: number, | ||||
| }, | ||||
| 	// The z-axis of the extrude group base plane in the 3D space | ||||
| 	zAxis: { | ||||
| 	x: number, | ||||
| 	y: number, | ||||
| 	z: number, | ||||
| }, | ||||
| } | ||||
| ``` | ||||
|  | ||||
| #### Returns | ||||
|  | ||||
| * `Geometries` - A set of geometry. | ||||
| ``` | ||||
| { | ||||
| 	type: "SketchGroups", | ||||
| } | | ||||
| { | ||||
| 	type: "ExtrudeGroups", | ||||
| } | ||||
| ``` | ||||
|  | ||||
|  | ||||
|  | ||||
| ### patternLinear | ||||
|  | ||||
| A linear pattern. | ||||
| @ -5149,7 +5338,7 @@ Start a sketch on a specific plane or face. | ||||
|  | ||||
|  | ||||
| ``` | ||||
| startSketchOn(data: SketchData, tag: String) -> SketchSurface | ||||
| startSketchOn(data: SketchData, tag: SketchOnFaceTag) -> SketchSurface | ||||
| ``` | ||||
|  | ||||
| #### Arguments | ||||
| @ -5239,7 +5428,11 @@ startSketchOn(data: SketchData, tag: String) -> SketchSurface | ||||
| }, | ||||
| } | ||||
| ``` | ||||
| * `tag`: `String` | ||||
| * `tag`: `SketchOnFaceTag` - A tag for sketch on face. | ||||
| ``` | ||||
| "start" | "end" | | ||||
| string | ||||
| ``` | ||||
|  | ||||
| #### Returns | ||||
|  | ||||
|  | ||||
							
								
								
									
										30
									
								
								src/App.tsx
									
									
									
									
									
								
							
							
						
						
									
										30
									
								
								src/App.tsx
									
									
									
									
									
								
							| @ -24,8 +24,6 @@ import { type IndexLoaderData } from 'lib/types' | ||||
| import { paths } from 'lib/paths' | ||||
| import { useGlobalStateContext } from 'hooks/useGlobalStateContext' | ||||
| import { onboardingPaths } from 'routes/Onboarding/paths' | ||||
| import { cameraMouseDragGuards } from 'lib/cameraControls' | ||||
| import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/models' | ||||
| import { CodeMenu } from 'components/CodeMenu' | ||||
| import { TextEditor } from 'components/TextEditor' | ||||
| import { Themes, getSystemTheme } from 'lib/theme' | ||||
| @ -56,8 +54,7 @@ export function App() { | ||||
|   })) | ||||
|  | ||||
|   const { settings } = useGlobalStateContext() | ||||
|   const { showDebugPanel, onboardingStatus, cameraControls, theme } = | ||||
|     settings?.context || {} | ||||
|   const { showDebugPanel, onboardingStatus, theme } = settings?.context || {} | ||||
|   const { state, send } = useModelingContext() | ||||
|  | ||||
|   const editorTheme = theme === Themes.System ? getSystemTheme() : theme | ||||
| @ -119,31 +116,6 @@ export function App() { | ||||
|         }, | ||||
|         cmd_id: newCmdId, | ||||
|       }) | ||||
|     } else { | ||||
|       const interactionGuards = cameraMouseDragGuards[cameraControls] | ||||
|       let interaction: CameraDragInteractionType_type | ||||
|  | ||||
|       const eWithButton = { ...e, button: buttonDownInStream } | ||||
|  | ||||
|       if (interactionGuards.pan.callback(eWithButton)) { | ||||
|         interaction = 'pan' | ||||
|       } else if (interactionGuards.rotate.callback(eWithButton)) { | ||||
|         interaction = 'rotate' | ||||
|       } else if (interactionGuards.zoom.dragCallback(eWithButton)) { | ||||
|         interaction = 'zoom' | ||||
|       } else { | ||||
|         return | ||||
|       } | ||||
|  | ||||
|       debounceSocketSend({ | ||||
|         type: 'modeling_cmd_req', | ||||
|         cmd: { | ||||
|           type: 'camera_drag_move', | ||||
|           interaction, | ||||
|           window: { x, y }, | ||||
|         }, | ||||
|         cmd_id: newCmdId, | ||||
|       }) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|  | ||||
							
								
								
									
										252
									
								
								src/clientSideScene/ClientSideSceneComp.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										252
									
								
								src/clientSideScene/ClientSideSceneComp.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,252 @@ | ||||
| import { useRef, useEffect, useState } from 'react' | ||||
| import { useModelingContext } from 'hooks/useModelingContext' | ||||
|  | ||||
| import { cameraMouseDragGuards } from 'lib/cameraControls' | ||||
| import { useGlobalStateContext } from 'hooks/useGlobalStateContext' | ||||
| import { useStore } from 'useStore' | ||||
| import { | ||||
|   DEBUG_SHOW_BOTH_SCENES, | ||||
|   ReactCameraProperties, | ||||
|   sceneInfra, | ||||
| } from './sceneInfra' | ||||
| import { throttle } from 'lib/utils' | ||||
|  | ||||
| function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } { | ||||
|   const [isCamMoving, setIsCamMoving] = useState(false) | ||||
|   const [isTween, setIsTween] = useState(false) | ||||
|  | ||||
|   const { state } = useModelingContext() | ||||
|  | ||||
|   useEffect(() => { | ||||
|     sceneInfra.setIsCamMovingCallback((isMoving, isTween) => { | ||||
|       setIsCamMoving(isMoving) | ||||
|       setIsTween(isTween) | ||||
|     }) | ||||
|   }, []) | ||||
|  | ||||
|   if (DEBUG_SHOW_BOTH_SCENES || !isCamMoving) | ||||
|     return { hideClient: false, hideServer: false } | ||||
|   let hideServer = state.matches('Sketch') || state.matches('Sketch no face') | ||||
|   if (isTween) { | ||||
|     hideServer = false | ||||
|   } | ||||
|  | ||||
|   return { hideClient: !hideServer, hideServer } | ||||
| } | ||||
|  | ||||
| export const ClientSideScene = ({ | ||||
|   cameraControls, | ||||
| }: { | ||||
|   cameraControls: ReturnType< | ||||
|     typeof useGlobalStateContext | ||||
|   >['settings']['context']['cameraControls'] | ||||
| }) => { | ||||
|   const canvasRef = useRef<HTMLDivElement>(null) | ||||
|   const { state, send } = useModelingContext() | ||||
|   const { hideClient, hideServer } = useShouldHideScene() | ||||
|   const { setHighlightRange } = useStore((s) => ({ | ||||
|     setHighlightRange: s.setHighlightRange, | ||||
|     highlightRange: s.highlightRange, | ||||
|   })) | ||||
|  | ||||
|   // Listen for changes to the camera controls setting | ||||
|   // and update the client-side scene's controls accordingly. | ||||
|   useEffect(() => { | ||||
|     sceneInfra.setInteractionGuards(cameraMouseDragGuards[cameraControls]) | ||||
|   }, [cameraControls]) | ||||
|   useEffect(() => { | ||||
|     sceneInfra.updateOtherSelectionColors( | ||||
|       state?.context?.selectionRanges?.otherSelections || [] | ||||
|     ) | ||||
|   }, [state?.context?.selectionRanges?.otherSelections]) | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if (!canvasRef.current) return | ||||
|     const canvas = canvasRef.current | ||||
|     canvas.appendChild(sceneInfra.renderer.domElement) | ||||
|     sceneInfra.animate() | ||||
|     sceneInfra.setHighlightCallback(setHighlightRange) | ||||
|     canvas.addEventListener('mousemove', sceneInfra.onMouseMove, false) | ||||
|     canvas.addEventListener('mousedown', sceneInfra.onMouseDown, false) | ||||
|     canvas.addEventListener('mouseup', sceneInfra.onMouseUp, false) | ||||
|     sceneInfra.setSend(send) | ||||
|     return () => { | ||||
|       canvas?.removeEventListener('mousemove', sceneInfra.onMouseMove) | ||||
|       canvas?.removeEventListener('mousedown', sceneInfra.onMouseDown) | ||||
|       canvas?.removeEventListener('mouseup', sceneInfra.onMouseUp) | ||||
|     } | ||||
|   }, []) | ||||
|  | ||||
|   return ( | ||||
|     <div | ||||
|       ref={canvasRef} | ||||
|       className={`absolute inset-0 h-full w-full transition-all duration-300 ${ | ||||
|         hideClient ? 'opacity-0' : 'opacity-100' | ||||
|       } ${hideServer ? 'bg-black' : ''} ${ | ||||
|         !hideClient && !hideServer && state.matches('Sketch') | ||||
|           ? 'bg-black/80' | ||||
|           : '' | ||||
|       }`} | ||||
|     ></div> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| const throttled = throttle((a: ReactCameraProperties) => { | ||||
|   if (a.type === 'perspective' && a.fov) { | ||||
|     sceneInfra.dollyZoom(a.fov) | ||||
|   } | ||||
| }, 1000 / 15) | ||||
|  | ||||
| export const CamDebugSettings = () => { | ||||
|   const [camSettings, setCamSettings] = useState<ReactCameraProperties>({ | ||||
|     type: 'perspective', | ||||
|     fov: 12, | ||||
|     position: [0, 0, 0], | ||||
|     quaternion: [0, 0, 0, 1], | ||||
|   }) | ||||
|   const [fov, setFov] = useState(12) | ||||
|  | ||||
|   useEffect(() => { | ||||
|     sceneInfra.setReactCameraPropertiesCallback(setCamSettings) | ||||
|   }, [sceneInfra]) | ||||
|   useEffect(() => { | ||||
|     if (camSettings.type === 'perspective' && camSettings.fov) { | ||||
|       setFov(camSettings.fov) | ||||
|     } | ||||
|   }, [(camSettings as any)?.fov]) | ||||
|  | ||||
|   return ( | ||||
|     <div> | ||||
|       <h3>cam settings</h3> | ||||
|       perspective cam | ||||
|       <input | ||||
|         type="checkbox" | ||||
|         checked={camSettings.type === 'perspective'} | ||||
|         onChange={(e) => { | ||||
|           if (camSettings.type === 'perspective') { | ||||
|             sceneInfra.useOrthographicCamera() | ||||
|           } else { | ||||
|             sceneInfra.usePerspectiveCamera() | ||||
|           } | ||||
|         }} | ||||
|       /> | ||||
|       {camSettings.type === 'perspective' && ( | ||||
|         <input | ||||
|           type="range" | ||||
|           min="4" | ||||
|           max="90" | ||||
|           step={0.5} | ||||
|           value={fov} | ||||
|           onChange={(e) => { | ||||
|             setFov(parseFloat(e.target.value)) | ||||
|  | ||||
|             throttled({ | ||||
|               ...camSettings, | ||||
|               fov: parseFloat(e.target.value), | ||||
|             }) | ||||
|           }} | ||||
|           className="w-full cursor-pointer pointer-events-auto" | ||||
|         /> | ||||
|       )} | ||||
|       {camSettings.type === 'perspective' && ( | ||||
|         <div> | ||||
|           <span>fov</span> | ||||
|           <input | ||||
|             type="number" | ||||
|             value={camSettings.fov} | ||||
|             className="text-black w-16" | ||||
|             onChange={(e) => { | ||||
|               sceneInfra.setCam({ | ||||
|                 ...camSettings, | ||||
|                 fov: parseFloat(e.target.value), | ||||
|               }) | ||||
|             }} | ||||
|           /> | ||||
|         </div> | ||||
|       )} | ||||
|       {camSettings.type === 'orthographic' && ( | ||||
|         <> | ||||
|           <div> | ||||
|             <span>fov</span> | ||||
|             <input | ||||
|               type="number" | ||||
|               value={camSettings.zoom} | ||||
|               className="text-black w-16" | ||||
|               onChange={(e) => { | ||||
|                 sceneInfra.setCam({ | ||||
|                   ...camSettings, | ||||
|                   zoom: parseFloat(e.target.value), | ||||
|                 }) | ||||
|               }} | ||||
|             /> | ||||
|           </div> | ||||
|         </> | ||||
|       )} | ||||
|       <div> | ||||
|         Position | ||||
|         <ul className="flex"> | ||||
|           <li> | ||||
|             <span className="pl-2 pr-1">x:</span> | ||||
|             <input | ||||
|               type="number" | ||||
|               step={5} | ||||
|               data-testid="cam-x-position" | ||||
|               value={camSettings.position[0]} | ||||
|               className="text-black w-16" | ||||
|               onChange={(e) => { | ||||
|                 sceneInfra.setCam({ | ||||
|                   ...camSettings, | ||||
|                   position: [ | ||||
|                     parseFloat(e.target.value), | ||||
|                     camSettings.position[1], | ||||
|                     camSettings.position[2], | ||||
|                   ], | ||||
|                 }) | ||||
|               }} | ||||
|             /> | ||||
|           </li> | ||||
|           <li> | ||||
|             <span className="pl-2 pr-1">y:</span> | ||||
|             <input | ||||
|               type="number" | ||||
|               step={5} | ||||
|               data-testid="cam-y-position" | ||||
|               value={camSettings.position[1]} | ||||
|               className="text-black w-16" | ||||
|               onChange={(e) => { | ||||
|                 sceneInfra.setCam({ | ||||
|                   ...camSettings, | ||||
|                   position: [ | ||||
|                     camSettings.position[0], | ||||
|                     parseFloat(e.target.value), | ||||
|                     camSettings.position[2], | ||||
|                   ], | ||||
|                 }) | ||||
|               }} | ||||
|             /> | ||||
|           </li> | ||||
|           <li> | ||||
|             <span className="pl-2 pr-1">z:</span> | ||||
|             <input | ||||
|               type="number" | ||||
|               step={5} | ||||
|               data-testid="cam-z-position" | ||||
|               value={camSettings.position[2]} | ||||
|               className="text-black w-16" | ||||
|               onChange={(e) => { | ||||
|                 sceneInfra.setCam({ | ||||
|                   ...camSettings, | ||||
|                   position: [ | ||||
|                     camSettings.position[0], | ||||
|                     camSettings.position[1], | ||||
|                     parseFloat(e.target.value), | ||||
|                   ], | ||||
|                 }) | ||||
|               }} | ||||
|             /> | ||||
|           </li> | ||||
|         </ul> | ||||
|       </div> | ||||
|     </div> | ||||
|   ) | ||||
| } | ||||
| @ -25,14 +25,14 @@ import { | ||||
|   INTERSECTION_PLANE_LAYER, | ||||
|   isQuaternionVertical, | ||||
|   RAYCASTABLE_PLANE, | ||||
|   setupSingleton, | ||||
|   sceneInfra, | ||||
|   SKETCH_GROUP_SEGMENTS, | ||||
|   SKETCH_LAYER, | ||||
|   X_AXIS, | ||||
|   XZ_PLANE, | ||||
|   Y_AXIS, | ||||
|   YZ_PLANE, | ||||
| } from './setup' | ||||
| } from './sceneInfra' | ||||
| import { | ||||
|   CallExpression, | ||||
|   getTangentialArcToInfo, | ||||
| @ -85,7 +85,10 @@ export const TANGENTIAL_ARC_TO_SEGMENT_BODY = 'tangential-arc-to-segment-body' | ||||
| export const TANGENTIAL_ARC_TO__SEGMENT_DASH = | ||||
|   'tangential-arc-to-segment-body-dashed' | ||||
| 
 | ||||
| class ClientSideScene { | ||||
| // This singleton Class is responsible for all of the things the user sees and interacts with.
 | ||||
| // That mostly mean sketch elements.
 | ||||
| // Cameras, controls, raycasters, etc are handled by sceneInfra
 | ||||
| class SceneEntities { | ||||
|   scene: Scene | ||||
|   sceneProgramMemory: ProgramMemory = { root: {}, return: null } | ||||
|   activeSegments: { [key: string]: Group } = {} | ||||
| @ -93,18 +96,18 @@ class ClientSideScene { | ||||
|   axisGroup: Group | null = null | ||||
|   currentSketchQuaternion: Quaternion | null = null | ||||
|   constructor() { | ||||
|     this.scene = setupSingleton?.scene | ||||
|     setupSingleton?.setOnCamChange(this.onCamChange) | ||||
|     this.scene = sceneInfra?.scene | ||||
|     sceneInfra?.setOnCamChange(this.onCamChange) | ||||
|   } | ||||
| 
 | ||||
|   onCamChange = () => { | ||||
|     const orthoFactor = orthoScale(setupSingleton.camera) | ||||
|     const orthoFactor = orthoScale(sceneInfra.camera) | ||||
| 
 | ||||
|     Object.values(this.activeSegments).forEach((segment) => { | ||||
|       const factor = | ||||
|         setupSingleton.camera instanceof OrthographicCamera | ||||
|         sceneInfra.camera instanceof OrthographicCamera | ||||
|           ? orthoFactor | ||||
|           : perspScale(setupSingleton.camera, segment) | ||||
|           : perspScale(sceneInfra.camera, segment) | ||||
|       if ( | ||||
|         segment.userData.from && | ||||
|         segment.userData.to && | ||||
| @ -135,9 +138,9 @@ class ClientSideScene { | ||||
|     }) | ||||
|     if (this.axisGroup) { | ||||
|       const factor = | ||||
|         setupSingleton.camera instanceof OrthographicCamera | ||||
|         sceneInfra.camera instanceof OrthographicCamera | ||||
|           ? orthoFactor | ||||
|           : perspScale(setupSingleton.camera, this.axisGroup) | ||||
|           : perspScale(sceneInfra.camera, this.axisGroup) | ||||
|       const x = this.axisGroup.getObjectByName(X_AXIS) | ||||
|       x?.scale.set(1, factor, 1) | ||||
|       const y = this.axisGroup.getObjectByName(Y_AXIS) | ||||
| @ -241,12 +244,13 @@ class ClientSideScene { | ||||
|       engineCommandManager, | ||||
|       programMemoryOverride, | ||||
|     }) | ||||
|     this.sceneProgramMemory = programMemory | ||||
|     const sketchGroup = sketchGroupFromPathToNode({ | ||||
|       pathToNode: sketchPathToNode, | ||||
|       ast: kclManager.ast, | ||||
|       programMemory, | ||||
|     }) | ||||
|     if (!Array.isArray(sketchGroup?.value)) return | ||||
|     this.sceneProgramMemory = programMemory | ||||
|     const group = new Group() | ||||
|     group.userData = { | ||||
|       type: SKETCH_GROUP_SEGMENTS, | ||||
| @ -258,11 +262,11 @@ class ClientSideScene { | ||||
|       sketchGroup.position[1], | ||||
|       sketchGroup.position[2] | ||||
|     ) | ||||
|     const orthoFactor = orthoScale(setupSingleton.camera) | ||||
|     const orthoFactor = orthoScale(sceneInfra.camera) | ||||
|     const factor = | ||||
|       setupSingleton.camera instanceof OrthographicCamera | ||||
|       sceneInfra.camera instanceof OrthographicCamera | ||||
|         ? orthoFactor | ||||
|         : perspScale(setupSingleton.camera, dummy) | ||||
|         : perspScale(sceneInfra.camera, dummy) | ||||
|     sketchGroup.value.forEach((segment, index) => { | ||||
|       let segPathToNode = getNodePathFromSourceRange( | ||||
|         draftSegment ? truncatedAst : kclManager.ast, | ||||
| @ -309,7 +313,7 @@ class ClientSideScene { | ||||
| 
 | ||||
|     this.scene.add(group) | ||||
|     if (!draftSegment) { | ||||
|       setupSingleton.setCallbacks({ | ||||
|       sceneInfra.setCallbacks({ | ||||
|         onDrag: (args) => { | ||||
|           this.onDragSegment({ | ||||
|             ...args, | ||||
| @ -319,7 +323,7 @@ class ClientSideScene { | ||||
|         onMove: () => {}, | ||||
|         onClick: (args) => { | ||||
|           if (!args || !args.object) { | ||||
|             setupSingleton.modelingSend({ | ||||
|             sceneInfra.modelingSend({ | ||||
|               type: 'Set selection', | ||||
|               data: { | ||||
|                 selectionType: 'singleCodeCursor', | ||||
| @ -330,7 +334,7 @@ class ClientSideScene { | ||||
|           const { object } = args | ||||
|           const event = getEventForSegmentSelection(object) | ||||
|           if (!event) return | ||||
|           setupSingleton.modelingSend(event) | ||||
|           sceneInfra.modelingSend(event) | ||||
|         }, | ||||
|         onMouseEnter: ({ object }) => { | ||||
|           // TODO change the color of the segment to yellow?
 | ||||
| @ -350,15 +354,15 @@ class ClientSideScene { | ||||
|               parent.userData.pathToNode, | ||||
|               'CallExpression' | ||||
|             ).node | ||||
|             setupSingleton.highlightCallback([node.start, node.end]) | ||||
|             sceneInfra.highlightCallback([node.start, node.end]) | ||||
|             const yellow = 0xffff00 | ||||
|             colorSegment(object, yellow) | ||||
|             return | ||||
|           } | ||||
|           setupSingleton.highlightCallback([0, 0]) | ||||
|           sceneInfra.highlightCallback([0, 0]) | ||||
|         }, | ||||
|         onMouseLeave: ({ object }) => { | ||||
|           setupSingleton.highlightCallback([0, 0]) | ||||
|           sceneInfra.highlightCallback([0, 0]) | ||||
|           const parent = getParentGroup(object) | ||||
|           const isSelected = parent?.userData?.isSelected | ||||
|           colorSegment(object, isSelected ? 0x0000ff : 0xffffff) | ||||
| @ -371,7 +375,7 @@ class ClientSideScene { | ||||
|         }, | ||||
|       }) | ||||
|     } else { | ||||
|       setupSingleton.setCallbacks({ | ||||
|       sceneInfra.setCallbacks({ | ||||
|         onDrag: () => {}, | ||||
|         onClick: async (args) => { | ||||
|           if (!args) return | ||||
| @ -426,7 +430,7 @@ class ClientSideScene { | ||||
|         }, | ||||
|       }) | ||||
|     } | ||||
|     setupSingleton.controls.enableRotate = false | ||||
|     sceneInfra.controls.enableRotate = false | ||||
|   } | ||||
|   updateAstAndRejigSketch = async ( | ||||
|     sketchPathToNode: PathToNode, | ||||
| @ -529,7 +533,7 @@ class ClientSideScene { | ||||
|       this.sceneProgramMemory = programMemory | ||||
|       const sketchGroup = programMemory.root[variableDeclarationName] | ||||
|         .value as Path[] | ||||
|       const orthoFactor = orthoScale(setupSingleton.camera) | ||||
|       const orthoFactor = orthoScale(sceneInfra.camera) | ||||
|       sketchGroup.forEach((segment, index) => { | ||||
|         const segPathToNode = getNodePathFromSourceRange( | ||||
|           modifiedAst, | ||||
| @ -545,9 +549,9 @@ class ClientSideScene { | ||||
|         // const prevSegment = sketchGroup.slice(index - 1)[0]
 | ||||
|         const type = group?.userData?.type | ||||
|         const factor = | ||||
|           setupSingleton.camera instanceof OrthographicCamera | ||||
|           sceneInfra.camera instanceof OrthographicCamera | ||||
|             ? orthoFactor | ||||
|             : perspScale(setupSingleton.camera, group) | ||||
|             : perspScale(sceneInfra.camera, group) | ||||
|         if (type === TANGENTIAL_ARC_TO_SEGMENT) { | ||||
|           this.updateTangentialArcToSegment({ | ||||
|             prevSegment: sketchGroup[index - 1], | ||||
| @ -704,9 +708,9 @@ class ClientSideScene { | ||||
|   } | ||||
|   async animateAfterSketch() { | ||||
|     if (isReducedMotion()) { | ||||
|       setupSingleton.usePerspectiveCamera() | ||||
|       sceneInfra.usePerspectiveCamera() | ||||
|     } else { | ||||
|       await setupSingleton.animateToPerspective() | ||||
|       await sceneInfra.animateToPerspective() | ||||
|     } | ||||
|   } | ||||
|   removeSketchGrid() { | ||||
| @ -739,7 +743,7 @@ class ClientSideScene { | ||||
|         reject() | ||||
|       } | ||||
|     } | ||||
|     setupSingleton.controls.enableRotate = true | ||||
|     sceneInfra.controls.enableRotate = true | ||||
|     this.activeSegments = {} | ||||
|     // maybe should reset onMove etc handlers
 | ||||
|     if (shouldResolve) resolve(true) | ||||
| @ -758,7 +762,7 @@ class ClientSideScene { | ||||
|     }) | ||||
|   } | ||||
|   setupDefaultPlaneHover() { | ||||
|     setupSingleton.setCallbacks({ | ||||
|     sceneInfra.setCallbacks({ | ||||
|       onMouseEnter: ({ object }) => { | ||||
|         if (object.parent.userData.type !== DEFAULT_PLANES) return | ||||
|         const type: DefaultPlane = object.userData.type | ||||
| @ -783,7 +787,7 @@ class ClientSideScene { | ||||
|           planeString = posNorm ? 'XZ' : '-XZ' | ||||
|           normal = posNorm ? [0, 1, 0] : [0, -1, 0] | ||||
|         } | ||||
|         setupSingleton.modelingSend({ | ||||
|         sceneInfra.modelingSend({ | ||||
|           type: 'Select default plane', | ||||
|           data: { | ||||
|             plane: planeString, | ||||
| @ -797,7 +801,7 @@ class ClientSideScene { | ||||
| 
 | ||||
| export type DefaultPlaneStr = 'XY' | 'XZ' | 'YZ' | '-XY' | '-XZ' | '-YZ' | ||||
| 
 | ||||
| export const clientSideScene = new ClientSideScene() | ||||
| export const sceneEntitiesManager = new SceneEntities() | ||||
| 
 | ||||
| // calculations/pure-functions/easy to test so no excuse not to
 | ||||
| 
 | ||||
| @ -1,5 +1,5 @@ | ||||
| import { Quaternion } from 'three' | ||||
| import { isQuaternionVertical } from './setup' | ||||
| import { isQuaternionVertical } from './sceneInfra' | ||||
| 
 | ||||
| describe('isQuaternionVertical', () => { | ||||
|   it('should identify vertical quaternions', () => { | ||||
| @ -24,7 +24,6 @@ import { | ||||
|   Object3DEventMap, | ||||
| } from 'three' | ||||
| import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' | ||||
| import { useRef, useEffect, useState } from 'react' | ||||
| import { engineCommandManager } from 'lang/std/engineConnection' | ||||
| import { v4 as uuidv4 } from 'uuid' | ||||
| import { isReducedMotion, roundOff, throttle } from 'lib/utils' | ||||
| @ -33,9 +32,7 @@ import { useModelingContext } from 'hooks/useModelingContext' | ||||
| import { deg2Rad } from 'lib/utils2d' | ||||
| import * as TWEEN from '@tweenjs/tween.js' | ||||
| import { MouseGuard, cameraMouseDragGuards } from 'lib/cameraControls' | ||||
| import { useGlobalStateContext } from 'hooks/useGlobalStateContext' | ||||
| import { SourceRange } from 'lang/wasm' | ||||
| import { useStore } from 'useStore' | ||||
| import { Axis } from 'lib/selections' | ||||
| import { createGridHelper } from './helpers' | ||||
| 
 | ||||
| @ -71,12 +68,14 @@ interface ThreeCamValues { | ||||
|   isPerspective: boolean | ||||
| } | ||||
| 
 | ||||
| const lastCmdDelay = 50 | ||||
| 
 | ||||
| let lastCmd: any = null | ||||
| let lastCmdTime: number = Date.now() | ||||
| let lastCmdTimeoutId: number | null = null | ||||
| 
 | ||||
| const sendLastReliableChannel = () => { | ||||
|   if (lastCmd && Date.now() - lastCmdTime >= 300) { | ||||
|   if (lastCmd && Date.now() - lastCmdTime >= lastCmdDelay) { | ||||
|     engineCommandManager.sendSceneCommand(lastCmd, true) | ||||
|     lastCmdTime = Date.now() | ||||
|   } | ||||
| @ -98,16 +97,57 @@ const throttledUpdateEngineCamera = throttle((threeValues: ThreeCamValues) => { | ||||
|   if (lastCmdTimeoutId !== null) { | ||||
|     clearTimeout(lastCmdTimeoutId) | ||||
|   } | ||||
|   lastCmdTimeoutId = setTimeout(sendLastReliableChannel, 300) as any as number | ||||
|   lastCmdTimeoutId = setTimeout( | ||||
|     sendLastReliableChannel, | ||||
|     lastCmdDelay | ||||
|   ) as any as number | ||||
| }, 1000 / 30) | ||||
| 
 | ||||
| let lastPerspectiveCmd: any = null | ||||
| let lastPerspectiveCmdTime: number = Date.now() | ||||
| let lastPerspectiveCmdTimeoutId: number | null = null | ||||
| 
 | ||||
| const sendLastPerspectiveReliableChannel = () => { | ||||
|   if ( | ||||
|     lastPerspectiveCmd && | ||||
|     Date.now() - lastPerspectiveCmdTime >= lastCmdDelay | ||||
|   ) { | ||||
|     engineCommandManager.sendSceneCommand(lastPerspectiveCmd, true) | ||||
|     lastPerspectiveCmdTime = Date.now() | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| const throttledUpdateEngineFov = throttle( | ||||
|   (vals: { | ||||
|     position: Vector3 | ||||
|     quaternion: Quaternion | ||||
|     zoom: number | ||||
|     fov: number | ||||
|   }) => updateEngineFov(vals), | ||||
|   }) => { | ||||
|     const cmd = { | ||||
|       type: 'modeling_cmd_req', | ||||
|       cmd_id: uuidv4(), | ||||
|       cmd: { | ||||
|         type: 'default_camera_perspective_settings', | ||||
|         ...convertThreeCamValuesToEngineCam({ | ||||
|           ...vals, | ||||
|           isPerspective: true, | ||||
|         }), | ||||
|         fov_y: vals.fov, | ||||
|         ...calculateNearFarFromFOV(vals.fov), | ||||
|       }, | ||||
|     } as any | ||||
|     engineCommandManager.sendSceneCommand(cmd) | ||||
|     lastPerspectiveCmd = cmd | ||||
|     lastPerspectiveCmdTime = Date.now() | ||||
|     if (lastPerspectiveCmdTimeoutId !== null) { | ||||
|       clearTimeout(lastPerspectiveCmdTimeoutId) | ||||
|     } | ||||
|     lastPerspectiveCmdTimeoutId = setTimeout( | ||||
|       sendLastPerspectiveReliableChannel, | ||||
|       lastCmdDelay | ||||
|     ) as any as number | ||||
|   }, | ||||
|   1000 / 15 | ||||
| ) | ||||
| 
 | ||||
| @ -138,7 +178,7 @@ interface onMoveCallbackArgs { | ||||
|   intersection: Intersection<Object3D<Object3DEventMap>> | ||||
| } | ||||
| 
 | ||||
| type ReactCameraProperties = | ||||
| export type ReactCameraProperties = | ||||
|   | { | ||||
|       type: 'perspective' | ||||
|       fov?: number | ||||
| @ -152,8 +192,11 @@ type ReactCameraProperties = | ||||
|       quaternion: [number, number, number, number] | ||||
|     } | ||||
| 
 | ||||
| class SetupSingleton { | ||||
|   static instance: SetupSingleton | ||||
| // This singleton class is responsible for all of the under the hood setup for the client side scene.
 | ||||
| // That is the cameras and switching between them, raycasters for click mouse events and their abstractions (onClick etc), setting up controls.
 | ||||
| // Anything that added the the scene for the user to interact with is probably in SceneEntities.ts
 | ||||
| class SceneInfra { | ||||
|   static instance: SceneInfra | ||||
|   scene: Scene | ||||
|   camera: PerspectiveCamera | OrthographicCamera | ||||
|   renderer: WebGLRenderer | ||||
| @ -290,7 +333,7 @@ class SetupSingleton { | ||||
|     const light = new AmbientLight(0x505050) // soft white light
 | ||||
|     this.scene.add(light) | ||||
| 
 | ||||
|     SetupSingleton.instance = this | ||||
|     SceneInfra.instance = this | ||||
|   } | ||||
|   private _isCamMovingCallback: (isMoving: boolean, isTween: boolean) => void = | ||||
|     () => {} | ||||
| @ -481,7 +524,7 @@ class SetupSingleton { | ||||
| 
 | ||||
|       const targetFov = 4 | ||||
|       const fovAnimationStep = (currentFov - targetFov) / FRAMES_TO_ANIMATE_IN | ||||
|       let frameWaitOnFinish = 5 | ||||
|       let frameWaitOnFinish = 10 | ||||
| 
 | ||||
|       const animateFovChange = () => { | ||||
|         if (this.camera instanceof PerspectiveCamera) { | ||||
| @ -670,7 +713,7 @@ class SetupSingleton { | ||||
|   } | null => { | ||||
|     this.planeRaycaster.setFromCamera( | ||||
|       this.currentMouseVector, | ||||
|       setupSingleton.camera | ||||
|       sceneInfra.camera | ||||
|     ) | ||||
|     const planeIntersects = this.planeRaycaster.intersectObjects( | ||||
|       this.scene.children, | ||||
| @ -933,7 +976,7 @@ class SetupSingleton { | ||||
|     if (planesGroup) this.scene.remove(planesGroup) | ||||
|   } | ||||
|   updateOtherSelectionColors = (otherSelections: Axis[]) => { | ||||
|     const axisGroup = setupSingleton.scene.children.find( | ||||
|     const axisGroup = sceneInfra.scene.children.find( | ||||
|       ({ userData }) => userData?.type === AXIS_GROUP | ||||
|     ) | ||||
|     const axisMap: { [key: string]: Axis } = { | ||||
| @ -955,247 +998,7 @@ class SetupSingleton { | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export const setupSingleton = new SetupSingleton() | ||||
| 
 | ||||
| function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } { | ||||
|   const [isCamMoving, setIsCamMoving] = useState(false) | ||||
|   const [isTween, setIsTween] = useState(false) | ||||
| 
 | ||||
|   const { state } = useModelingContext() | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     setupSingleton.setIsCamMovingCallback((isMoving, isTween) => { | ||||
|       setIsCamMoving(isMoving) | ||||
|       setIsTween(isTween) | ||||
|     }) | ||||
|   }, []) | ||||
| 
 | ||||
|   if (DEBUG_SHOW_BOTH_SCENES || !isCamMoving) | ||||
|     return { hideClient: false, hideServer: false } | ||||
|   let hideServer = state.matches('Sketch') || state.matches('Sketch no face') | ||||
|   if (isTween) { | ||||
|     hideServer = false | ||||
|   } | ||||
| 
 | ||||
|   return { hideClient: !hideServer, hideServer } | ||||
| } | ||||
| 
 | ||||
| export const ClientSideScene = ({ | ||||
|   cameraControls, | ||||
| }: { | ||||
|   cameraControls: ReturnType< | ||||
|     typeof useGlobalStateContext | ||||
|   >['settings']['context']['cameraControls'] | ||||
| }) => { | ||||
|   const canvasRef = useRef<HTMLDivElement>(null) | ||||
|   const { state, send } = useModelingContext() | ||||
|   const { hideClient, hideServer } = useShouldHideScene() | ||||
|   const { setHighlightRange } = useStore((s) => ({ | ||||
|     setHighlightRange: s.setHighlightRange, | ||||
|     highlightRange: s.highlightRange, | ||||
|   })) | ||||
| 
 | ||||
|   // Listen for changes to the camera controls setting
 | ||||
|   // and update the client-side scene's controls accordingly.
 | ||||
|   useEffect(() => { | ||||
|     setupSingleton.setInteractionGuards(cameraMouseDragGuards[cameraControls]) | ||||
|   }, [cameraControls]) | ||||
|   useEffect(() => { | ||||
|     setupSingleton.updateOtherSelectionColors( | ||||
|       state?.context?.selectionRanges?.otherSelections || [] | ||||
|     ) | ||||
|   }, [state?.context?.selectionRanges?.otherSelections]) | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     if (!canvasRef.current) return | ||||
|     const canvas = canvasRef.current | ||||
|     canvas.appendChild(setupSingleton.renderer.domElement) | ||||
|     setupSingleton.animate() | ||||
|     setupSingleton.setHighlightCallback(setHighlightRange) | ||||
|     canvas.addEventListener('mousemove', setupSingleton.onMouseMove, false) | ||||
|     canvas.addEventListener('mousedown', setupSingleton.onMouseDown, false) | ||||
|     canvas.addEventListener('mouseup', setupSingleton.onMouseUp, false) | ||||
|     setupSingleton.setSend(send) | ||||
|     return () => { | ||||
|       canvas?.removeEventListener('mousemove', setupSingleton.onMouseMove) | ||||
|       canvas?.removeEventListener('mousedown', setupSingleton.onMouseDown) | ||||
|       canvas?.removeEventListener('mouseup', setupSingleton.onMouseUp) | ||||
|     } | ||||
|   }, []) | ||||
| 
 | ||||
|   return ( | ||||
|     <div | ||||
|       ref={canvasRef} | ||||
|       className={`absolute inset-0 h-full w-full transition-all duration-300 ${ | ||||
|         hideClient ? 'opacity-0' : 'opacity-100' | ||||
|       } ${hideServer ? 'bg-black' : ''} ${ | ||||
|         !hideClient && !hideServer && state.matches('Sketch') | ||||
|           ? 'bg-black/80' | ||||
|           : '' | ||||
|       }`}
 | ||||
|     ></div> | ||||
|   ) | ||||
| } | ||||
| 
 | ||||
| const throttled = throttle((a: ReactCameraProperties) => { | ||||
|   if (a.type === 'perspective' && a.fov) { | ||||
|     setupSingleton.dollyZoom(a.fov) | ||||
|   } | ||||
| }, 1000 / 15) | ||||
| 
 | ||||
| export const CamDebugSettings = () => { | ||||
|   const [camSettings, setCamSettings] = useState<ReactCameraProperties>({ | ||||
|     type: 'perspective', | ||||
|     fov: 12, | ||||
|     position: [0, 0, 0], | ||||
|     quaternion: [0, 0, 0, 1], | ||||
|   }) | ||||
|   const [fov, setFov] = useState(12) | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     setupSingleton.setReactCameraPropertiesCallback(setCamSettings) | ||||
|   }, [setupSingleton]) | ||||
|   useEffect(() => { | ||||
|     if (camSettings.type === 'perspective' && camSettings.fov) { | ||||
|       setFov(camSettings.fov) | ||||
|     } | ||||
|   }, [(camSettings as any)?.fov]) | ||||
| 
 | ||||
|   return ( | ||||
|     <div> | ||||
|       <h3>cam settings</h3> | ||||
|       perspective cam | ||||
|       <input | ||||
|         type="checkbox" | ||||
|         checked={camSettings.type === 'perspective'} | ||||
|         onChange={(e) => { | ||||
|           if (camSettings.type === 'perspective') { | ||||
|             setupSingleton.useOrthographicCamera() | ||||
|           } else { | ||||
|             setupSingleton.usePerspectiveCamera() | ||||
|           } | ||||
|         }} | ||||
|       /> | ||||
|       {camSettings.type === 'perspective' && ( | ||||
|         <input | ||||
|           type="range" | ||||
|           min="4" | ||||
|           max="90" | ||||
|           step={0.5} | ||||
|           value={fov} | ||||
|           onChange={(e) => { | ||||
|             setFov(parseFloat(e.target.value)) | ||||
| 
 | ||||
|             throttled({ | ||||
|               ...camSettings, | ||||
|               fov: parseFloat(e.target.value), | ||||
|             }) | ||||
|           }} | ||||
|           className="w-full cursor-pointer pointer-events-auto" | ||||
|         /> | ||||
|       )} | ||||
|       {camSettings.type === 'perspective' && ( | ||||
|         <div> | ||||
|           <span>fov</span> | ||||
|           <input | ||||
|             type="number" | ||||
|             value={camSettings.fov} | ||||
|             className="text-black w-16" | ||||
|             onChange={(e) => { | ||||
|               setupSingleton.setCam({ | ||||
|                 ...camSettings, | ||||
|                 fov: parseFloat(e.target.value), | ||||
|               }) | ||||
|             }} | ||||
|           /> | ||||
|         </div> | ||||
|       )} | ||||
|       {camSettings.type === 'orthographic' && ( | ||||
|         <> | ||||
|           <div> | ||||
|             <span>fov</span> | ||||
|             <input | ||||
|               type="number" | ||||
|               value={camSettings.zoom} | ||||
|               className="text-black w-16" | ||||
|               onChange={(e) => { | ||||
|                 setupSingleton.setCam({ | ||||
|                   ...camSettings, | ||||
|                   zoom: parseFloat(e.target.value), | ||||
|                 }) | ||||
|               }} | ||||
|             /> | ||||
|           </div> | ||||
|         </> | ||||
|       )} | ||||
|       <div> | ||||
|         Position | ||||
|         <ul className="flex"> | ||||
|           <li> | ||||
|             <span className="pl-2 pr-1">x:</span> | ||||
|             <input | ||||
|               type="number" | ||||
|               step={5} | ||||
|               data-testid="cam-x-position" | ||||
|               value={camSettings.position[0]} | ||||
|               className="text-black w-16" | ||||
|               onChange={(e) => { | ||||
|                 setupSingleton.setCam({ | ||||
|                   ...camSettings, | ||||
|                   position: [ | ||||
|                     parseFloat(e.target.value), | ||||
|                     camSettings.position[1], | ||||
|                     camSettings.position[2], | ||||
|                   ], | ||||
|                 }) | ||||
|               }} | ||||
|             /> | ||||
|           </li> | ||||
|           <li> | ||||
|             <span className="pl-2 pr-1">y:</span> | ||||
|             <input | ||||
|               type="number" | ||||
|               step={5} | ||||
|               data-testid="cam-y-position" | ||||
|               value={camSettings.position[1]} | ||||
|               className="text-black w-16" | ||||
|               onChange={(e) => { | ||||
|                 setupSingleton.setCam({ | ||||
|                   ...camSettings, | ||||
|                   position: [ | ||||
|                     camSettings.position[0], | ||||
|                     parseFloat(e.target.value), | ||||
|                     camSettings.position[2], | ||||
|                   ], | ||||
|                 }) | ||||
|               }} | ||||
|             /> | ||||
|           </li> | ||||
|           <li> | ||||
|             <span className="pl-2 pr-1">z:</span> | ||||
|             <input | ||||
|               type="number" | ||||
|               step={5} | ||||
|               data-testid="cam-z-position" | ||||
|               value={camSettings.position[2]} | ||||
|               className="text-black w-16" | ||||
|               onChange={(e) => { | ||||
|                 setupSingleton.setCam({ | ||||
|                   ...camSettings, | ||||
|                   position: [ | ||||
|                     camSettings.position[0], | ||||
|                     camSettings.position[1], | ||||
|                     parseFloat(e.target.value), | ||||
|                   ], | ||||
|                 }) | ||||
|               }} | ||||
|             /> | ||||
|           </li> | ||||
|         </ul> | ||||
|       </div> | ||||
|     </div> | ||||
|   ) | ||||
| } | ||||
| export const sceneInfra = new SceneInfra() | ||||
| 
 | ||||
| function convertThreeCamValuesToEngineCam({ | ||||
|   position, | ||||
| @ -1242,29 +1045,6 @@ function calculateNearFarFromFOV(fov: number) { | ||||
|   return { z_near: 0.1, z_far } | ||||
| } | ||||
| 
 | ||||
| function updateEngineFov(args: { | ||||
|   position: Vector3 | ||||
|   quaternion: Quaternion | ||||
|   zoom: number | ||||
|   fov: number | ||||
| }) { | ||||
|   engineCommandManager.sendSceneCommand( | ||||
|     { | ||||
|       type: 'modeling_cmd_req', | ||||
|       cmd_id: uuidv4(), | ||||
|       cmd: { | ||||
|         type: 'default_camera_perspective_settings', | ||||
|         ...convertThreeCamValuesToEngineCam({ | ||||
|           ...args, | ||||
|           isPerspective: true, | ||||
|         }), | ||||
|         fov_y: args.fov, | ||||
|         ...calculateNearFarFromFOV(args.fov), | ||||
|       }, | ||||
|     } as any /* TODO - this command isn't in the spec yet, remove any when it is */ | ||||
|   ) | ||||
| } | ||||
| 
 | ||||
| export function isQuaternionVertical(q: Quaternion) { | ||||
|   const v = new Vector3(0, 0, 1).applyQuaternion(q) | ||||
|   // no x or y components means it's vertical
 | ||||
| @ -25,9 +25,9 @@ import { | ||||
|   TANGENTIAL_ARC_TO_SEGMENT, | ||||
|   TANGENTIAL_ARC_TO_SEGMENT_BODY, | ||||
|   TANGENTIAL_ARC_TO__SEGMENT_DASH, | ||||
| } from './clientSideScene' | ||||
| } from './sceneEntities' | ||||
| import { getTangentPointFromPreviousArc } from 'lib/utils2d' | ||||
| import { ARROWHEAD } from './setup' | ||||
| import { ARROWHEAD } from './sceneInfra' | ||||
|  | ||||
| export function straightSegment({ | ||||
|   from, | ||||
|  | ||||
| @ -1,10 +1,10 @@ | ||||
| import { useState, useEffect } from 'react' | ||||
| import { setupSingleton } from '../clientSideScene/setup' | ||||
| import { sceneInfra } from '../clientSideScene/sceneInfra' | ||||
| import { engineCommandManager } from 'lang/std/engineConnection' | ||||
| import { throttle, isReducedMotion } from 'lib/utils' | ||||
|  | ||||
| const updateDollyZoom = throttle( | ||||
|   (newFov: number) => setupSingleton.dollyZoom(newFov), | ||||
|   (newFov: number) => sceneInfra.dollyZoom(newFov), | ||||
|   1000 / 15 | ||||
| ) | ||||
|  | ||||
| @ -15,19 +15,19 @@ export const CamToggle = () => { | ||||
|  | ||||
|   useEffect(() => { | ||||
|     engineCommandManager.waitForReady.then(async () => { | ||||
|       setupSingleton.dollyZoom(fov) | ||||
|       sceneInfra.dollyZoom(fov) | ||||
|     }) | ||||
|   }, []) | ||||
|  | ||||
|   const toggleCamera = () => { | ||||
|     if (isPerspective) { | ||||
|       isReducedMotion() | ||||
|         ? setupSingleton.useOrthographicCamera() | ||||
|         : setupSingleton.animateToOrthographic() | ||||
|         ? sceneInfra.useOrthographicCamera() | ||||
|         : sceneInfra.animateToOrthographic() | ||||
|     } else { | ||||
|       isReducedMotion() | ||||
|         ? setupSingleton.usePerspectiveCamera() | ||||
|         : setupSingleton.animateToPerspective() | ||||
|         ? sceneInfra.usePerspectiveCamera() | ||||
|         : sceneInfra.animateToPerspective() | ||||
|     } | ||||
|     setIsPerspective(!isPerspective) | ||||
|   } | ||||
| @ -60,9 +60,9 @@ export const CamToggle = () => { | ||||
|       <button | ||||
|         onClick={() => { | ||||
|           if (enableRotate) { | ||||
|             setupSingleton.controls.enableRotate = false | ||||
|             sceneInfra.controls.enableRotate = false | ||||
|           } else { | ||||
|             setupSingleton.controls.enableRotate = true | ||||
|             sceneInfra.controls.enableRotate = true | ||||
|           } | ||||
|           setEnableRotate(!enableRotate) | ||||
|         }} | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel' | ||||
| import { AstExplorer } from './AstExplorer' | ||||
| import { EngineCommands } from './EngineCommands' | ||||
| import { CamDebugSettings } from 'clientSideScene/setup' | ||||
| import { CamDebugSettings } from 'clientSideScene/ClientSideSceneComp' | ||||
|  | ||||
| export const DebugPanel = ({ className, ...props }: CollapsiblePanelProps) => { | ||||
|   return ( | ||||
|  | ||||
| @ -33,9 +33,10 @@ import { applyConstraintIntersect } from './Toolbar/Intersect' | ||||
| import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance' | ||||
| import useStateMachineCommands from 'hooks/useStateMachineCommands' | ||||
| import { modelingMachineConfig } from 'lib/commandBarConfigs/modelingCommandConfig' | ||||
| import { setupSingleton } from 'clientSideScene/setup' | ||||
| import { getSketchQuaternion } from 'clientSideScene/clientSideScene' | ||||
| import { sceneInfra } from 'clientSideScene/sceneInfra' | ||||
| import { getSketchQuaternion } from 'clientSideScene/sceneEntities' | ||||
| import { startSketchOnDefault } from 'lang/modifyAst' | ||||
| import { Program } from 'lang/wasm' | ||||
|  | ||||
| type MachineContext<T extends AnyStateMachine> = { | ||||
|   state: StateFrom<T> | ||||
| @ -189,6 +190,17 @@ export const ModelingMachineProvider = ({ | ||||
|         }, | ||||
|       }, | ||||
|       services: { | ||||
|         'AST-undo-startSketchOn': async ({ sketchPathToNode }) => { | ||||
|           if (!sketchPathToNode) return | ||||
|           const newAst: Program = JSON.parse(JSON.stringify(kclManager.ast)) | ||||
|           const varDecIndex = sketchPathToNode[1][0] | ||||
|           // remove body item at varDecIndex | ||||
|           newAst.body = newAst.body.filter((_, i) => i !== varDecIndex) | ||||
|           await kclManager.executeAstMock(newAst, { updates: 'code' }) | ||||
|           sceneInfra.setCallbacks({ | ||||
|             onClick: () => {}, | ||||
|           }) | ||||
|         }, | ||||
|         'animate-to-face': async (_, { data: { plane, normal } }) => { | ||||
|           const { modifiedAst, pathToNode } = startSketchOnDefault( | ||||
|             kclManager.ast, | ||||
| @ -196,7 +208,7 @@ export const ModelingMachineProvider = ({ | ||||
|           ) | ||||
|           await kclManager.updateAst(modifiedAst, false) | ||||
|           const quaternion = getSketchQuaternion(pathToNode, normal) | ||||
|           await setupSingleton.tweenCameraToQuaternion(quaternion) | ||||
|           await sceneInfra.tweenCameraToQuaternion(quaternion) | ||||
|           return { | ||||
|             sketchPathToNode: pathToNode, | ||||
|             sketchNormalBackUp: normal, | ||||
| @ -210,7 +222,7 @@ export const ModelingMachineProvider = ({ | ||||
|             sketchPathToNode || [], | ||||
|             sketchNormalBackUp | ||||
|           ) | ||||
|           await setupSingleton.tweenCameraToQuaternion(quaternion) | ||||
|           await sceneInfra.tweenCameraToQuaternion(quaternion) | ||||
|         }, | ||||
|         'Get horizontal info': async ({ | ||||
|           selectionRanges, | ||||
|  | ||||
| @ -15,7 +15,7 @@ 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/setup' | ||||
| import { ClientSideScene } from 'clientSideScene/ClientSideSceneComp' | ||||
|  | ||||
| export const Stream = ({ className = '' }: { className?: string }) => { | ||||
|   const [isLoading, setIsLoading] = useState(true) | ||||
|  | ||||
| @ -26,7 +26,7 @@ import interact from '@replit/codemirror-interact' | ||||
| import { engineCommandManager } from '../lang/std/engineConnection' | ||||
| import { kclManager, useKclContext } from 'lang/KclSingleton' | ||||
| import { ModelingMachineEvent } from 'machines/modelingMachine' | ||||
| import { setupSingleton } from 'clientSideScene/setup' | ||||
| import { sceneInfra } from 'clientSideScene/sceneInfra' | ||||
|  | ||||
| export const editorShortcutMeta = { | ||||
|   formatCode: { | ||||
| @ -119,7 +119,7 @@ export const TextEditor = ({ | ||||
|     if (!editorView) { | ||||
|       setEditorView(viewUpdate.view) | ||||
|     } | ||||
|     if (setupSingleton.selected) return // mid drag | ||||
|     if (sceneInfra.selected) return // mid drag | ||||
|     const ignoreEvents: ModelingMachineEvent['type'][] = [ | ||||
|       'Equip Line tool', | ||||
|       'Equip tangential arc to', | ||||
|  | ||||
| @ -99,7 +99,7 @@ class KclManager { | ||||
|           }) | ||||
|       }) | ||||
|     } else { | ||||
|       localStorage?.setItem(PERSIST_CODE_TOKEN, code) | ||||
|       safteLSSetItem(PERSIST_CODE_TOKEN, code) | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @ -154,16 +154,16 @@ class KclManager { | ||||
|       this.code = '' | ||||
|       return | ||||
|     } | ||||
|     const storedCode = localStorage.getItem(PERSIST_CODE_TOKEN) | ||||
|     const storedCode = safeLSGetItem(PERSIST_CODE_TOKEN) || '' | ||||
|     // TODO #819 remove zustand persistence logic in a few months | ||||
|     // short term migration, shouldn't make a difference for tauri app users | ||||
|     // anyway since that's filesystem based. | ||||
|     const zustandStore = JSON.parse(localStorage.getItem('store') || '{}') | ||||
|     const zustandStore = JSON.parse(safeLSGetItem('store') || '{}') | ||||
|     if (storedCode === null && zustandStore?.state?.code) { | ||||
|       this.code = zustandStore.state.code | ||||
|       localStorage.setItem(PERSIST_CODE_TOKEN, this._code) | ||||
|       safteLSSetItem(PERSIST_CODE_TOKEN, this._code) | ||||
|       zustandStore.state.code = '' | ||||
|       localStorage.setItem('store', JSON.stringify(zustandStore)) | ||||
|       safteLSSetItem('store', JSON.stringify(zustandStore)) | ||||
|     } else if (storedCode === null) { | ||||
|       this.code = bracket | ||||
|     } else { | ||||
| @ -457,3 +457,13 @@ export function KclContextProvider({ | ||||
|     </KclContext.Provider> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| function safeLSGetItem(key: string) { | ||||
|   if (typeof window === 'undefined') return null | ||||
|   return localStorage?.getItem(key) | ||||
| } | ||||
|  | ||||
| function safteLSSetItem(key: string, value: string) { | ||||
|   if (typeof window === 'undefined') return | ||||
|   localStorage?.setItem(key, value) | ||||
| } | ||||
|  | ||||
| @ -30,7 +30,7 @@ import { | ||||
|   createFirstArg, | ||||
| } from './std/sketch' | ||||
| import { isLiteralArrayOrStatic } from './std/sketchcombos' | ||||
| import { DefaultPlaneStr } from 'clientSideScene/clientSideScene' | ||||
| import { DefaultPlaneStr } from 'clientSideScene/sceneEntities' | ||||
| import { roundOff } from 'lib/utils' | ||||
|  | ||||
| export function startSketchOnDefault( | ||||
|  | ||||
| @ -5,7 +5,7 @@ import { exportSave } from 'lib/exportSave' | ||||
| import { v4 as uuidv4 } from 'uuid' | ||||
| import * as Sentry from '@sentry/react' | ||||
| import { getNodePathFromSourceRange } from 'lang/queryAst' | ||||
| import { setupSingleton } from 'clientSideScene/setup' | ||||
| import { sceneInfra } from 'clientSideScene/sceneInfra' | ||||
|  | ||||
| let lastMessage = '' | ||||
|  | ||||
| @ -1012,7 +1012,7 @@ export class EngineCommandManager { | ||||
|             gizmo_mode: true, | ||||
|           }, | ||||
|         }) | ||||
|         setupSingleton.onStreamStart() | ||||
|         sceneInfra.onStreamStart() | ||||
|  | ||||
|         executeCode(undefined, true) | ||||
|       }, | ||||
|  | ||||
| @ -14,11 +14,11 @@ import { CommandArgument } from './commandTypes' | ||||
| import { | ||||
|   STRAIGHT_SEGMENT, | ||||
|   TANGENTIAL_ARC_TO_SEGMENT, | ||||
|   clientSideScene, | ||||
|   sceneEntitiesManager, | ||||
|   getParentGroup, | ||||
| } from 'clientSideScene/clientSideScene' | ||||
| } from 'clientSideScene/sceneEntities' | ||||
| import { Mesh } from 'three' | ||||
| import { AXIS_GROUP, X_AXIS } from 'clientSideScene/setup' | ||||
| 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' | ||||
| @ -401,7 +401,7 @@ function updateSceneObjectColors(codeBasedSelections: Selection[]) { | ||||
|     console.error('error parsing code in processCodeMirrorRanges', e) | ||||
|     return | ||||
|   } | ||||
|   Object.values(clientSideScene.activeSegments).forEach((segmentGroup) => { | ||||
|   Object.values(sceneEntitiesManager.activeSegments).forEach((segmentGroup) => { | ||||
|     if ( | ||||
|       ![STRAIGHT_SEGMENT, TANGENTIAL_ARC_TO_SEGMENT].includes( | ||||
|         segmentGroup?.userData?.type | ||||
|  | ||||
| @ -4,7 +4,6 @@ import { | ||||
|   EngineCommand, | ||||
| } from '../lang/std/engineConnection' | ||||
| import { Models } from '@kittycad/lib' | ||||
| import { v4 as uuidv4 } from 'uuid' | ||||
|  | ||||
| type WebSocketResponse = Models['OkWebSocketResponseData_type'] | ||||
|  | ||||
|  | ||||
| @ -1,6 +1,5 @@ | ||||
| import { PathToNode, VariableDeclarator } from 'lang/wasm' | ||||
| import { engineCommandManager } from 'lang/std/engineConnection' | ||||
| import { isReducedMotion } from 'lib/utils' | ||||
| import { | ||||
|   Axis, | ||||
|   Selection, | ||||
| @ -8,7 +7,6 @@ import { | ||||
|   Selections, | ||||
| } from 'lib/selections' | ||||
| import { assign, createMachine } from 'xstate' | ||||
| import { v4 as uuidv4 } from 'uuid' | ||||
| import { isCursorInSketchCommandRange } from 'lang/util' | ||||
| import { getNodePathFromSourceRange } from 'lang/queryAst' | ||||
| import { kclManager } from 'lang/KclSingleton' | ||||
| @ -46,11 +44,11 @@ import { Models } from '@kittycad/lib/dist/types/src' | ||||
| import { ModelingCommandSchema } from 'lib/commandBarConfigs/modelingCommandConfig' | ||||
| import { | ||||
|   DefaultPlaneStr, | ||||
|   clientSideScene, | ||||
|   sceneEntitiesManager, | ||||
|   quaternionFromSketchGroup, | ||||
|   sketchGroupFromPathToNode, | ||||
| } from 'clientSideScene/clientSideScene' | ||||
| import { setupSingleton } from 'clientSideScene/setup' | ||||
| } from 'clientSideScene/sceneEntities' | ||||
| import { sceneInfra } from 'clientSideScene/sceneInfra' | ||||
|  | ||||
| export const MODELING_PERSIST_KEY = 'MODELING_PERSIST_KEY' | ||||
|  | ||||
| @ -116,7 +114,7 @@ export type MoveDesc = { line: number; snippet: string } | ||||
|  | ||||
| export const modelingMachine = createMachine( | ||||
|   { | ||||
|     /** @xstate-layout N4IgpgJg5mDOIC5QFkD2EwBsCWA7KAxAMICGuAxlgNoAMAuoqAA6qzYAu2qujIAHogDMARhoA6GgE4A7COEBWGoIBMswQBoQAT0TDBANn1j9NAByTlw08tODTAFlMBfJ5rQYc+AgGUw7AASwWGDknNy0DEggLGxhPFECCMIK8mLSyvL2gvKmwvr29vqSmjoIBvKSYgoFwsr2MpLyLm7oWHhQYtgQmGA+foHBoVy4EbwxHMO8ifryqTNFyvqmNLX2NPYlulZGCjSGohlF+oLNIO5t+J3dvQCiuOxgAE6BANZ+5AAWo1HjcVOI0mE0mMkhkwns0ms6wymwQ0nWYlM8MkVmkFnB1lO508HS6PQIdwez1gb3YnyowkizFYE24-zK1jE8kEKP00mk9jqBmEsMk+Sq6zWCkM5mUJ1cZ1aOKu+JufHYjwArhhvtTYpMEohbKYxJYOTlpIpcspYclTDtFJDdrMaDksVL2mJvKTPsQyJRMM73l96GMaX9NQhlHVKszbVlhBZ5ApTUa0go6gd9BkmhLsY6vWSPk6XR8AJLXPoBII9IbhX0-f0a0CJOpIiQVQXpNGFU3ZSomQSCPaiAoGfT2jwZ3M570F-HIEhvAZQAC2YHu-gAbk9OOQSJhVdEq3TA2LMsZbZJOWj9Bj9KaOeJzPlZhlAct7IOLh1M59R1nx71J9OgnOFwEHyoI82AAF7cOwG5br81b8IgYp2BIIjwjQwaOOYprHJU4KWCI2SZPIwbPtKb7ZqRX7ENwsAKiQeD+EBIHgfcG7+BA2DUe6YDQTu8Q1vByFiIUmTrMe8JrPIsKqGKxjRpI3ZWNkwjEcO3ofp8FFEFRNF0SujxrixbEcRQXEVmqtK8XBQZdsoYiCIax5ssyyh7JJJjCFUzlnvYih5Mm0jKZcpFqfmhaabg1GPLRuD+AAggAQt4-gABrcequ58VZjiIiYZ40Osgj2OCsIqIyswqFIhpKPCA5pg6gUjuRoVaZFdHxYlACaqXmfSYpZYYhHyNIfnsqYxWFUYKiRrkXYiIVAWvg1uYac1UX+GQUA9F1AYZXUeyInsFgGJISh5dIkm2PYiI2jQHK5JG-m1UO9WqY1+JhRFq09Pg7A+lS25pRZtZrOIkJItCBRZEskkObZMwOENCwFfNwXBct4XadFTBPFjuBseQiqYCQzyGZBxlbbBtbKCiYgWLthp6JI1gaNoQg4bZeQqBy6EFA9LRPQtL1LU16MtdFDFgRBG6YFoa04FAIymf93V7ioRg2Aoj6DYRo0s0kDiCIitPst5kYmE+j0vijr29O9GPLqu2Drpg0uy9g8vk+llm9TZpizDIhQ2NGZ265GzJVOawYODddhZMjQXW5RIurbAuAkEw-jsKgyUe4D-FDcY5rHWsx0GDdklHYiwZSMc3aLBYceLWOwsfXRKdpxnWedYrMGe7WdlGPUx61A4tMXrrdSGlUJuzPk2wKA3gtN29K10WAACOiosV9UA-TnPXZEYcmQrdQJ2Zkkm2jqqFslTkLVzVfOW-HQvL0ndFMETUvUN3PH78dgneXSGyW0zJijj0WJdZMjQUSAmjLaB+kp+ZWxfjbFe0VHhgFnKgFc-hyBoPYLAPeKtahIVmNGFkyYdalEsDkYwehsgrEGo4cUj8SKN0-IWAASmAAAtGAPgIRFQPCITtcaTIbQZAKksdkppnKVDkoYGQLJahdgXlmVGhYbgb2wOnAAMngMAHdUCbh-gDHqfJ5E2GOFkJEjQgSmnDEhRo0ZHyGCKGo98CctGKh0RndaAFsAsSJuQDuIjLIqBEPtJEiwhqgkGqaTklRyhKBtJCLIqZWEqXUTFAA7rRQCwEJbMUwKxdipNKD+DwAAM1QAQCA3AwCdFwEuVAbwxAwHYDw8WTFIKYB4dU1AYTEhdjZNlRo6xfZyLAdQjkwIChRhsAozIHjsy5PyfRQpPSDJlM4pU3ANSCBPEeMBMQTBCbsBqY8Wc7S-BdM2ZLPpAyhlCAMDqZklVbwcmSBsceyJjB2ROjdWBzgLZsNUmsjg9s9KO22UZCpAy6kNKaS0tpHSeG6X0o8-ZgzTHKwyl2Se8gTDHUyGiWaPzqFnnchCPIuRfaGiKObTJz1sl5MhRimFJSSa7IRUck5ZySAXOAtctFHKnb9Oxc8so-ddQ2F9pyWxyZJJEsqENZyk1ahLCJSssQEKAhtWSnsg59TcCNLwCixpaKSAACNYA8L4BKmpUquzLHDooJRMx4Qxl1iVVVrzCgKHhHUDJiCn4jj1bFBKhreWPGOY8U55zLkituTau1Dqnm4u2uEuw14VXH2vkUCSPquQ0xtNYawKIA46ojQa9qRrakmrNc01plqU22p4VoR1OK-o91ztKtEaRQZwMKvUMepQuyNAkI0HN3Z9R6GrWy-VUa60xrjQmwVSabmdNTR2rtzqc2CUtBEtJNcxrUzcVIe6g13Ggqye+CN60ej1sRaa5FLat08MfbwjNPbf6BhdSDOoYoqrOWbOdBwTI67dlMMsRYIb0wsvvYuta+An2rv5Ym4VH6v17szRTF5jJajpDFAhFYBVzoFDSFIUEoIOQWCZaGsFrL1nbx+s+xtb7UW3NYx8XDv6zH-rsjqX2mRRTmhUAUYqKwbIzH9vCc0og5ILpYwuHeHxn18vjQKoVVyP08b436AT+K7Khl6k5ZYFQKXwRkOIDI5hIzORWLIZTkKsaPBxnjAmRNSlwsMQijj5r31orcx5x2XnHg8O5cZAzlYjPZsWLZXIhUlGOGhMVBMNM2Q3giaPFzAQQsLk84TYmOzjIadjRhjdWHgvY0K2F4rkXSuUBi2ZLNwzVaJfBPdAqj4TQ+t7JlyEfIcvBgQQhgW6j9GmqMZgIsAxSxxGdVJREik9AKX9rCZMl00TBjsEovKyzb2IezFNwxmdjEEm0enUmMB7iBJKcE0JeHe5CFsMCa9oITCoSc0W0oehbRIVQkCLILIZA6tOzNsQeZcAcAIPuvIaR3mGlvhUc+usaO6gjqoc09QHDKHBwYyH0PYcUn43i7NhFBJWGjmeYSzIHEyCZKIctZhi4ZAJ9N87mAxC4GFRuObJYQiLee32-CNk7AQOA5GIFppkjAmPLYIlyYbT1yOxN98EOudiAAHJZwAAqoDwAQggMUIAQECJBPS-gWBG+dekHUJVDTeSFOaXkjOlDJkWMsCJ0YdXE-YHDkXPUbryLMGiJEp8RCxjPBIfIICT1nnx2rlG-u4eUkM+T2syRUh4Ro1zFRmFIydfZDkI0x0xt1XV9mAAKv4u7QTHghMzgLwYwuydtaEEUA2tRwRF1wpaaPlRCo2HqLIcwswdW1-wAEhvTfaneN8Zr1AxjnUzBz9JvKXucptgBydPUIPaMrP8Lz-wVSSCUD6At1iYAz8E3y4TU1UrK1IQszRwoSwrMIGnUyQogoVBEu1mRjIGwFnEFXaA7mtwf16AC2bTaWANAIeB4Uzh4TP0oClTyHBEPDsENGjG9mZlKGcRpjDy+y6wDSAJhwQPAMzkgLIF6F8GLFb2GHQK7FSGZCJUsBuipkaDHS1EnlkGciPU4McHIJALAPwAgP4TKXAJJG9BfSbQtTEHgMFV4WQJkKzHQKgVIUVyUHSTklhA5BsmPEOi8hyBqglF5wwHgCiHGwzw7yDGTCnVkGzyUFUC7EknSDSFpiOlHTDHg0rxlDAFsPwwQBHSulwM1T8kKE-1qCkA8nSG8g9yKEkBWSCJeyDCUHcmOlsQjzUGSGKhMF1FHlQlrj5D8KQWfiXkCNi0z3ggDgLjRDMHMCEkMFjCL1kDIxTGyBg2SOTyCgjW6QeR83KT82xVSL7SpkiRx2jmsEcAsHcKLyHnaJZH7AcDyyhUxSGJ5VGOqLsKpm8iZFsGZ3yAcCI2VXZGME8mSGlwVyT2ZSr11WQwNSSnrTGPMSJSZD2DQghE5FG2KgyEqFtEmiYXWDITWNrReJ2OCKplmFLQTCKA5j1GKhZB9ksEZn4JUEWDWK-QhNayhOgQbDwl9lEBmB5HHlsAHmckDmsm+15kYzvVWWQx4xxKVl2O-2sAvTkgxFsGhn5DsBOKvDsDYLWIK1xnq28yi3hW2NxLSKpmcl1FNhSSBDMF+1ZliJEEoRg2WEjBYTpOOzECX2MVeP-V8kEj5FS1ykZhkFhHJKZAMFUDyFKOvQ5zO2X2539yNPxUsEulHXNMU3MGDj+xWFVWGkhgKhqGdMh15yuQ3A9PCSJXEGOnBgcz7B4KSFQgBIsGTG7DskWHWAjK11138ANyNysOlNF3aPjFnjXyrj0FlwByyEKBHkGhukOzuJTxh3YFjKz2kj2CJJREUHIVjDMHjEIkKmvhTEnzr04Fnw7i7M7yylDicJZB2xchDhM2yndQrXqGriPxP1QKqLLPpGOB2H9mWC5h5ioUQEaEMNBD0EWAWDkREMoPEOoIFVNTnKSGDDmTyh-IOiAz6wILDmODDD8lUCPCfLEKgAkL4CkPELUM+A-NqEUCoykFmHqGB3wIBARAqkKHZFvMWHFBcCAA */ | ||||
|     /** @xstate-layout N4IgpgJg5mDOIC5QFkD2EwBsCWA7KAxAMICGuAxlgNoAMAuoqAA6qzYAu2qujIAHogC0AdgCsAZgB04gEyjhADnEA2GgoUAWJQBoQAT0QBGGuICckmoZkbTM42YWKAvk91oMOfAQDKYdgAJYLDByTm5aBiQQFjYwniiBBFtpGhlxDRphGg1ZURlhXQMEcRLDSVF5UwV84XEK8Rc3dCw8KElsCEwwHz9A4NCuXAjeGI5B3kTBDOFJYWVlQ2UlY3UrQsQFUQVy0Q00w3FLGVNTBtcQdxb8ds7ugFFcdjAAJ0CAaz9yAAthqNG4iZCdKiWZpcQKEyHTKmTLrYrCGb5DRaGHKBGiYzKRoXZqeNodLoEB5PV6wD7sb5UQyRZisMbcQEIQRLGiSdRmfIqUxLCpw5QqcrQ4SGTTWUx5bGXPE3Ql3PjsZ4AVwwv1psXGCSEMjkkhkNFE-MNBxo4vEcM22wqOQxikMKwUktxrUk3nJ32IZEomFdnx+9BGdIBmoQdoUykkxmEyIhELMsjhobKoYUdsOIq2jo8zp9FK+LrdXwAkrcegEgl0BuF-X9AxrQIlDHspPzRKYbAsttYzfojCYZBYaEs5uCaCZuZmrm0c99877i4TkCQPn0oABbMCPfwANxenHIJEwquitYZwaspiT1U0c00qQKPZDGgOKVHcgk+SUGgn0uned-8+6RdlyCNcNwCL5UGebAAC9uHYA8j3+Ot+CMJFJFUMN9TtfI0RkBNxDtcoNHkSx+WMCoZG-bMC1nXMAOIbhYAVEg8H8CCoNgx4D38CBsCYz0wEQk94nrVDdgsJsTU-C91DhNJuQsCpRFHYiLwOKjrl-WjvnoohGOY1id2ePduN4-iKEE6s1XpESUJDGQlkkcUZE2RYnzUPY5M5cprBFDQEWEBzdg0qcaP-Es9NwJjnhY3B-AAQQAIW8fwAA0hPVU9RPszZJByMRG1Oc80jk8UNCcg1FFfVtajOJos00sKC10-SYtYpKUoATQymzGSsXLrDsZQ5EMA0DTwh9DnUUElMWNQoy-c4pWo31tKLCLWti-wyCgLoeqDbKDhyaQlCveQU2EUw5OUxEtnkdE0ROYQQrWtaWqigy4q6fB2D9Glj0y2yGxKGYVANE0Dn2UQ5IWbY5jbQxLujFRnqWp1GtW8LCUi6KtqYF58dwXjyEVTASFeMz4Is-bkIbbVthsIrrEsTDlDklNyuFKpTARaxkTEF6tKx7occ+tjIJguCD0wPRtpwKAhisgHerPBz+wtLQJGIjJ+QTDFw2OLZ-MCsR+TqnEGtCzHmo2j62rioyTMwGW5ewBWaayuyrEuvLZEccVFjMDEE2GkEth5qolmORxzeWjHcze23cdY2BcBIJh-HYVA0o9oGjAIqQHJHBQeeqMw5OyfttQNeZVC2QLKLRy3XuFhi7a21P08z7PuqVpDPeB59MjIhFxGFbI5MbGZdkcOQLyjZQckFpq5yTsWwAAR0VbjvqgX7c7644pAxY2ef1OQx4TWpWRyExR31ZQDVMZfrdX7HNtYphyel6g++EvrZAzFSEXUapwnxRgTKcIBpQFgnEyGoUQL8E6t1FvbfwzwwCrlQDufw5AP6PFgAfVWWh0JWGIsNTQo0dAPhFARCwixbDZBKGoYaSCZytwAEpgEEGAPgIRFRPCIYdOoUgDiGFOKkdIYhg4PnUP2dIqQ0S1yUAsNhf4bayi3tgDOAAZPAYBu6oEPH-QGfVUSCnqLYMQ1CiiBXDMiAi2o1CKPHE3ScLcNH3C0RnKmMBHjYG4uTcg3chF2SGtscEBoCKP0UCIhMxgyg3jrrE5Ei81GSHigAdxYuBCWnF4KYB4nxKmlB-B4AAGaoAIBAbgYB2i4C3KgD4kgYDsEEOxSWXFMCCAqagUJiQ0jZDyhoReJpahPVqHJJ85hNguX2MoWwlV0lZJyeLDiUtCmUwEmU3AlSCAvGeJBSQTAybsEqc8VcLS-DtLyRsnpuy+kmJVtlQZII7TIlTMRFyPM5Lgn7NkQco46g4UMMs7JHBty7mwPuTZxTtm9OqbU+pjTmmtMEI7aFB57mVP6YgfY-YDi2hMGicR+QvJKDZGIOQdhSiBTBasjFMKinmVKQig5RyTkkDOZBS5aLGVYt6bihA+K2T6jEPPI2Xk9gRkfqoNIcj2T0ohR1NKOy9k1NwHUvAKK6lopIAAI1gIIPg2LHn-X7nnYV0TZjDVfBfLYpVhoRgyE+Reshhq1CVQEFVqU1VVPZc8Y5pzzm8uuQao1JrBVPIOmEgiMwcjgiUK2fUWhSqLGkFsR+BFshRmfm4n8NEVnKuSv4TqfrEWauRU03VYbDWCD0KaoV8yM3yAqLYKBxE4SHDRHlbmyktiHGCvmlaCci3epLWWtlzxDmBs5dyi5Vy2nhvrY26NtM8VxosDhMeDl+Suq7W5CwCz5jV0JY3eq7itJju2vgLo5aNVaoadWxdggdpdFXea-+wZ9ig0sAcPIrYA7XQNE5Qdf6KhhgRF6m9u0DFTpnUGrlIaX1vu4VGz9pjv0EThtkfqVg1Dz2uojCM-6shyHPmiaDu9fr3qRdq59aLqNfA-QGTDLyCIghyPJcBew7ww1yoOWlKhEZWCoxuPeXxy0BsQ-O0NbSmMsZrGx2N6bjAmC2Isa8E0igOWlSelE-l7oGmg-jZ4hNiak3JsykpcGHkVsfTql9pnzPQss88QQWyLKKesjGgZasKpKGsMNBy1RoYPjmSCMejZ5iAYA7HdGVtR3goCM5jcFmyYUzhRZKT06OXBp5U5gmaXXMZY81lyg3nla+bxf58UgWRnaijmFnTAVpDChGY-E4cX0l6M1YYzApY+gVjiE2vU5gnx6hFJkdMJVws8wjHVk01hMLVB6-o-rRJvGZx2mBAJhSgkhLXQPPFCJyqjTMKOdQ3M2YPh5vGkGjgsiuIvQW1avWDFZyMZIQsuAOAEFGxIaQ3aITpn8pfB8JL0Il0uhzfUjhQXDvjjOd7-Xvu-fYP96krHnlhJctsE4NhFDKT9ioOE8MnKNgcHUbkpo1t9c+5gSQuAeUHkG+WEII2juWuW-j1suxhTQ7DGTmEGbSULL-XaPNL2R3I-WwzyQAA5bOAAFVAeB2CwAIPFCAEBAjwWMv4Fg6um3zHKjCMabYXKDg0HyBZvsI5ZEUKddJP2-tCoSfYzYWQVDnxNDbmhYI8pKIcoC3TCPpdI7zK7jHVIMM44bFhNkMIYQjNkKORsCZjiJM2OkBYunThYkR4lmcAAVHb-jAnPGCVnNn-ROdx+q8K4a5gFlKFNEt44menxsjcnqSwxEMxF9emX-Au3K-V6qXcLbKOGcm+RL7TImxxSDm04gNs4ZeZpBItbyjQ+tKKiJtnfixlfwAHlcD2arc0+K3gS+CAPzUwQx-2Bn8Vg39dIYoQVQWhiem+o4RGyzALRhguRkJPhqL+DM7+DlIkCUA9DDY8RgAwGkwpZkyapCqCCFQnTE4YiIzEQERXQQ46htjcjRZHAVCowR5tBkDYCrhcqtDdyG5oHdAPpX51I0F0FPCCBZyCAwGUBCoVCXiL4ArAKwhEERJWDMLfJRjqAvQcH0H4CMGcqaq17DaDBNqBQkbUqtgYQQIPgzwRh1DDRqb8g1ByG-acEMFZz+C8LFIMFki+iX70bNLyFcE8EOG5gCGPykKcgnDahpAIhwhPiRZ5AQiLx2ANaaB04faoBfZK7+Cq7q6a6kAWTGLv7HbCpja9piCHA5CIwmBwinDqziKKATbupVAuDnDM4YDwBRBxxQDY6N6CAg7SByCxIYSaA2JCDiQQjTI8x6gORIyIJD4EhgCNEf7MgkKVQ8zgjETXjmgpjlBTTnaSGWBS4WyXoFjjEZHcgMxe7Eq+5thdr9gLKVCHBRaOD8jpLCzbGWoHAnC6iLwphjyhZ1DxLJAHBjypDVAXi-HQYdL5KmTla2aVK3F9RhgMwZClzCg1CpC-LJDAhQjzBWLh4bGvZJYMpQpMqeasoPJglnhyIyrp4iLN6GBeTKRbpPgcwxYYjQY+p+r4mHQlxSBqCOIF52CjiEE6ZFR5SVSYRzSdh0kToMlKbx5GAlz9ihF5G9Gr7FCDggjVwUGthyLrH1GvTXqoYik+Yf4ijijoRQgJpVB5CnDXRqBsjyoVCqBzBb5iY-SSa9KMlewlw3zqaPwpgF7zAwwCiXTiKXFDFXF76FrJaG5FZEwlZWY4kgmoCOkNibAb7cjKS3jtglByS2izC+mtijSVCF5UGvQz6xGYAxl4r2AWC1SbA2DqBLAAGLGnB3Y1AlC7ColqlaT5lfbR5FnCqjRlCZAlDllVAQl8gfHzDpCp4p4LLRGo7M4XIHgdlijxqjzHDOKyCylZBlDtjiKlAuSmETny7xGJEEKznzCIinCyCNgZDQg3a2IIhsh-IUTuqKDnpoky5R7o4dnkQt7azMzghmAZ40IihJgigpjGjpgOiBmrQj5+KcDj7dyzmAG8yGguSVBcm9h5CPFhg8hZC-7DG5n76H567kwv4Fjn5vkLDxp6gLCEqSEAFbDoSXYqAxa3zYVPmR6QHZx8FjGilNF7BhzpDiKqB1CXTDR8g6hPj2D9paB+zmG0EKFQBKHMEkVSA8zi42DyD4aXmIDDThgmDqBzAzxTSUHMXUEWEyWMG2FMT2FbGcU6mOC6gIiYRjzcg0pdpzCCj2AQhRiBQXiLQuBAA */ | ||||
|     id: 'Modeling', | ||||
|  | ||||
|     tsTypes: {} as import('./modelingMachine.typegen').Typegen0, | ||||
| @ -292,7 +290,7 @@ export const modelingMachine = createMachine( | ||||
|               }, | ||||
|             }, | ||||
|  | ||||
|             entry: ['equip select'], | ||||
|             entry: 'setup client side sketch segments', | ||||
|           }, | ||||
|  | ||||
|           'Await horizontal distance info': { | ||||
| @ -381,8 +379,8 @@ export const modelingMachine = createMachine( | ||||
|  | ||||
|           'Line tool': { | ||||
|             exit: [ | ||||
|               'tear down client sketch', | ||||
|               'setup client side sketch segments', | ||||
|               // 'tear down client sketch', | ||||
|               // 'setup client side sketch segments', | ||||
|             ], | ||||
|  | ||||
|             on: { | ||||
| @ -427,6 +425,8 @@ export const modelingMachine = createMachine( | ||||
|                     target: 'normal', | ||||
|                     actions: 'set up draft line without teardown', | ||||
|                   }, | ||||
|  | ||||
|                   Cancel: '#Modeling.Sketch.undo startSketchOn', | ||||
|                 }, | ||||
|               }, | ||||
|             }, | ||||
| @ -445,11 +445,6 @@ export const modelingMachine = createMachine( | ||||
|           }, | ||||
|  | ||||
|           'Tangential arc to': { | ||||
|             exit: [ | ||||
|               'tear down client sketch', | ||||
|               'setup client side sketch segments', | ||||
|             ], | ||||
|  | ||||
|             entry: 'set up draft arc', | ||||
|  | ||||
|             on: { | ||||
| @ -461,6 +456,14 @@ export const modelingMachine = createMachine( | ||||
|               'Equip Line tool': 'Line tool', | ||||
|             }, | ||||
|           }, | ||||
|  | ||||
|           'undo startSketchOn': { | ||||
|             invoke: { | ||||
|               src: 'AST-undo-startSketchOn', | ||||
|               id: 'AST-undo-startSketchOn', | ||||
|               onDone: '#Modeling.idle', | ||||
|             }, | ||||
|           }, | ||||
|         }, | ||||
|  | ||||
|         initial: 'Init', | ||||
| @ -476,7 +479,7 @@ export const modelingMachine = createMachine( | ||||
|           'remove sketch grid', | ||||
|         ], | ||||
|  | ||||
|         entry: ['add axis n grid', 'setup client side sketch segments'], | ||||
|         entry: ['add axis n grid', 'conditionally equip line tool'], | ||||
|       }, | ||||
|  | ||||
|       'Sketch no face': { | ||||
| @ -602,7 +605,7 @@ export const modelingMachine = createMachine( | ||||
|         if (!sketchPathToNode) return {} | ||||
|         return getSketchMetadataFromPathToNode(sketchPathToNode) | ||||
|       }), | ||||
|       'hide default planes': () => setupSingleton.removeDefaultPlanes(), | ||||
|       'hide default planes': () => sceneInfra.removeDefaultPlanes(), | ||||
|       'reset sketch metadata': assign({ | ||||
|         sketchPathToNode: null, | ||||
|         sketchEnginePathId: '', | ||||
| @ -619,17 +622,7 @@ export const modelingMachine = createMachine( | ||||
|           selectionRanges | ||||
|         ) | ||||
|       }), | ||||
|       // TODO figure out why data isn't typed with sketchPathToNode and sketchNormalBackUp | ||||
|       'set new sketch metadata': assign((_, { data }) => data), | ||||
|       'equip select': () => | ||||
|         engineCommandManager.sendSceneCommand({ | ||||
|           type: 'modeling_cmd_req', | ||||
|           cmd_id: uuidv4(), | ||||
|           cmd: { | ||||
|             type: 'set_tool', | ||||
|             tool: 'select', | ||||
|           }, | ||||
|         }), | ||||
|       // TODO implement source ranges for all of these constraints | ||||
|       // need to make the async like the modal constraints | ||||
|       'Make selection horizontal': ({ selectionRanges, sketchPathToNode }) => { | ||||
| @ -639,7 +632,7 @@ export const modelingMachine = createMachine( | ||||
|           kclManager.ast, | ||||
|           kclManager.programMemory | ||||
|         ) | ||||
|         clientSideScene.updateAstAndRejigSketch( | ||||
|         sceneEntitiesManager.updateAstAndRejigSketch( | ||||
|           sketchPathToNode || [], | ||||
|           modifiedAst | ||||
|         ) | ||||
| @ -651,7 +644,7 @@ export const modelingMachine = createMachine( | ||||
|           kclManager.ast, | ||||
|           kclManager.programMemory | ||||
|         ) | ||||
|         clientSideScene.updateAstAndRejigSketch( | ||||
|         sceneEntitiesManager.updateAstAndRejigSketch( | ||||
|           sketchPathToNode || [], | ||||
|           modifiedAst | ||||
|         ) | ||||
| @ -664,7 +657,7 @@ export const modelingMachine = createMachine( | ||||
|           selectionRanges, | ||||
|           constraint: 'setVertDistance', | ||||
|         }) | ||||
|         clientSideScene.updateAstAndRejigSketch( | ||||
|         sceneEntitiesManager.updateAstAndRejigSketch( | ||||
|           sketchPathToNode || [], | ||||
|           modifiedAst | ||||
|         ) | ||||
| @ -674,7 +667,7 @@ export const modelingMachine = createMachine( | ||||
|           selectionRanges, | ||||
|           constraint: 'setHorzDistance', | ||||
|         }) | ||||
|         clientSideScene.updateAstAndRejigSketch( | ||||
|         sceneEntitiesManager.updateAstAndRejigSketch( | ||||
|           sketchPathToNode || [], | ||||
|           modifiedAst | ||||
|         ) | ||||
| @ -684,7 +677,7 @@ export const modelingMachine = createMachine( | ||||
|           selectionRanges, | ||||
|           constraint: 'snapToXAxis', | ||||
|         }) | ||||
|         clientSideScene.updateAstAndRejigSketch( | ||||
|         sceneEntitiesManager.updateAstAndRejigSketch( | ||||
|           sketchPathToNode || [], | ||||
|           modifiedAst | ||||
|         ) | ||||
| @ -694,7 +687,7 @@ export const modelingMachine = createMachine( | ||||
|           selectionRanges, | ||||
|           constraint: 'snapToYAxis', | ||||
|         }) | ||||
|         clientSideScene.updateAstAndRejigSketch( | ||||
|         sceneEntitiesManager.updateAstAndRejigSketch( | ||||
|           sketchPathToNode || [], | ||||
|           modifiedAst | ||||
|         ) | ||||
| @ -703,7 +696,7 @@ export const modelingMachine = createMachine( | ||||
|         const { modifiedAst } = applyConstraintEqualLength({ | ||||
|           selectionRanges, | ||||
|         }) | ||||
|         clientSideScene.updateAstAndRejigSketch( | ||||
|         sceneEntitiesManager.updateAstAndRejigSketch( | ||||
|           sketchPathToNode || [], | ||||
|           modifiedAst | ||||
|         ) | ||||
| @ -712,7 +705,7 @@ export const modelingMachine = createMachine( | ||||
|         const { modifiedAst } = applyConstraintEqualAngle({ | ||||
|           selectionRanges, | ||||
|         }) | ||||
|         clientSideScene.updateAstAndRejigSketch( | ||||
|         sceneEntitiesManager.updateAstAndRejigSketch( | ||||
|           sketchPathToNode || [], | ||||
|           modifiedAst | ||||
|         ) | ||||
| @ -724,7 +717,7 @@ export const modelingMachine = createMachine( | ||||
|         const { modifiedAst } = applyRemoveConstrainingValues({ | ||||
|           selectionRanges, | ||||
|         }) | ||||
|         clientSideScene.updateAstAndRejigSketch( | ||||
|         sceneEntitiesManager.updateAstAndRejigSketch( | ||||
|           sketchPathToNode || [], | ||||
|           modifiedAst | ||||
|         ) | ||||
| @ -747,49 +740,63 @@ export const modelingMachine = createMachine( | ||||
|           focusPath: pathToExtrudeArg, | ||||
|         }) | ||||
|       }, | ||||
|       'conditionally equip line tool': (_, { type }) => { | ||||
|         if (type === 'done.invoke.animate-to-face') { | ||||
|           sceneInfra.modelingSend('Equip Line tool') | ||||
|         } | ||||
|       }, | ||||
|       'setup client side sketch segments': ({ sketchPathToNode }, { type }) => { | ||||
|         if (type !== 'done.invoke.animate-to-face') { | ||||
|           clientSideScene.setupSketch({ | ||||
|         if (Object.keys(sceneEntitiesManager.activeSegments).length > 0) { | ||||
|           sceneEntitiesManager | ||||
|             .tearDownSketch({ removeAxis: false }) | ||||
|             .then(() => { | ||||
|               sceneEntitiesManager.setupSketch({ | ||||
|                 sketchPathToNode: sketchPathToNode || [], | ||||
|               }) | ||||
|             }) | ||||
|         } else { | ||||
|           setupSingleton.modelingSend('Equip Line tool') | ||||
|           sceneEntitiesManager.setupSketch({ | ||||
|             sketchPathToNode: sketchPathToNode || [], | ||||
|           }) | ||||
|         } | ||||
|       }, | ||||
|       'animate after sketch': () => { | ||||
|         clientSideScene.animateAfterSketch() | ||||
|         sceneEntitiesManager.animateAfterSketch() | ||||
|       }, | ||||
|       'tear down client sketch': () => | ||||
|         clientSideScene.tearDownSketch({ removeAxis: false }), | ||||
|       'remove sketch grid': () => clientSideScene.removeSketchGrid(), | ||||
|       'tear down client sketch': () => { | ||||
|         if (sceneEntitiesManager.activeSegments) { | ||||
|           sceneEntitiesManager.tearDownSketch({ removeAxis: false }) | ||||
|         } | ||||
|       }, | ||||
|       'remove sketch grid': () => sceneEntitiesManager.removeSketchGrid(), | ||||
|       'set up draft line': ({ sketchPathToNode }) => { | ||||
|         clientSideScene.setUpDraftLine(sketchPathToNode || []) | ||||
|         sceneEntitiesManager.setUpDraftLine(sketchPathToNode || []) | ||||
|       }, | ||||
|       'set up draft arc': ({ sketchPathToNode }) => { | ||||
|         clientSideScene.setUpDraftArc(sketchPathToNode || []) | ||||
|         sceneEntitiesManager.setUpDraftArc(sketchPathToNode || []) | ||||
|       }, | ||||
|       'set up draft line without teardown': ({ sketchPathToNode }) => | ||||
|         clientSideScene.setupSketch({ | ||||
|         sceneEntitiesManager.setupSketch({ | ||||
|           sketchPathToNode: sketchPathToNode || [], | ||||
|           draftSegment: 'line', | ||||
|         }), | ||||
|       'show default planes': () => { | ||||
|         setupSingleton.showDefaultPlanes() | ||||
|         clientSideScene.setupDefaultPlaneHover() | ||||
|         sceneInfra.showDefaultPlanes() | ||||
|         sceneEntitiesManager.setupDefaultPlaneHover() | ||||
|       }, | ||||
|       'setup noPoints onClick listener': ({ sketchPathToNode }) => { | ||||
|         clientSideScene.createIntersectionPlane() | ||||
|         sceneEntitiesManager.createIntersectionPlane() | ||||
|         const sketchGroup = sketchGroupFromPathToNode({ | ||||
|           pathToNode: sketchPathToNode || [], | ||||
|           ast: kclManager.ast, | ||||
|           programMemory: kclManager.programMemory, | ||||
|         }) | ||||
|         const quaternion = quaternionFromSketchGroup(sketchGroup) | ||||
|         clientSideScene.intersectionPlane && | ||||
|           clientSideScene.intersectionPlane.setRotationFromQuaternion( | ||||
|         sceneEntitiesManager.intersectionPlane && | ||||
|           sceneEntitiesManager.intersectionPlane.setRotationFromQuaternion( | ||||
|             quaternion | ||||
|           ) | ||||
|         setupSingleton.setCallbacks({ | ||||
|         sceneInfra.setCallbacks({ | ||||
|           onClick: async (args) => { | ||||
|             if (!args) return | ||||
|             const { intersection2d } = args | ||||
| @ -800,13 +807,13 @@ export const modelingMachine = createMachine( | ||||
|               [intersection2d.x, intersection2d.y] | ||||
|             ) | ||||
|             await kclManager.updateAst(modifiedAst, false) | ||||
|             clientSideScene.removeIntersectionPlane() | ||||
|             setupSingleton.modelingSend('Add start point') | ||||
|             sceneEntitiesManager.removeIntersectionPlane() | ||||
|             sceneInfra.modelingSend('Add start point') | ||||
|           }, | ||||
|         }) | ||||
|       }, | ||||
|       'add axis n grid': ({ sketchPathToNode }) => | ||||
|         clientSideScene.createSketchAxis(sketchPathToNode || []), | ||||
|         sceneEntitiesManager.createSketchAxis(sketchPathToNode || []), | ||||
|     }, | ||||
|     // end actions | ||||
|   } | ||||
|  | ||||
| @ -22,7 +22,6 @@ const SignIn = () => { | ||||
|     }, | ||||
|   } = useGlobalStateContext() | ||||
|  | ||||
|   const appliedTheme = theme === Themes.System ? getSystemTheme() : theme | ||||
|   const signInTauri = async () => { | ||||
|     // We want to invoke our command to login via device auth. | ||||
|     try { | ||||
|  | ||||
| @ -862,7 +862,7 @@ impl ExtrudeSurface { | ||||
|  | ||||
|     pub fn get_name(&self) -> String { | ||||
|         match self { | ||||
|             ExtrudeSurface::ExtrudePlane(ep) => ep.name.clone(), | ||||
|             ExtrudeSurface::ExtrudePlane(ep) => ep.name.to_string(), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
| @ -8,12 +8,11 @@ pub use local::FileManager; | ||||
| #[cfg(target_arch = "wasm32")] | ||||
| #[cfg(not(test))] | ||||
| pub mod wasm; | ||||
| use anyhow::Result; | ||||
| #[cfg(target_arch = "wasm32")] | ||||
| #[cfg(not(test))] | ||||
| pub use wasm::FileManager; | ||||
|  | ||||
| use anyhow::Result; | ||||
|  | ||||
| #[async_trait::async_trait(?Send)] | ||||
| pub trait FileSystem: Clone { | ||||
|     /// Read a file from the local file system. | ||||
|  | ||||
| @ -123,7 +123,10 @@ async fn inner_extrude(length: f64, sketch_group: Box<SketchGroup>, args: Args) | ||||
|     } | ||||
|  | ||||
|     Ok(Box::new(ExtrudeGroup { | ||||
|         id, | ||||
|         // Ok so you would think that the id would be the id of the extrude group, | ||||
|         // that we passed in to the function, but it's actually the id of the | ||||
|         // sketch group. | ||||
|         id: sketch_group.id, | ||||
|         value: new_value, | ||||
|         height: length, | ||||
|         position: sketch_group.position, | ||||
|  | ||||
| @ -20,9 +20,9 @@ use parse_display::{Display, FromStr}; | ||||
| use schemars::JsonSchema; | ||||
| use serde::{Deserialize, Serialize}; | ||||
|  | ||||
| use self::kcl_stdlib::KclStdLibFn; | ||||
| use self::{kcl_stdlib::KclStdLibFn, sketch::SketchOnFaceTag}; | ||||
| use crate::{ | ||||
|     ast::types::{parse_json_number_as_f64, parse_json_value_as_string}, | ||||
|     ast::types::parse_json_number_as_f64, | ||||
|     docs::StdLibFn, | ||||
|     engine::EngineManager, | ||||
|     errors::{KclError, KclErrorDetails}, | ||||
| @ -73,6 +73,7 @@ lazy_static! { | ||||
|         Box::new(crate::std::sketch::BezierCurve), | ||||
|         Box::new(crate::std::sketch::Hole), | ||||
|         Box::new(crate::std::patterns::PatternLinear), | ||||
|         Box::new(crate::std::patterns::PatternCircular), | ||||
|         Box::new(crate::std::import::Import), | ||||
|         Box::new(crate::std::math::Cos), | ||||
|         Box::new(crate::std::math::Sin), | ||||
| @ -406,7 +407,9 @@ impl Args { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn get_data_and_optional_tag<T: serde::de::DeserializeOwned>(&self) -> Result<(T, Option<String>), KclError> { | ||||
|     fn get_data_and_optional_tag<T: serde::de::DeserializeOwned>( | ||||
|         &self, | ||||
|     ) -> Result<(T, Option<SketchOnFaceTag>), KclError> { | ||||
|         let first_value = self | ||||
|             .args | ||||
|             .first() | ||||
| @ -426,8 +429,13 @@ impl Args { | ||||
|         })?; | ||||
|  | ||||
|         if let Some(second_value) = self.args.get(1) { | ||||
|             let tag = parse_json_value_as_string(&second_value.get_json_value()?); | ||||
|             Ok((data, tag)) | ||||
|             let tag: SketchOnFaceTag = serde_json::from_value(second_value.get_json_value()?).map_err(|e| { | ||||
|                 KclError::Type(KclErrorDetails { | ||||
|                     message: format!("Failed to deserialize SketchOnFaceTag from JSON: {}", e), | ||||
|                     source_ranges: vec![self.source_range], | ||||
|                 }) | ||||
|             })?; | ||||
|             Ok((data, Some(tag))) | ||||
|         } else { | ||||
|             Ok((data, None)) | ||||
|         } | ||||
|  | ||||
| @ -27,6 +27,25 @@ pub struct LinearPatternData { | ||||
|     pub axis: [f64; 3], | ||||
| } | ||||
|  | ||||
| /// Data for a circular pattern. | ||||
| #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] | ||||
| #[ts(export)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct CircularPatternData { | ||||
|     /// The number of repetitions. Must be greater than 0. | ||||
|     /// This excludes the original entity. For example, if `repetitions` is 1, | ||||
|     /// the original entity will be copied once. | ||||
|     pub repetitions: usize, | ||||
|     /// The axis around which to make the pattern. This is a 3D vector. | ||||
|     pub axis: [f64; 3], | ||||
|     /// The center about which to make th pattern. This is a 3D vector. | ||||
|     pub center: [f64; 3], | ||||
|     /// The arc angle (in degrees) to place the repetitions. Must be greater than 0. | ||||
|     pub arc_degrees: f64, | ||||
|     /// Whether or not to rotate the duplicates as they are copied. | ||||
|     pub rotate_duplicates: bool, | ||||
| } | ||||
|  | ||||
| /// A linear pattern. | ||||
| pub async fn pattern_linear(args: Args) -> Result<MemoryItem, KclError> { | ||||
|     let (data, geometry): (LinearPatternData, Geometry) = args.get_data_and_geometry()?; | ||||
| @ -47,6 +66,26 @@ pub async fn pattern_linear(args: Args) -> Result<MemoryItem, KclError> { | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// A circular pattern. | ||||
| pub async fn pattern_circular(args: Args) -> Result<MemoryItem, KclError> { | ||||
|     let (data, geometry): (CircularPatternData, Geometry) = args.get_data_and_geometry()?; | ||||
|  | ||||
|     if data.axis == [0.0, 0.0, 0.0] { | ||||
|         return Err(KclError::Semantic(KclErrorDetails { | ||||
|             message: | ||||
|                 "The axis of the circular pattern cannot be the zero vector. Otherwise they will just duplicate in place." | ||||
|                     .to_string(), | ||||
|             source_ranges: vec![args.source_range], | ||||
|         })); | ||||
|     } | ||||
|  | ||||
|     let new_geometries = inner_pattern_circular(data, geometry, args).await?; | ||||
|     match new_geometries { | ||||
|         Geometries::SketchGroups(sketch_groups) => Ok(MemoryItem::SketchGroups { value: sketch_groups }), | ||||
|         Geometries::ExtrudeGroups(extrude_groups) => Ok(MemoryItem::ExtrudeGroups { value: extrude_groups }), | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// A linear pattern. | ||||
| #[stdlib { | ||||
|     name = "patternLinear", | ||||
| @ -99,3 +138,62 @@ async fn inner_pattern_linear(data: LinearPatternData, geometry: Geometry, args: | ||||
|  | ||||
|     Ok(geometries) | ||||
| } | ||||
|  | ||||
| /// A Circular pattern. | ||||
| #[stdlib { | ||||
|     name = "patternCircular", | ||||
| }] | ||||
| async fn inner_pattern_circular( | ||||
|     data: CircularPatternData, | ||||
|     geometry: Geometry, | ||||
|     args: Args, | ||||
| ) -> Result<Geometries, KclError> { | ||||
|     let id = uuid::Uuid::new_v4(); | ||||
|  | ||||
|     let resp = args | ||||
|         .send_modeling_cmd( | ||||
|             id, | ||||
|             ModelingCmd::EntityCircularPattern { | ||||
|                 axis: data.axis.into(), | ||||
|                 entity_id: geometry.id(), | ||||
|                 center: data.center.into(), | ||||
|                 num_repetitions: data.repetitions as u32, | ||||
|                 arc_degrees: data.arc_degrees, | ||||
|                 rotate_duplicates: data.rotate_duplicates, | ||||
|             }, | ||||
|         ) | ||||
|         .await?; | ||||
|  | ||||
|     let kittycad::types::OkWebSocketResponseData::Modeling { | ||||
|         modeling_response: kittycad::types::OkModelingCmdResponse::EntityCircularPattern { data: pattern_info }, | ||||
|     } = &resp | ||||
|     else { | ||||
|         return Err(KclError::Engine(KclErrorDetails { | ||||
|             message: format!("EntityCircularPattern response was not as expected: {:?}", resp), | ||||
|             source_ranges: vec![args.source_range], | ||||
|         })); | ||||
|     }; | ||||
|  | ||||
|     let geometries = match geometry { | ||||
|         Geometry::SketchGroup(sketch_group) => { | ||||
|             let mut geometries = vec![sketch_group.clone()]; | ||||
|             for id in pattern_info.entity_ids.iter() { | ||||
|                 let mut new_sketch_group = sketch_group.clone(); | ||||
|                 new_sketch_group.id = *id; | ||||
|                 geometries.push(new_sketch_group); | ||||
|             } | ||||
|             Geometries::SketchGroups(geometries) | ||||
|         } | ||||
|         Geometry::ExtrudeGroup(extrude_group) => { | ||||
|             let mut geometries = vec![extrude_group.clone()]; | ||||
|             for id in pattern_info.entity_ids.iter() { | ||||
|                 let mut new_extrude_group = extrude_group.clone(); | ||||
|                 new_extrude_group.id = *id; | ||||
|                 geometries.push(new_extrude_group); | ||||
|             } | ||||
|             Geometries::ExtrudeGroups(geometries) | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     Ok(geometries) | ||||
| } | ||||
|  | ||||
| @ -4,6 +4,7 @@ use anyhow::Result; | ||||
| use derive_docs::stdlib; | ||||
| use kittycad::types::{Angle, ModelingCmd, Point3D}; | ||||
| use kittycad_execution_plan_macros::ExecutionPlanValue; | ||||
| use parse_display::{Display, FromStr}; | ||||
| use schemars::JsonSchema; | ||||
| use serde::{Deserialize, Serialize}; | ||||
|  | ||||
| @ -813,7 +814,7 @@ impl SketchSurface { | ||||
|  | ||||
| /// Start a sketch on a specific plane or face. | ||||
| pub async fn start_sketch_on(args: Args) -> Result<MemoryItem, KclError> { | ||||
|     let (data, tag): (SketchData, Option<String>) = args.get_data_and_optional_tag()?; | ||||
|     let (data, tag): (SketchData, Option<SketchOnFaceTag>) = args.get_data_and_optional_tag()?; | ||||
|  | ||||
|     match inner_start_sketch_on(data, tag, args).await? { | ||||
|         SketchSurface::Plane(plane) => Ok(MemoryItem::Plane(plane)), | ||||
| @ -825,7 +826,11 @@ pub async fn start_sketch_on(args: Args) -> Result<MemoryItem, KclError> { | ||||
| #[stdlib { | ||||
|     name = "startSketchOn", | ||||
| }] | ||||
| async fn inner_start_sketch_on(data: SketchData, tag: Option<String>, args: Args) -> Result<SketchSurface, KclError> { | ||||
| async fn inner_start_sketch_on( | ||||
|     data: SketchData, | ||||
|     tag: Option<SketchOnFaceTag>, | ||||
|     args: Args, | ||||
| ) -> Result<SketchSurface, KclError> { | ||||
|     match data { | ||||
|         SketchData::Plane(plane_data) => { | ||||
|             let plane = start_sketch_on_plane(plane_data, args).await?; | ||||
| @ -838,18 +843,51 @@ async fn inner_start_sketch_on(data: SketchData, tag: Option<String>, args: Args | ||||
|                     source_ranges: vec![args.source_range], | ||||
|                 })); | ||||
|             }; | ||||
|             let face = start_sketch_on_face(extrude_group, &tag, args).await?; | ||||
|             let face = start_sketch_on_face(extrude_group, tag, args).await?; | ||||
|             Ok(SketchSurface::Face(face)) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| async fn start_sketch_on_face(extrude_group: Box<ExtrudeGroup>, tag: &str, args: Args) -> Result<Box<Face>, KclError> { | ||||
|     let extrude_plane = extrude_group | ||||
| /// A tag for sketch on face. | ||||
| #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, FromStr, Display)] | ||||
| #[ts(export)] | ||||
| #[serde(rename_all = "snake_case", untagged)] | ||||
| #[display("{0}")] | ||||
| pub enum SketchOnFaceTag { | ||||
|     StartOrEnd(StartOrEnd), | ||||
|     /// A string tag for the face you want to sketch on. | ||||
|     String(String), | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, FromStr, Display)] | ||||
| #[ts(export)] | ||||
| #[serde(rename_all = "snake_case")] | ||||
| #[display(style = "snake_case")] | ||||
| pub enum StartOrEnd { | ||||
|     /// The start face as in before you extruded. This could also be known as the bottom | ||||
|     /// face. But we do not call it bottom because it would be the top face if you | ||||
|     /// extruded it in the opposite direction or flipped the camera. | ||||
|     #[serde(rename = "start", alias = "START")] | ||||
|     Start, | ||||
|     /// The end face after you extruded. This could also be known as the top | ||||
|     /// face. But we do not call it top because it would be the bottom face if you | ||||
|     /// extruded it in the opposite direction or flipped the camera. | ||||
|     #[serde(rename = "end", alias = "END")] | ||||
|     End, | ||||
| } | ||||
|  | ||||
| async fn start_sketch_on_face( | ||||
|     extrude_group: Box<ExtrudeGroup>, | ||||
|     tag: SketchOnFaceTag, | ||||
|     args: Args, | ||||
| ) -> Result<Box<Face>, KclError> { | ||||
|     let extrude_plane_id = match tag { | ||||
|         SketchOnFaceTag::String(ref s) => extrude_group | ||||
|             .value | ||||
|             .iter() | ||||
|             .find_map(|extrude_surface| match extrude_surface { | ||||
|             ExtrudeSurface::ExtrudePlane(extrude_plane) if extrude_plane.name == tag => Some(extrude_plane), | ||||
|                 ExtrudeSurface::ExtrudePlane(extrude_plane) if extrude_plane.name == *s => Some(extrude_plane.face_id), | ||||
|                 ExtrudeSurface::ExtrudePlane(_) => None, | ||||
|             }) | ||||
|             .ok_or_else(|| { | ||||
| @ -857,7 +895,20 @@ async fn start_sketch_on_face(extrude_group: Box<ExtrudeGroup>, tag: &str, args: | ||||
|                     message: format!("Expected a face with the tag `{}`", tag), | ||||
|                     source_ranges: vec![args.source_range], | ||||
|                 }) | ||||
|         })?; | ||||
|             })?, | ||||
|         SketchOnFaceTag::StartOrEnd(StartOrEnd::Start) => extrude_group.start_cap_id.ok_or_else(|| { | ||||
|             KclError::Type(KclErrorDetails { | ||||
|                 message: "Expected a start face to sketch on".to_string(), | ||||
|                 source_ranges: vec![args.source_range], | ||||
|             }) | ||||
|         })?, | ||||
|         SketchOnFaceTag::StartOrEnd(StartOrEnd::End) => extrude_group.end_cap_id.ok_or_else(|| { | ||||
|             KclError::Type(KclErrorDetails { | ||||
|                 message: "Expected an end face to sketch on".to_string(), | ||||
|                 source_ranges: vec![args.source_range], | ||||
|             }) | ||||
|         })?, | ||||
|     }; | ||||
|  | ||||
|     // Enter sketch mode on the face. | ||||
|     let id = uuid::Uuid::new_v4(); | ||||
| @ -866,7 +917,7 @@ async fn start_sketch_on_face(extrude_group: Box<ExtrudeGroup>, tag: &str, args: | ||||
|         ModelingCmd::EnableSketchMode { | ||||
|             animated: false, | ||||
|             ortho: false, | ||||
|             entity_id: extrude_plane.face_id, | ||||
|             entity_id: extrude_plane_id, | ||||
|         }, | ||||
|     ) | ||||
|     .await?; | ||||
| @ -1645,4 +1696,43 @@ mod tests { | ||||
|         let data: PlaneData = serde_json::from_str(&str_json).unwrap(); | ||||
|         assert_eq!(data, PlaneData::NegXZ); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_deserialize_sketch_on_face_tag() { | ||||
|         let data = "start"; | ||||
|         let mut str_json = serde_json::to_string(&data).unwrap(); | ||||
|         assert_eq!(str_json, "\"start\""); | ||||
|  | ||||
|         str_json = "\"end\"".to_string(); | ||||
|         let data: crate::std::sketch::SketchOnFaceTag = serde_json::from_str(&str_json).unwrap(); | ||||
|         assert_eq!( | ||||
|             data, | ||||
|             crate::std::sketch::SketchOnFaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::End) | ||||
|         ); | ||||
|  | ||||
|         str_json = "\"thing\"".to_string(); | ||||
|         let data: crate::std::sketch::SketchOnFaceTag = serde_json::from_str(&str_json).unwrap(); | ||||
|         assert_eq!(data, crate::std::sketch::SketchOnFaceTag::String("thing".to_string())); | ||||
|  | ||||
|         str_json = "\"END\"".to_string(); | ||||
|         let data: crate::std::sketch::SketchOnFaceTag = serde_json::from_str(&str_json).unwrap(); | ||||
|         assert_eq!( | ||||
|             data, | ||||
|             crate::std::sketch::SketchOnFaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::End) | ||||
|         ); | ||||
|  | ||||
|         str_json = "\"start\"".to_string(); | ||||
|         let data: crate::std::sketch::SketchOnFaceTag = serde_json::from_str(&str_json).unwrap(); | ||||
|         assert_eq!( | ||||
|             data, | ||||
|             crate::std::sketch::SketchOnFaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::Start) | ||||
|         ); | ||||
|  | ||||
|         str_json = "\"START\"".to_string(); | ||||
|         let data: crate::std::sketch::SketchOnFaceTag = serde_json::from_str(&str_json).unwrap(); | ||||
|         assert_eq!( | ||||
|             data, | ||||
|             crate::std::sketch::SketchOnFaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::Start) | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -91,6 +91,62 @@ const part002 = startSketchOn(part001, "here") | ||||
|     twenty_twenty::assert_image("tests/executor/outputs/sketch_on_face.png", &result, 0.999); | ||||
| } | ||||
|  | ||||
| #[tokio::test(flavor = "multi_thread")] | ||||
| async fn serial_test_sketch_on_face_start() { | ||||
|     let code = r#"fn cube = (pos, scale) => { | ||||
|   const sg = startSketchOn('XY') | ||||
|     |> startProfileAt(pos, %) | ||||
|     |> line([0, scale], %) | ||||
|     |> line([scale, 0], %) | ||||
|     |> line([0, -scale], %) | ||||
|  | ||||
|   return sg | ||||
| } | ||||
| const part001 = cube([0,0], 20) | ||||
|     |> close(%) | ||||
|     |> extrude(20, %) | ||||
|  | ||||
| const part002 = startSketchOn(part001, "start") | ||||
|   |> startProfileAt([0, 0], %) | ||||
|   |> line([0, 10], %) | ||||
|   |> line([10, 0], %) | ||||
|   |> line([0, -10], %) | ||||
|   |> close(%) | ||||
|   |> extrude(5, %) | ||||
| "#; | ||||
|  | ||||
|     let result = execute_and_snapshot(code).await.unwrap(); | ||||
|     twenty_twenty::assert_image("tests/executor/outputs/sketch_on_face_start.png", &result, 0.999); | ||||
| } | ||||
|  | ||||
| #[tokio::test(flavor = "multi_thread")] | ||||
| async fn serial_test_sketch_on_face_end() { | ||||
|     let code = r#"fn cube = (pos, scale) => { | ||||
|   const sg = startSketchOn('XY') | ||||
|     |> startProfileAt(pos, %) | ||||
|     |> line([0, scale], %) | ||||
|     |> line([scale, 0], %) | ||||
|     |> line([0, -scale], %) | ||||
|  | ||||
|   return sg | ||||
| } | ||||
| const part001 = cube([0,0], 20) | ||||
|     |> close(%) | ||||
|     |> extrude(20, %) | ||||
|  | ||||
| const part002 = startSketchOn(part001, "END") | ||||
|   |> startProfileAt([0, 0], %) | ||||
|   |> line([0, 10], %) | ||||
|   |> line([10, 0], %) | ||||
|   |> line([0, -10], %) | ||||
|   |> close(%) | ||||
|   |> extrude(5, %) | ||||
| "#; | ||||
|  | ||||
|     let result = execute_and_snapshot(code).await.unwrap(); | ||||
|     twenty_twenty::assert_image("tests/executor/outputs/sketch_on_face_end.png", &result, 0.999); | ||||
| } | ||||
|  | ||||
| #[tokio::test(flavor = "multi_thread")] | ||||
| async fn serial_test_execute_with_function_sketch() { | ||||
|     let code = r#"fn box = (h, l, w) => { | ||||
| @ -712,6 +768,88 @@ const rectangle = startSketchOn('XY') | ||||
|     twenty_twenty::assert_image("tests/executor/outputs/patterns_linear_basic_holes.png", &result, 0.999); | ||||
| } | ||||
|  | ||||
| #[tokio::test(flavor = "multi_thread")] | ||||
| async fn serial_test_patterns_circular_basic_2d() { | ||||
|     let code = r#"fn circle = (pos, radius) => { | ||||
|   const sg = startSketchOn('XY') | ||||
|     |> startProfileAt([pos[0] + radius, pos[1]], %) | ||||
|     |> arc({ | ||||
|        angle_end: 360, | ||||
|        angle_start: 0, | ||||
|        radius: radius | ||||
|      }, %) | ||||
|     |> close(%) | ||||
|   return sg | ||||
| } | ||||
|  | ||||
| const part = circle([0,0], 2) | ||||
|     |> patternCircular({axis: [0,0,1], center: [20, 20, 20], repetitions: 12, arcDegrees: 210, rotateDuplicates: true}, %) | ||||
| "#; | ||||
|  | ||||
|     let result = execute_and_snapshot(code).await.unwrap(); | ||||
|     twenty_twenty::assert_image("tests/executor/outputs/patterns_circular_basic_2d.png", &result, 0.999); | ||||
| } | ||||
|  | ||||
| #[tokio::test(flavor = "multi_thread")] | ||||
| async fn serial_test_patterns_circular_basic_3d() { | ||||
|     let code = r#"fn circle = (pos, radius) => { | ||||
|   const sg = startSketchOn('XY') | ||||
|     |> startProfileAt([pos[0] + radius, pos[1]], %) | ||||
|     |> arc({ | ||||
|        angle_end: 360, | ||||
|        angle_start: 0, | ||||
|        radius: radius | ||||
|      }, %) | ||||
|     |> close(%) | ||||
|   return sg | ||||
| } | ||||
|  | ||||
| const part = startSketchOn('XY') | ||||
|     |> startProfileAt([0, 0], %) | ||||
|     |> line([0,1], %) | ||||
|     |> line([1, 0], %) | ||||
|     |> line([0, -1], %) | ||||
|     |> close(%) | ||||
|     |> extrude(1, %) | ||||
|     |> patternCircular({axis: [0,1,0], center: [-20, -20, -20], repetitions: 40, arcDegrees: 360, rotateDuplicates: false}, %) | ||||
| "#; | ||||
|  | ||||
|     let result = execute_and_snapshot(code).await.unwrap(); | ||||
|     twenty_twenty::assert_image("tests/executor/outputs/patterns_circular_basic_3d.png", &result, 0.999); | ||||
| } | ||||
|  | ||||
| #[tokio::test(flavor = "multi_thread")] | ||||
| async fn serial_test_patterns_circular_3d_tilted_axis() { | ||||
|     let code = r#"fn circle = (pos, radius) => { | ||||
|   const sg = startSketchOn('XY') | ||||
|     |> startProfileAt([pos[0] + radius, pos[1]], %) | ||||
|     |> arc({ | ||||
|        angle_end: 360, | ||||
|        angle_start: 0, | ||||
|        radius: radius | ||||
|      }, %) | ||||
|     |> close(%) | ||||
|   return sg | ||||
| } | ||||
|  | ||||
| const part = startSketchOn('XY') | ||||
|     |> startProfileAt([0, 0], %) | ||||
|     |> line([0,1], %) | ||||
|     |> line([1, 0], %) | ||||
|     |> line([0, -1], %) | ||||
|     |> close(%) | ||||
|     |> extrude(1, %) | ||||
|     |> patternCircular({axis: [1,1,-1], center: [10, 0, 10], repetitions: 10, arcDegrees: 360, rotateDuplicates: true}, %) | ||||
| "#; | ||||
|  | ||||
|     let result = execute_and_snapshot(code).await.unwrap(); | ||||
|     twenty_twenty::assert_image( | ||||
|         "tests/executor/outputs/patterns_circular_3d_tilted_axis.png", | ||||
|         &result, | ||||
|         0.999, | ||||
|     ); | ||||
| } | ||||
|  | ||||
| #[tokio::test(flavor = "multi_thread")] | ||||
| async fn serial_test_import_file_doesnt_exist() { | ||||
|     let code = r#"const model = import("thing.obj")"#; | ||||
|  | ||||
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 70 KiB | 
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 72 KiB | 
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 74 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/wasm-lib/tests/executor/outputs/sketch_on_face_end.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/wasm-lib/tests/executor/outputs/sketch_on_face_end.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 35 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/wasm-lib/tests/executor/outputs/sketch_on_face_start.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/wasm-lib/tests/executor/outputs/sketch_on_face_start.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 36 KiB | 
		Reference in New Issue
	
	Block a user