Compare commits
	
		
			25 Commits
		
	
	
		
			franknoiro
			...
			kurt-xstat
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 2f36b6b7cb | |||
| a37ccec39e | |||
| 6287c943dd | |||
| 6ef052cb7f | |||
| 7f10ea7371 | |||
| 00dcd3ac78 | |||
| 1baa3819db | |||
| 448db9d48c | |||
| 23b484d365 | |||
| 672cd652a8 | |||
| cad4e18530 | |||
| 484717a354 | |||
| c989340bcf | |||
| 5dc1adacae | |||
| f2ea91b1ba | |||
| b8ceea179c | |||
| 149130d264 | |||
| b9e544d410 | |||
| a4e39ce2e9 | |||
| 18cf6113d5 | |||
| 5dea7fd042 | |||
| 809ea86bfa | |||
| c23b046c5e | |||
| 456d4912ca | |||
| 522352ec75 | 
| @ -1470,9 +1470,13 @@ test('Sketch on face', async ({ page, context }) => { | ||||
|   await page.getByText('startProfileAt([1.03, 1.03], %)').click() | ||||
|   await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible() | ||||
|   await page.getByRole('button', { name: 'Edit Sketch' }).click() | ||||
|   await page.setViewportSize({ width: 1200, height: 1200 }) | ||||
|   await u.openAndClearDebugPanel() | ||||
|   await u.updateCamPosition([452, -152, 1166]) | ||||
|   await u.closeDebugPanel() | ||||
|   await page.waitForTimeout(200) | ||||
|  | ||||
|   const pointToDragFirst = [691, 237] | ||||
|   const pointToDragFirst = [787, 565] | ||||
|   await page.mouse.move(pointToDragFirst[0], pointToDragFirst[1]) | ||||
|   await page.mouse.down() | ||||
|   await page.mouse.move(pointToDragFirst[0] - 20, pointToDragFirst[1], { | ||||
| @ -1486,7 +1490,9 @@ test('Sketch on face', async ({ page, context }) => { | ||||
|   await expect(page.locator('.cm-content')) | ||||
|     .toContainText(`const part002 = startSketchOn(part001, 'seg01') | ||||
| |> startProfileAt([1.03, 1.03], %) | ||||
| |> line([2.81, -0.33], %) | ||||
| |> line([${process?.env?.CI ? 2.74 : 2.93}, -${ | ||||
|     process?.env?.CI ? 0.24 : 0.2 | ||||
|   }], %) | ||||
| |> line([-4.44, -2.13], %) | ||||
| |> close(%)`) | ||||
|  | ||||
| @ -1509,7 +1515,9 @@ test('Sketch on face', async ({ page, context }) => { | ||||
|   await expect(page.locator('.cm-content')) | ||||
|     .toContainText(`const part002 = startSketchOn(part001, 'seg01') | ||||
| |> startProfileAt([1.03, 1.03], %) | ||||
| |> line([2.81, -0.33], %) | ||||
| |> line([${process?.env?.CI ? 2.74 : 2.93}, -${ | ||||
|     process?.env?.CI ? 0.24 : 0.2 | ||||
|   }], %) | ||||
| |> line([-4.44, -2.13], %) | ||||
| |> close(%) | ||||
| |> extrude(5 + 7, %)`) | ||||
|  | ||||
| Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB | 
| Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 48 KiB | 
| Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB | 
| Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 39 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/clientSideSceneAssets/extra-segment-texture.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 327 B | 
| @ -28,12 +28,15 @@ export function createGridHelper({ | ||||
|   gridHelper.rotation.x = Math.PI / 2 | ||||
|   return gridHelper | ||||
| } | ||||
| const fudgeFactor = 72.66985970437086 | ||||
|  | ||||
| export const orthoScale = (cam: OrthographicCamera | PerspectiveCamera) => | ||||
|   0.55 / cam.zoom | ||||
|   (0.55 * fudgeFactor) / cam.zoom / window.innerHeight | ||||
|  | ||||
| export const perspScale = (cam: PerspectiveCamera, group: Group | Mesh) => | ||||
|   (group.position.distanceTo(cam.position) * cam.fov) / 4000 | ||||
|   (group.position.distanceTo(cam.position) * cam.fov * fudgeFactor) / | ||||
|   4000 / | ||||
|   window.innerHeight | ||||
|  | ||||
| export function isQuaternionVertical(q: Quaternion) { | ||||
|   const v = new Vector3(0, 0, 1).applyQuaternion(q) | ||||
|  | ||||
| @ -12,6 +12,7 @@ import { | ||||
|   OrthographicCamera, | ||||
|   PerspectiveCamera, | ||||
|   PlaneGeometry, | ||||
|   Points, | ||||
|   Quaternion, | ||||
|   Scene, | ||||
|   Shape, | ||||
| @ -87,14 +88,17 @@ import { EngineCommandManager } from 'lang/std/engineConnection' | ||||
|  | ||||
| type DraftSegment = 'line' | 'tangentialArcTo' | ||||
|  | ||||
| export const EXTRA_SEGMENT_HANDLE = 'extraSegmentHandle' | ||||
| export const EXTRA_SEGMENT_OFFSET_PX = 8 | ||||
| export const PROFILE_START = 'profile-start' | ||||
| export const STRAIGHT_SEGMENT = 'straight-segment' | ||||
| export const STRAIGHT_SEGMENT_BODY = 'straight-segment-body' | ||||
| export const STRAIGHT_SEGMENT_DASH = 'straight-segment-body-dashed' | ||||
| export const TANGENTIAL_ARC_TO_SEGMENT = 'tangential-arc-to-segment' | ||||
| 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' | ||||
| export const PROFILE_START = 'profile-start' | ||||
| export const TANGENTIAL_ARC_TO_SEGMENT = 'tangential-arc-to-segment' | ||||
| export const TANGENTIAL_ARC_TO_SEGMENT_BODY = 'tangential-arc-to-segment-body' | ||||
| export const MIN_SEGMENT_LENGTH = 60 // in pixels | ||||
|  | ||||
| // This singleton Class is responsible for all of the things the user sees and interacts with. | ||||
| // That mostly mean sketch elements. | ||||
| @ -111,8 +115,12 @@ export class SceneEntities { | ||||
|     this.engineCommandManager = engineCommandManager | ||||
|     this.scene = sceneInfra?.scene | ||||
|     sceneInfra?.camControls.subscribeToCamChange(this.onCamChange) | ||||
|     window.addEventListener('resize', this.onWindowResize) | ||||
|   } | ||||
|  | ||||
|   onWindowResize = () => { | ||||
|     this.onCamChange() | ||||
|   } | ||||
|   onCamChange = () => { | ||||
|     const orthoFactor = orthoScale(sceneInfra.camControls.camera) | ||||
|  | ||||
| @ -282,7 +290,6 @@ export class SceneEntities { | ||||
|     sketchGroup: SketchGroup | ||||
|     variableDeclarationName: string | ||||
|   }> { | ||||
|     sceneInfra.resetMouseListeners() | ||||
|     this.createIntersectionPlane() | ||||
|  | ||||
|     const { truncatedAst, programMemoryOverride, variableDeclarationName } = | ||||
| @ -295,7 +302,7 @@ export class SceneEntities { | ||||
|     }) | ||||
|     const sketchGroup = sketchGroupFromPathToNode({ | ||||
|       pathToNode: sketchPathToNode, | ||||
|       ast: kclManager.ast, | ||||
|       ast: maybeModdedAst, | ||||
|       programMemory, | ||||
|     }) | ||||
|     if (!Array.isArray(sketchGroup?.value)) | ||||
| @ -383,6 +390,7 @@ export class SceneEntities { | ||||
|           pathToNode: segPathToNode, | ||||
|           isDraftSegment, | ||||
|           scale: factor, | ||||
|           texture: sceneInfra.extraSegmentTexture, | ||||
|         }) | ||||
|       } else { | ||||
|         seg = straightSegment({ | ||||
| @ -393,6 +401,7 @@ export class SceneEntities { | ||||
|           isDraftSegment, | ||||
|           scale: factor, | ||||
|           callExpName, | ||||
|           texture: sceneInfra.extraSegmentTexture, | ||||
|         }) | ||||
|       } | ||||
|       seg.layers.set(SKETCH_LAYER) | ||||
| @ -435,6 +444,7 @@ export class SceneEntities { | ||||
|   ) => { | ||||
|     await kclManager.updateAst(modifiedAst, false) | ||||
|     await this.tearDownSketch({ removeAxis: false }) | ||||
|     sceneInfra.resetMouseListeners() | ||||
|     await this.setupSketch({ | ||||
|       sketchPathToNode, | ||||
|       forward, | ||||
| @ -442,7 +452,12 @@ export class SceneEntities { | ||||
|       position: origin, | ||||
|       maybeModdedAst: kclManager.ast, | ||||
|     }) | ||||
|     this.setupSketchIdleCallbacks(sketchPathToNode) | ||||
|     this.setupSketchIdleCallbacks({ | ||||
|       forward, | ||||
|       up, | ||||
|       position: origin, | ||||
|       pathToNode: sketchPathToNode, | ||||
|     }) | ||||
|   } | ||||
|   setUpDraftSegment = async ( | ||||
|     sketchPathToNode: PathToNode, | ||||
| @ -467,19 +482,20 @@ export class SceneEntities { | ||||
|  | ||||
|     const index = sg.value.length // because we've added a new segment that's not in the memory yet, no need for `-1` | ||||
|  | ||||
|     let modifiedAst = addNewSketchLn({ | ||||
|       node: kclManager.ast, | ||||
|     const mod = addNewSketchLn({ | ||||
|       node: _ast, | ||||
|       programMemory: kclManager.programMemory, | ||||
|       to: [lastSeg.to[0], lastSeg.to[1]], | ||||
|       from: [lastSeg.to[0], lastSeg.to[1]], | ||||
|       fnName: segmentName, | ||||
|       pathToNode: sketchPathToNode, | ||||
|     }).modifiedAst | ||||
|     modifiedAst = parse(recast(modifiedAst)) | ||||
|     }) | ||||
|     const modifiedAst = parse(recast(mod.modifiedAst)) | ||||
|  | ||||
|     const draftExpressionsIndices = { start: index, end: index } | ||||
|  | ||||
|     if (shouldTearDown) await this.tearDownSketch({ removeAxis: false }) | ||||
|     sceneInfra.resetMouseListeners() | ||||
|     const { truncatedAst, programMemoryOverride, sketchGroup } = | ||||
|       await this.setupSketch({ | ||||
|         sketchPathToNode, | ||||
| @ -549,10 +565,101 @@ export class SceneEntities { | ||||
|       ...mouseEnterLeaveCallbacks(), | ||||
|     }) | ||||
|   } | ||||
|   setupSketchIdleCallbacks = (pathToNode: PathToNode) => { | ||||
|   setupSketchIdleCallbacks = ({ | ||||
|     pathToNode, | ||||
|     up, | ||||
|     forward, | ||||
|     position, | ||||
|   }: { | ||||
|     pathToNode: PathToNode | ||||
|     forward: [number, number, number] | ||||
|     up: [number, number, number] | ||||
|     position?: [number, number, number] | ||||
|   }) => { | ||||
|     let addingNewSegmentStatus: 'nothing' | 'pending' | 'added' = 'nothing' | ||||
|     sceneInfra.setCallbacks({ | ||||
|       onDrag: ({ selected, intersectionPoint, mouseEvent, intersects }) => { | ||||
|       onDragEnd: async () => { | ||||
|         if (addingNewSegmentStatus !== 'nothing') { | ||||
|           await this.tearDownSketch({ removeAxis: false }) | ||||
|           this.setupSketch({ | ||||
|             sketchPathToNode: pathToNode, | ||||
|             maybeModdedAst: kclManager.ast, | ||||
|             up, | ||||
|             forward, | ||||
|             position, | ||||
|           }) | ||||
|           // setting up the callbacks again resets value in closures | ||||
|           this.setupSketchIdleCallbacks({ | ||||
|             pathToNode, | ||||
|             up, | ||||
|             forward, | ||||
|             position, | ||||
|           }) | ||||
|         } | ||||
|       }, | ||||
|       onDrag: async ({ | ||||
|         selected, | ||||
|         intersectionPoint, | ||||
|         mouseEvent, | ||||
|         intersects, | ||||
|       }) => { | ||||
|         if (mouseEvent.which !== 1) return | ||||
|  | ||||
|         const group = getParentGroup(selected, [EXTRA_SEGMENT_HANDLE]) | ||||
|         if (group?.name === EXTRA_SEGMENT_HANDLE) { | ||||
|           const segGroup = getParentGroup(selected) | ||||
|           const pathToNode: PathToNode = segGroup?.userData?.pathToNode | ||||
|           const pathToNodeIndex = pathToNode.findIndex( | ||||
|             (x) => x[1] === 'PipeExpression' | ||||
|           ) | ||||
|  | ||||
|           const sketchGroup = sketchGroupFromPathToNode({ | ||||
|             pathToNode, | ||||
|             ast: kclManager.ast, | ||||
|             programMemory: kclManager.programMemory, | ||||
|           }) | ||||
|  | ||||
|           const pipeIndex = pathToNode[pathToNodeIndex + 1][0] as number | ||||
|           if (addingNewSegmentStatus === 'nothing') { | ||||
|             const prevSegment = sketchGroup.value[pipeIndex - 2] | ||||
|             const mod = addNewSketchLn({ | ||||
|               node: kclManager.ast, | ||||
|               programMemory: kclManager.programMemory, | ||||
|               to: [intersectionPoint.twoD.x, intersectionPoint.twoD.y], | ||||
|               from: [prevSegment.from[0], prevSegment.from[1]], | ||||
|               // TODO assuming it's always a straight segments being added | ||||
|               // as this is easiest, and we'll need to add "tabbing" behavior | ||||
|               // to support other segment types | ||||
|               fnName: 'line', | ||||
|               pathToNode: pathToNode, | ||||
|               spliceBetween: true, | ||||
|             }) | ||||
|             addingNewSegmentStatus = 'pending' | ||||
|             await kclManager.executeAstMock(mod.modifiedAst, { | ||||
|               updates: 'code', | ||||
|             }) | ||||
|             await this.tearDownSketch({ removeAxis: false }) | ||||
|             this.setupSketch({ | ||||
|               sketchPathToNode: pathToNode, | ||||
|               maybeModdedAst: kclManager.ast, | ||||
|               up, | ||||
|               forward, | ||||
|               position, | ||||
|             }) | ||||
|             addingNewSegmentStatus = 'added' | ||||
|           } else if (addingNewSegmentStatus === 'added') { | ||||
|             const pathToNodeForNewSegment = pathToNode.slice(0, pathToNodeIndex) | ||||
|             pathToNodeForNewSegment.push([pipeIndex - 2, 'index']) | ||||
|             this.onDragSegment({ | ||||
|               sketchPathToNode: pathToNodeForNewSegment, | ||||
|               object: selected, | ||||
|               intersection2d: intersectionPoint.twoD, | ||||
|               intersects, | ||||
|             }) | ||||
|           } | ||||
|           return | ||||
|         } | ||||
|  | ||||
|         this.onDragSegment({ | ||||
|           object: selected, | ||||
|           intersection2d: intersectionPoint.twoD, | ||||
| @ -755,8 +862,7 @@ export class SceneEntities { | ||||
|     group.userData.to = to | ||||
|     group.userData.prevSegment = prevSegment | ||||
|     const arrowGroup = group.getObjectByName(ARROWHEAD) as Group | ||||
|  | ||||
|     arrowGroup.position.set(to[0], to[1], 0) | ||||
|     const extraSegmentGroup = group.getObjectByName(EXTRA_SEGMENT_HANDLE) | ||||
|  | ||||
|     const previousPoint = | ||||
|       prevSegment?.type === 'TangentialArcTo' | ||||
| @ -774,13 +880,40 @@ export class SceneEntities { | ||||
|       obtuse: true, | ||||
|     }) | ||||
|  | ||||
|     const arrowheadAngle = | ||||
|       arcInfo.endAngle + (Math.PI / 2) * (arcInfo.ccw ? 1 : -1) | ||||
|     arrowGroup.quaternion.setFromUnitVectors( | ||||
|       new Vector3(0, 1, 0), | ||||
|       new Vector3(Math.cos(arrowheadAngle), Math.sin(arrowheadAngle), 0) | ||||
|     ) | ||||
|     arrowGroup.scale.set(scale, scale, scale) | ||||
|     const pxLength = arcInfo.arcLength / scale | ||||
|     const shouldHide = pxLength < MIN_SEGMENT_LENGTH | ||||
|  | ||||
|     if (arrowGroup) { | ||||
|       arrowGroup.position.set(to[0], to[1], 0) | ||||
|  | ||||
|       const arrowheadAngle = | ||||
|         arcInfo.endAngle + (Math.PI / 2) * (arcInfo.ccw ? 1 : -1) | ||||
|       arrowGroup.quaternion.setFromUnitVectors( | ||||
|         new Vector3(0, 1, 0), | ||||
|         new Vector3(Math.cos(arrowheadAngle), Math.sin(arrowheadAngle), 0) | ||||
|       ) | ||||
|       arrowGroup.scale.set(scale, scale, scale) | ||||
|       arrowGroup.visible = !shouldHide | ||||
|     } | ||||
|  | ||||
|     if (extraSegmentGroup) { | ||||
|       const circumferenceInPx = (2 * Math.PI * arcInfo.radius) / scale | ||||
|       const extraSegmentAngleDelta = | ||||
|         (EXTRA_SEGMENT_OFFSET_PX / circumferenceInPx) * Math.PI * 2 | ||||
|       const extraSegmentAngle = | ||||
|         arcInfo.startAngle + (arcInfo.ccw ? 1 : -1) * extraSegmentAngleDelta | ||||
|       const extraSegmentOffset = new Vector2( | ||||
|         Math.cos(extraSegmentAngle) * arcInfo.radius, | ||||
|         Math.sin(extraSegmentAngle) * arcInfo.radius | ||||
|       ) | ||||
|       extraSegmentGroup.position.set( | ||||
|         arcInfo.center[0] + extraSegmentOffset.x, | ||||
|         arcInfo.center[1] + extraSegmentOffset.y, | ||||
|         0 | ||||
|       ) | ||||
|       extraSegmentGroup.scale.set(scale, scale, scale) | ||||
|       extraSegmentGroup.visible = !shouldHide | ||||
|     } | ||||
|  | ||||
|     const tangentialArcToSegmentBody = group.children.find( | ||||
|       (child) => child.userData.type === TANGENTIAL_ARC_TO_SEGMENT_BODY | ||||
| @ -827,10 +960,17 @@ export class SceneEntities { | ||||
|     group.userData.from = from | ||||
|     group.userData.to = to | ||||
|     const shape = new Shape() | ||||
|     shape.moveTo(0, -0.08 * scale) | ||||
|     shape.lineTo(0, 0.08 * scale) // The width of the line | ||||
|     shape.moveTo(0, -1.2 * scale) // The width of the line in px (2.4px in this case) | ||||
|     shape.lineTo(0, 1.2 * scale) | ||||
|     const arrowGroup = group.getObjectByName(ARROWHEAD) as Group | ||||
|  | ||||
|     const length = Math.sqrt( | ||||
|       Math.pow(to[0] - from[0], 2) + Math.pow(to[1] - from[1], 2) | ||||
|     ) | ||||
|  | ||||
|     const pxLength = length / scale | ||||
|     const shouldHide = pxLength < MIN_SEGMENT_LENGTH | ||||
|  | ||||
|     if (arrowGroup) { | ||||
|       arrowGroup.position.set(to[0], to[1], 0) | ||||
|  | ||||
| @ -842,6 +982,21 @@ export class SceneEntities { | ||||
|         .normalize() | ||||
|       arrowGroup.quaternion.setFromUnitVectors(new Vector3(0, 1, 0), dir) | ||||
|       arrowGroup.scale.set(scale, scale, scale) | ||||
|       arrowGroup.visible = !shouldHide | ||||
|     } | ||||
|  | ||||
|     const extraSegmentGroup = group.getObjectByName(EXTRA_SEGMENT_HANDLE) | ||||
|     if (extraSegmentGroup) { | ||||
|       const offsetFromBase = new Vector2(to[0] - from[0], to[1] - from[1]) | ||||
|         .normalize() | ||||
|         .multiplyScalar(EXTRA_SEGMENT_OFFSET_PX * scale) | ||||
|       extraSegmentGroup.position.set( | ||||
|         from[0] + offsetFromBase.x, | ||||
|         from[1] + offsetFromBase.y, | ||||
|         0 | ||||
|       ) | ||||
|       extraSegmentGroup.scale.set(scale, scale, scale) | ||||
|       extraSegmentGroup.visible = !shouldHide | ||||
|     } | ||||
|  | ||||
|     const straightSegmentBody = group.children.find( | ||||
| @ -1160,7 +1315,7 @@ function colorSegment(object: any, color: number) { | ||||
|   ]) | ||||
|   if (straightSegmentBody) { | ||||
|     straightSegmentBody.traverse((child) => { | ||||
|       if (child instanceof Mesh) { | ||||
|       if (child instanceof Mesh && !child.userData.ignoreColorChange) { | ||||
|         child.material.color.set(color) | ||||
|       } | ||||
|     }) | ||||
| @ -1264,7 +1419,7 @@ function massageFormats(a: any): Vector3 { | ||||
|  | ||||
| function mouseEnterLeaveCallbacks() { | ||||
|   return { | ||||
|     onMouseEnter: ({ selected }: OnMouseEnterLeaveArgs) => { | ||||
|     onMouseEnter: ({ selected, dragSelected }: OnMouseEnterLeaveArgs) => { | ||||
|       if ([X_AXIS, Y_AXIS].includes(selected?.userData?.type)) { | ||||
|         const obj = selected as Mesh | ||||
|         const mat = obj.material as MeshBasicMaterial | ||||
| @ -1286,6 +1441,14 @@ function mouseEnterLeaveCallbacks() { | ||||
|         sceneInfra.highlightCallback([node.start, node.end]) | ||||
|         const yellow = 0xffff00 | ||||
|         colorSegment(selected, yellow) | ||||
|         const extraSegmentGroup = parent.getObjectByName(EXTRA_SEGMENT_HANDLE) | ||||
|         if (extraSegmentGroup) { | ||||
|           extraSegmentGroup.traverse((child) => { | ||||
|             if (child instanceof Points || child instanceof Mesh) { | ||||
|               child.material.opacity = dragSelected ? 0 : 1 | ||||
|             } | ||||
|           }) | ||||
|         } | ||||
|         return | ||||
|       } | ||||
|       sceneInfra.highlightCallback([0, 0]) | ||||
| @ -1302,6 +1465,14 @@ function mouseEnterLeaveCallbacks() { | ||||
|         selected, | ||||
|         isSelected ? 0x0000ff : parent?.userData?.baseColor || 0xffffff | ||||
|       ) | ||||
|       const extraSegmentGroup = parent?.getObjectByName(EXTRA_SEGMENT_HANDLE) | ||||
|       if (extraSegmentGroup) { | ||||
|         extraSegmentGroup.traverse((child) => { | ||||
|           if (child instanceof Points || child instanceof Mesh) { | ||||
|             child.material.opacity = 0 | ||||
|           } | ||||
|         }) | ||||
|       } | ||||
|       if ([X_AXIS, Y_AXIS].includes(selected?.userData?.type)) { | ||||
|         const obj = selected as Mesh | ||||
|         const mat = obj.material as MeshBasicMaterial | ||||
|  | ||||
| @ -18,6 +18,8 @@ import { | ||||
|   Intersection, | ||||
|   Object3D, | ||||
|   Object3DEventMap, | ||||
|   TextureLoader, | ||||
|   Texture, | ||||
| } from 'three' | ||||
| import { compareVec2Epsilon2 } from 'lang/std/sketch' | ||||
| import { useModelingContext } from 'hooks/useModelingContext' | ||||
| @ -54,6 +56,7 @@ export const ARROWHEAD = 'arrowhead' | ||||
|  | ||||
| export interface OnMouseEnterLeaveArgs { | ||||
|   selected: Object3D<Object3DEventMap> | ||||
|   dragSelected?: Object3D<Object3DEventMap> | ||||
|   mouseEvent: MouseEvent | ||||
| } | ||||
|  | ||||
| @ -98,18 +101,25 @@ export class SceneInfra { | ||||
|   isFovAnimationInProgress = false | ||||
|   _baseUnit: BaseUnit = 'mm' | ||||
|   _baseUnitMultiplier = 1 | ||||
|   extraSegmentTexture: Texture | ||||
|   onDragStartCallback: (arg: OnDragCallbackArgs) => void = () => {} | ||||
|   onDragEndCallback: (arg: OnDragCallbackArgs) => void = () => {} | ||||
|   onDragCallback: (arg: OnDragCallbackArgs) => void = () => {} | ||||
|   onMoveCallback: (arg: OnMoveCallbackArgs) => void = () => {} | ||||
|   onClickCallback: (arg: OnClickCallbackArgs) => void = () => {} | ||||
|   onMouseEnter: (arg: OnMouseEnterLeaveArgs) => void = () => {} | ||||
|   onMouseLeave: (arg: OnMouseEnterLeaveArgs) => void = () => {} | ||||
|   setCallbacks = (callbacks: { | ||||
|     onDragStart?: (arg: OnDragCallbackArgs) => void | ||||
|     onDragEnd?: (arg: OnDragCallbackArgs) => void | ||||
|     onDrag?: (arg: OnDragCallbackArgs) => void | ||||
|     onMove?: (arg: OnMoveCallbackArgs) => void | ||||
|     onClick?: (arg: OnClickCallbackArgs) => void | ||||
|     onMouseEnter?: (arg: OnMouseEnterLeaveArgs) => void | ||||
|     onMouseLeave?: (arg: OnMouseEnterLeaveArgs) => void | ||||
|   }) => { | ||||
|     this.onDragStartCallback = callbacks.onDragStart || this.onDragStartCallback | ||||
|     this.onDragEndCallback = callbacks.onDragEnd || this.onDragEndCallback | ||||
|     this.onDragCallback = callbacks.onDrag || this.onDragCallback | ||||
|     this.onMoveCallback = callbacks.onMove || this.onMoveCallback | ||||
|     this.onClickCallback = callbacks.onClick || this.onClickCallback | ||||
| @ -128,6 +138,8 @@ export class SceneInfra { | ||||
|   } | ||||
|   resetMouseListeners = () => { | ||||
|     this.setCallbacks({ | ||||
|       onDragStart: () => {}, | ||||
|       onDragEnd: () => {}, | ||||
|       onDrag: () => {}, | ||||
|       onMove: () => {}, | ||||
|       onClick: () => {}, | ||||
| @ -212,6 +224,13 @@ export class SceneInfra { | ||||
|     const light = new AmbientLight(0x505050) // soft white light | ||||
|     this.scene.add(light) | ||||
|  | ||||
|     const textureLoader = new TextureLoader() | ||||
|     this.extraSegmentTexture = textureLoader.load( | ||||
|       '/clientSideSceneAssets/extra-segment-texture.png' | ||||
|     ) | ||||
|     this.extraSegmentTexture.anisotropy = | ||||
|       this.renderer?.capabilities?.getMaxAnisotropy?.() | ||||
|  | ||||
|     SceneInfra.instance = this | ||||
|   } | ||||
|  | ||||
| @ -360,6 +379,7 @@ export class SceneInfra { | ||||
|         this.hoveredObject = firstIntersectObject | ||||
|         this.onMouseEnter({ | ||||
|           selected: this.hoveredObject, | ||||
|           dragSelected: this.selected?.object, | ||||
|           mouseEvent: mouseEvent, | ||||
|         }) | ||||
|       } | ||||
| @ -367,6 +387,7 @@ export class SceneInfra { | ||||
|       if (this.hoveredObject) { | ||||
|         this.onMouseLeave({ | ||||
|           selected: this.hoveredObject, | ||||
|           dragSelected: this.selected?.object, | ||||
|           mouseEvent: mouseEvent, | ||||
|         }) | ||||
|         this.hoveredObject = null | ||||
| @ -455,8 +476,16 @@ export class SceneInfra { | ||||
|  | ||||
|     if (this.selected) { | ||||
|       if (this.selected.hasBeenDragged) { | ||||
|         // this is where we could fire a onDragEnd event | ||||
|         // console.log('onDragEnd', this.selected) | ||||
|         // TODO do the types properly here | ||||
|         this.onDragEndCallback({ | ||||
|           intersectionPoint: { | ||||
|             twoD: planeIntersectPoint?.twoD as any, | ||||
|             threeD: planeIntersectPoint?.threeD as any, | ||||
|           }, | ||||
|           intersects, | ||||
|           mouseEvent, | ||||
|           selected: this.selected as any, | ||||
|         }) | ||||
|       } else if (planeIntersectPoint?.twoD && planeIntersectPoint?.threeD) { | ||||
|         // fire onClick event as there was no drags | ||||
|         this.onClickCallback({ | ||||
|  | ||||
| @ -12,14 +12,20 @@ import { | ||||
|   Mesh, | ||||
|   MeshBasicMaterial, | ||||
|   NormalBufferAttributes, | ||||
|   Points, | ||||
|   PointsMaterial, | ||||
|   Shape, | ||||
|   SphereGeometry, | ||||
|   Texture, | ||||
|   Vector2, | ||||
|   Vector3, | ||||
| } from 'three' | ||||
| import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js' | ||||
| import { PathToNode, SketchGroup, getTangentialArcToInfo } from 'lang/wasm' | ||||
| import { | ||||
|   EXTRA_SEGMENT_HANDLE, | ||||
|   EXTRA_SEGMENT_OFFSET_PX, | ||||
|   MIN_SEGMENT_LENGTH, | ||||
|   PROFILE_START, | ||||
|   STRAIGHT_SEGMENT, | ||||
|   STRAIGHT_SEGMENT_BODY, | ||||
| @ -44,7 +50,7 @@ export function profileStart({ | ||||
| }) { | ||||
|   const group = new Group() | ||||
|  | ||||
|   const geometry = new BoxGeometry(0.8, 0.8, 0.8) | ||||
|   const geometry = new BoxGeometry(12, 12, 12) // in pixels scaled later | ||||
|   const body = new MeshBasicMaterial({ color: 0xffffff }) | ||||
|   const mesh = new Mesh(geometry, body) | ||||
|  | ||||
| @ -71,6 +77,7 @@ export function straightSegment({ | ||||
|   isDraftSegment, | ||||
|   scale = 1, | ||||
|   callExpName, | ||||
|   texture, | ||||
| }: { | ||||
|   from: Coords2d | ||||
|   to: Coords2d | ||||
| @ -79,12 +86,13 @@ export function straightSegment({ | ||||
|   isDraftSegment?: boolean | ||||
|   scale?: number | ||||
|   callExpName: string | ||||
|   texture: Texture | ||||
| }): Group { | ||||
|   const group = new Group() | ||||
|  | ||||
|   const shape = new Shape() | ||||
|   shape.moveTo(0, -0.08 * scale) | ||||
|   shape.lineTo(0, 0.08 * scale) // The width of the line | ||||
|   shape.moveTo(0, -1.2 * scale) | ||||
|   shape.lineTo(0, 1.2 * scale) | ||||
|  | ||||
|   let geometry | ||||
|   if (isDraftSegment) { | ||||
| @ -122,24 +130,44 @@ export function straightSegment({ | ||||
|   } | ||||
|   group.name = STRAIGHT_SEGMENT | ||||
|  | ||||
|   const length = Math.sqrt( | ||||
|     Math.pow(to[0] - from[0], 2) + Math.pow(to[1] - from[1], 2) | ||||
|   ) | ||||
|   const arrowGroup = createArrowhead(scale) | ||||
|   arrowGroup.position.set(to[0], to[1], 0) | ||||
|   const dir = new Vector3() | ||||
|     .subVectors(new Vector3(to[0], to[1], 0), new Vector3(from[0], from[1], 0)) | ||||
|     .normalize() | ||||
|   arrowGroup.quaternion.setFromUnitVectors(new Vector3(0, 1, 0), dir) | ||||
|   const pxLength = length / scale | ||||
|   const shouldHide = pxLength < MIN_SEGMENT_LENGTH | ||||
|   arrowGroup.visible = !shouldHide | ||||
|  | ||||
|   group.add(mesh) | ||||
|   if (callExpName !== 'close') group.add(arrowGroup) | ||||
|  | ||||
|   const extraSegmentGroup = createExtraSegmentHandle(scale, texture) | ||||
|   const offsetFromBase = new Vector2(to[0] - from[0], to[1] - from[1]) | ||||
|     .normalize() | ||||
|     .multiplyScalar(EXTRA_SEGMENT_OFFSET_PX * scale) | ||||
|   extraSegmentGroup.position.set( | ||||
|     from[0] + offsetFromBase.x, | ||||
|     from[1] + offsetFromBase.y, | ||||
|     0 | ||||
|   ) | ||||
|   extraSegmentGroup.visible = !shouldHide | ||||
|   group.add(extraSegmentGroup) | ||||
|  | ||||
|   return group | ||||
| } | ||||
|  | ||||
| function createArrowhead(scale = 1): Group { | ||||
|   const arrowMaterial = new MeshBasicMaterial({ color: 0xffffff }) | ||||
|   const arrowheadMesh = new Mesh(new ConeGeometry(0.31, 1.5, 12), arrowMaterial) | ||||
|   arrowheadMesh.position.set(0, -0.6, 0) | ||||
|   const sphereMesh = new Mesh(new SphereGeometry(0.27, 12, 12), arrowMaterial) | ||||
|   // specify the size of the geometry in pixels (i.e. cone height = 20px, cone radius = 4.5px) | ||||
|   // we'll scale the group to the correct size later to match these sizes in screen space | ||||
|   const arrowheadMesh = new Mesh(new ConeGeometry(4.5, 20, 12), arrowMaterial) | ||||
|   arrowheadMesh.position.set(0, -9, 0) | ||||
|   const sphereMesh = new Mesh(new SphereGeometry(4, 12, 12), arrowMaterial) | ||||
|  | ||||
|   const arrowGroup = new Group() | ||||
|   arrowGroup.userData.type = ARROWHEAD | ||||
| @ -150,6 +178,36 @@ function createArrowhead(scale = 1): Group { | ||||
|   return arrowGroup | ||||
| } | ||||
|  | ||||
| function createExtraSegmentHandle(scale: number, texture: Texture): Group { | ||||
|   const particleMaterial = new PointsMaterial({ | ||||
|     size: 12, // in pixels | ||||
|     map: texture, | ||||
|     transparent: true, | ||||
|     opacity: 0, | ||||
|     depthTest: false, | ||||
|   }) | ||||
|   const mat = new MeshBasicMaterial({ | ||||
|     transparent: true, | ||||
|     color: 0xffffff, | ||||
|     opacity: 0, | ||||
|   }) | ||||
|   const particleGeometry = new BufferGeometry().setFromPoints([ | ||||
|     new Vector3(0, 0, 0), | ||||
|   ]) | ||||
|   const sphereMesh = new Mesh(new SphereGeometry(6, 12, 12), mat) // sphere radius in pixels | ||||
|   const particle = new Points(particleGeometry, particleMaterial) | ||||
|   particle.userData.ignoreColorChange = true | ||||
|   particle.userData.type = EXTRA_SEGMENT_HANDLE | ||||
|  | ||||
|   const extraSegmentGroup = new Group() | ||||
|   extraSegmentGroup.userData.type = EXTRA_SEGMENT_HANDLE | ||||
|   extraSegmentGroup.name = EXTRA_SEGMENT_HANDLE | ||||
|   extraSegmentGroup.add(sphereMesh) | ||||
|   extraSegmentGroup.add(particle) | ||||
|   extraSegmentGroup.scale.set(scale, scale, scale) | ||||
|   return extraSegmentGroup | ||||
| } | ||||
|  | ||||
| export function tangentialArcToSegment({ | ||||
|   prevSegment, | ||||
|   from, | ||||
| @ -158,6 +216,7 @@ export function tangentialArcToSegment({ | ||||
|   pathToNode, | ||||
|   isDraftSegment, | ||||
|   scale = 1, | ||||
|   texture, | ||||
| }: { | ||||
|   prevSegment: SketchGroup['value'][number] | ||||
|   from: Coords2d | ||||
| @ -166,6 +225,7 @@ export function tangentialArcToSegment({ | ||||
|   pathToNode: PathToNode | ||||
|   isDraftSegment?: boolean | ||||
|   scale?: number | ||||
|   texture: Texture | ||||
| }): Group { | ||||
|   const group = new Group() | ||||
|  | ||||
| @ -178,12 +238,13 @@ export function tangentialArcToSegment({ | ||||
|         ) | ||||
|       : prevSegment.from | ||||
|  | ||||
|   const { center, radius, startAngle, endAngle, ccw } = getTangentialArcToInfo({ | ||||
|     arcStartPoint: from, | ||||
|     arcEndPoint: to, | ||||
|     tanPreviousPoint: previousPoint, | ||||
|     obtuse: true, | ||||
|   }) | ||||
|   const { center, radius, startAngle, endAngle, ccw, arcLength } = | ||||
|     getTangentialArcToInfo({ | ||||
|       arcStartPoint: from, | ||||
|       arcEndPoint: to, | ||||
|       tanPreviousPoint: previousPoint, | ||||
|       obtuse: true, | ||||
|     }) | ||||
|  | ||||
|   const geometry = createArcGeometry({ | ||||
|     center, | ||||
| @ -219,8 +280,28 @@ export function tangentialArcToSegment({ | ||||
|     new Vector3(0, 1, 0), | ||||
|     new Vector3(Math.cos(arrowheadAngle), Math.sin(arrowheadAngle), 0) | ||||
|   ) | ||||
|   const pxLength = arcLength / scale | ||||
|   const shouldHide = pxLength < MIN_SEGMENT_LENGTH | ||||
|   arrowGroup.visible = !shouldHide | ||||
|  | ||||
|   group.add(mesh, arrowGroup) | ||||
|   const extraSegmentGroup = createExtraSegmentHandle(scale, texture) | ||||
|   const circumferenceInPx = (2 * Math.PI * radius) / scale | ||||
|   const extraSegmentAngleDelta = | ||||
|     (EXTRA_SEGMENT_OFFSET_PX / circumferenceInPx) * Math.PI * 2 | ||||
|   const extraSegmentAngle = startAngle + (ccw ? 1 : -1) * extraSegmentAngleDelta | ||||
|   const extraSegmentOffset = new Vector2( | ||||
|     Math.cos(extraSegmentAngle) * radius, | ||||
|     Math.sin(extraSegmentAngle) * radius | ||||
|   ) | ||||
|   extraSegmentGroup.position.set( | ||||
|     center[0] + extraSegmentOffset.x, | ||||
|     center[1] + extraSegmentOffset.y, | ||||
|     0 | ||||
|   ) | ||||
|  | ||||
|   extraSegmentGroup.visible = !shouldHide | ||||
|  | ||||
|   group.add(mesh, arrowGroup, extraSegmentGroup) | ||||
|  | ||||
|   return group | ||||
| } | ||||
| @ -242,8 +323,8 @@ export function createArcGeometry({ | ||||
|   isDashed?: boolean | ||||
|   scale?: number | ||||
| }): BufferGeometry { | ||||
|   const dashSize = 1.2 * scale | ||||
|   const gapSize = 1.2 * scale | ||||
|   const dashSizePx = 18 * scale | ||||
|   const gapSizePx = 18 * scale | ||||
|   const arcStart = new EllipseCurve( | ||||
|     center[0], | ||||
|     center[1], | ||||
| @ -265,8 +346,8 @@ export function createArcGeometry({ | ||||
|     0 | ||||
|   ) | ||||
|   const shape = new Shape() | ||||
|   shape.moveTo(0, -0.08 * scale) | ||||
|   shape.lineTo(0, 0.08 * scale) // The width of the line | ||||
|   shape.moveTo(0, -1.2 * scale) | ||||
|   shape.lineTo(0, 1.2 * scale) // The width of the line | ||||
|  | ||||
|   if (!isDashed) { | ||||
|     const points = arcStart.getPoints(50) | ||||
| @ -281,7 +362,7 @@ export function createArcGeometry({ | ||||
|   } | ||||
|  | ||||
|   const length = arcStart.getLength() | ||||
|   const totalDashes = length / (dashSize + gapSize) // rounding makes the dashes jittery since the new dash is suddenly appears instead of growing into place | ||||
|   const totalDashes = length / (dashSizePx + gapSizePx) // rounding makes the dashes jittery since the new dash is suddenly appears instead of growing into place | ||||
|   const dashesAtEachEnd = Math.min(100, totalDashes / 2) // Assuming we want 50 dashes total, 25 at each end | ||||
|  | ||||
|   const dashGeometries = [] | ||||
| @ -289,8 +370,8 @@ export function createArcGeometry({ | ||||
|   // Function to create a dash at a specific t value (0 to 1 along the curve) | ||||
|   const createDashAt = (t: number, curve: EllipseCurve) => { | ||||
|     const startVec = curve.getPoint(t) | ||||
|     const endVec = curve.getPoint(Math.min(0.5, t + dashSize / length)) | ||||
|     const midVec = curve.getPoint(Math.min(0.5, t + dashSize / length / 2)) | ||||
|     const endVec = curve.getPoint(Math.min(0.5, t + dashSizePx / length)) | ||||
|     const midVec = curve.getPoint(Math.min(0.5, t + dashSizePx / length / 2)) | ||||
|     const dashCurve = new CurvePath<Vector3>() | ||||
|     dashCurve.add( | ||||
|       new CatmullRomCurve3([ | ||||
| @ -314,7 +395,8 @@ export function createArcGeometry({ | ||||
|   } | ||||
|  | ||||
|   // fill in the remaining arc | ||||
|   const remainingArcLength = length - dashesAtEachEnd * 2 * (dashSize + gapSize) | ||||
|   const remainingArcLength = | ||||
|     length - dashesAtEachEnd * 2 * (dashSizePx + gapSizePx) | ||||
|   if (remainingArcLength > 0) { | ||||
|     const remainingArcStartT = dashesAtEachEnd / totalDashes | ||||
|     const remainingArcEndT = 1 - remainingArcStartT | ||||
| @ -359,8 +441,8 @@ export function dashedStraight( | ||||
|   shape: Shape, | ||||
|   scale = 1 | ||||
| ): BufferGeometry<NormalBufferAttributes> { | ||||
|   const dashSize = 1.2 * scale | ||||
|   const gapSize = 1.2 * scale // todo: gabSize is not respected | ||||
|   const dashSize = 18 * scale | ||||
|   const gapSize = 18 * scale // todo: gabSize is not respected | ||||
|   const dashLine = new LineCurve3( | ||||
|     new Vector3(from[0], from[1], 0), | ||||
|     new Vector3(to[0], to[1], 0) | ||||
|  | ||||
| @ -162,6 +162,7 @@ export const line: SketchLineHelper = { | ||||
|     replaceExisting, | ||||
|     referencedSegment, | ||||
|     createCallback, | ||||
|     spliceBetween, | ||||
|   }) => { | ||||
|     const _node = { ...node } | ||||
|     const { node: pipe } = getNodeFromPath<PipeExpression | CallExpression>( | ||||
| @ -178,6 +179,30 @@ export const line: SketchLineHelper = { | ||||
|     const newXVal = createLiteral(roundOff(to[0] - from[0], 2)) | ||||
|     const newYVal = createLiteral(roundOff(to[1] - from[1], 2)) | ||||
|  | ||||
|     if (spliceBetween && !createCallback && pipe.type === 'PipeExpression') { | ||||
|       const callExp = createCallExpression('line', [ | ||||
|         createArrayExpression([newXVal, newYVal]), | ||||
|         createPipeSubstitution(), | ||||
|       ]) | ||||
|       const pathToNodeIndex = pathToNode.findIndex( | ||||
|         (x) => x[1] === 'PipeExpression' | ||||
|       ) | ||||
|       const pipeIndex = pathToNode[pathToNodeIndex + 1][0] | ||||
|       if (typeof pipeIndex === 'undefined' || typeof pipeIndex === 'string') { | ||||
|         throw new Error('pipeIndex is undefined') | ||||
|         // return | ||||
|       } | ||||
|       pipe.body = [ | ||||
|         ...pipe.body.slice(0, pipeIndex), | ||||
|         callExp, | ||||
|         ...pipe.body.slice(pipeIndex), | ||||
|       ] | ||||
|       return { | ||||
|         modifiedAst: _node, | ||||
|         pathToNode, | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     if (replaceExisting && createCallback && pipe.type !== 'CallExpression') { | ||||
|       const { index: callIndex } = splitPathAtPipeExpression(pathToNode) | ||||
|       const { callExp, valueUsedInTransform } = createCallback( | ||||
| @ -1023,15 +1048,6 @@ export function changeSketchArguments( | ||||
|   throw new Error(`not a sketch line helper: ${callExpression?.callee?.name}`) | ||||
| } | ||||
|  | ||||
| interface CreateLineFnCallArgs { | ||||
|   node: Program | ||||
|   programMemory: ProgramMemory | ||||
|   to: [number, number] | ||||
|   from: [number, number] | ||||
|   fnName: ToolTip | ||||
|   pathToNode: PathToNode | ||||
| } | ||||
|  | ||||
| export function compareVec2Epsilon( | ||||
|   vec1: [number, number], | ||||
|   vec2: [number, number], | ||||
| @ -1056,6 +1072,16 @@ export function compareVec2Epsilon2( | ||||
|   return distance < compareEpsilon | ||||
| } | ||||
|  | ||||
| interface CreateLineFnCallArgs { | ||||
|   node: Program | ||||
|   programMemory: ProgramMemory | ||||
|   to: [number, number] | ||||
|   from: [number, number] | ||||
|   fnName: ToolTip | ||||
|   pathToNode: PathToNode | ||||
|   spliceBetween?: boolean | ||||
| } | ||||
|  | ||||
| export function addNewSketchLn({ | ||||
|   node: _node, | ||||
|   programMemory: previousProgramMemory, | ||||
| @ -1063,6 +1089,7 @@ export function addNewSketchLn({ | ||||
|   fnName, | ||||
|   pathToNode, | ||||
|   from, | ||||
|   spliceBetween = false, | ||||
| }: CreateLineFnCallArgs): { | ||||
|   modifiedAst: Program | ||||
|   pathToNode: PathToNode | ||||
| @ -1083,6 +1110,7 @@ export function addNewSketchLn({ | ||||
|     to, | ||||
|     from, | ||||
|     replaceExisting: false, | ||||
|     spliceBetween, | ||||
|   }) | ||||
| } | ||||
|  | ||||
|  | ||||
| @ -35,6 +35,8 @@ interface addCall extends ModifyAstBase { | ||||
|   referencedSegment?: Path | ||||
|   replaceExisting?: boolean | ||||
|   createCallback?: TransformCallback // TODO: #29 probably should not be optional | ||||
|   /// defaults to false, normal behavior  is to add a new callExpression to the end of the pipeExpression | ||||
|   spliceBetween?: boolean | ||||
| } | ||||
|  | ||||
| interface updateArgs extends ModifyAstBase { | ||||
|  | ||||
| @ -251,6 +251,7 @@ export function getTangentialArcToInfo({ | ||||
|   startAngle: number | ||||
|   endAngle: number | ||||
|   ccw: boolean | ||||
|   arcLength: number | ||||
| } { | ||||
|   const result = get_tangential_arc_to_info( | ||||
|     arcStartPoint[0], | ||||
| @ -268,6 +269,7 @@ export function getTangentialArcToInfo({ | ||||
|     startAngle: result.start_angle, | ||||
|     endAngle: result.end_angle, | ||||
|     ccw: result.ccw > 0, | ||||
|     arcLength: result.arc_length, | ||||
|   } | ||||
| } | ||||
|  | ||||
|  | ||||
| @ -133,7 +133,7 @@ export type MoveDesc = { line: number; snippet: string } | ||||
|  | ||||
| export const modelingMachine = createMachine( | ||||
|   { | ||||
|     /** @xstate-layout N4IgpgJg5mDOIC5QFkD2EwBsCWA7KAxAMICGuAxlgNoAMAuoqAA6qzYAu2qujIAHogC0AdgCsAZgB04gEyjhADnEA2GgoUAWJQBoQAT0QBGGuICckmoZkbTM42YWKAvk91oMOfAQDKYdgAJYLDByTm5aBiQQFjYwniiBBFtpGhlxDRphGg1ZURlhXQMEcRLDSVF5UwV84XEK8Rc3dCw8KElsCEwwHz9A4NCuXAjeGI5B3kTBLWFJDWV5hVSZZTrlBUKjS1FZiUrDeWVDasaQdxb8ds7ugFFcdjAAJ0CAaz9yAAthqNG4iaFlmZWZSmZRaKyGQzKYQaDYIGSmDSzdKyDSGcQ0KrAk5nTxtDpdAi3e5PWCvdgfKiGSLMVhjbh-BCCaw0WaQrZqdGLAr6RAZGSSWzCZSiIEKNFi5TY5q4y4E658dgPACuGC+NNi4wSQgqM3hNFEwNEGlEYthENELJkCjWplMFpsGlRUo8rVlNz4LAe7DV0Vpvy1jKtCkkVuZMjsDpNsOEwjKUIkcxNqWhomd5za3jJH2IZEomEzb0+9BGfs1oESEOtkmM0K0ahMplkZqOZUrYrRlgUJrTMoL5Pekj7HwAklcegEgl0BuFi99S-SA4ZoWUdWsTSLlsozZlzNkZPrG0a5F2e66hwPz6OCcgSK8+lAALZgO7+ABuj045BImB9PzL-CMeFW2qTQhU0ZMzVRKR0RoVJ6nyJQNFPC5z0HLN3ivbobzvIJH2fAJ3lQB5sAAL24dhv1-ed4nLQDoUkVRrX1CF8ihGQzXECFymNLJIU4-U8mQjN0LQwtMOIbhYEVEg8H8QjiLIu5v38CBsCk3MwCojUF1ohArCNCwNDSDFENMI51h5OEzGUCwKgtbJRDMtEhNE-tXJHMciEk6TZPfL1sC-TAVLUiiKE02d1TpGiAL05ZgztK0RVBYxNHYyzw1qcprCODQY2EZYjRc1DL087yHhk3B-AAQQAIW8fwAA0tKihkrC7JExEMGxOPhNJYXhAy7ShRY4NMWoGlcU5pTPESSoJLzcCk8rZNq+qAE1mv9XS2u2aw7GUOR9mFA7YU5YN8rstlHEdIrZvQ8SFqWir-DIKAuk2-8K2RaQlBA+QxWEUx+otXV1zyg1AbG27C3cjDSsWnzKq6fB2CLalfW06KvvEGYVANDE0TSfZ+sOc7gVRQHHSUKFobcubukexH-CYR4WdwVTyCVTASCeVT1LCj6dJiqwrVmW0eoyI5VC3dKxURWMqjG-LHVy1NJpxGaYfpiSEeWyr5NI8jv0wPQXpwKAhgijGWsXZZ+S7TQu3SI0aBWM0kpDO1QPysQVgmpoXRQu6xPhp7fI-ALjdN79sAtwWsbo8wcitQGkrMEUzQO7YTTGzFqiqWpaY+WGHrK57YFwEgmH8dhUEa+PWs4qRlnERYFDG6ozH6vcQzkeZ5n1RxwyLi97tDpmK6rmu642q2-yFr60QsIVOJjHHjBhdKupmI0h8cpc5hyEeS-HvX-DAABHJVlORqBUYb23G3KbfAayPIJG5Iol3RJETFg-VjqmGPtrRmZ8mA82NtQOe1FG75AsOGFY+xGwU03l-RsMwTCcUOLaTIag1YB3TLDE+80y6yQeGAB8qB3z+HIKQu4sAH7bWsMGQ41gDRWlRCacQZolBlEsMCfcOROQHWAWPAkAAlMAggwB8BCEqe4jDhZ1CkGiVQ+kdQGlhCCGyjk14misGZbIoiQ5yivtgauAAZPAYBp6oB-NAzGrV246JKKCW0CZ1CwiUNnTIe05CJghMY-s4lrhmOrqFGAdxsDKR5uQaeijEj7WDK3A0WD-rKObJYSQYFVDpMdKCY+VUADuMkCJEUNkpIKfNQqUH8HgAAZqgAgEBuBgHaLgV8qBXiSBgOwQQBtFIUUwIIBpqAEmIGMoifJGQxpmCVtw9KqJzBdlDFg2wENCklI4HJcpgzlLVI0nU3AjSCCPAeERSQTBubsEaQ8B8PS-D9N2UbYZozxlWX1NWLqqI0TGitGNfqrd+TZFdrBOorFDCbNKW+COgVgr81qaM5prT2mdO6b0wQflPzfhGccsZDiba6SJvyNEih7BQkMIKfqVopCOHfnYUo+UoXbKxZHKpIVDlIrORcq5JAblEXuRi1lgVcWNPecSyQix5D7zEJoalRlqzHVSK3ECShmUBFWo1I5JyWm4DaXgNFbSMUkAAEawEEHwUV+L0bzwTlZSE2SDqwXDLsCyRR4QHWrBkVEoJZAHULuraaQcYbFOhZqhq2qmncoeJc65tzBWPNNeay1byCVbRikTGYQj1B1FMPqLQ-UzI2TqNaCQG9oRAMDYHYSIatkarqv4NakbkV6tRV0o1iazWCD0Fa8VWDpAaMcvCHGxpTquyzYrC0XD83quqg2ptXKHjnJjby-ldyHl9KTd23tabPoTNXvAle+V5jpEhKdSEiJXbaJdaSmQs7XpdGbbq-VHT20bsEA+qRqabUwIDJmlIEJwWOTtIYYGBoBTogtOaE0UJhD3vwI+xdy7Y18vje+z9O6f2OL-Zxc62Q2pWDUGZT+EyRSAjRPIOC+oASztvqjJ9KKDVvoxXR94mGSzYaJZxbYyczAoP3PkEm7VXaMpUEuKwtHnx33eM26NKG10Jr6ax9jc5OMZv7cYEw+i1i5TSu60E-J5jMIRGIGMBpZ0sweGzDmXMebwpqTYpFz623oseZZ6zAVbMPEEAcsKKnIrpsSXbAUXDrAHTinIfq1RtjrzmMKdxch-ZTWrUQ0N2z3PPhs9zXmHKwqyaXTyuNAr30ZfZp57LPncuUH89bQLEzgtexROFtYkX0p5WkLGOLwH35JY1sGtyVi9W2MwOOPoU44jiv3EnQjRxMhHDyAs91Y1qxewxMwxid6q2ENQoNmxtc7GEjCTXV6+FolBVifE3dC8JkxkRPsMwsF1CKxlkUJWSIcaOCyCCY+u3huSGHLgDgBBJsqElSk2bCI5BA0smsMoaR24PZxoDNIP3rF-YB0DykWHCUZvyPyC0VoL2EeBNGQGswVgSHWcCWC+DkvbZEr9-bmBJC4AFd+Ubk4QgTau3a6wmVZDqClsyA0L3ECghmMuVWcsoKQq272BnaOmeSAAHJ1wAAqoDwOwWABAqoQAgIECiXpmaa7uOK+YiI80GkckZRYoJYTzCTmYV+mQc0KFR0NpXqv-Aa61zr0gYV7HY7q3pTQu5ZBAoxFoE0brEC8TFtg0oVoVjHwx+wYHPPWr8NmF2LIKhqNR7NGkKQcwhTLFBcsJ0cvNZuTT8DqkHGccVmYpKvNea5iyFgl1IvZlslOzmECbqkpq-9eLgAFRO1EmJDw4m1w5-0bnwe91wgOuYYESg7TolsPCIvqJJUXv3JYY03YR81rchP-Ap3p+z6aaEpU5j-CM9QHY83jpf6KEchaE6lkEQ2RjOGOoLIV2UvY+JUdmOudSL0c8AAeVwBbRfUNUkCqm8DH0EDAJaUEEgPYBgMtiX2uz0hKGDDyFqCckdDqDUFhCTAFGwTUFylbhKDg1P1hn8FZ38HqRIEoB6HGxUjAHYK5gCF5T1XeSZC4iOARCRy0ChBsFEAd2WH3zSByEhiFBsBHhYLrnYM4N8AnAX0GHeSFC+R0wkDmWyEOFhAdAgxKDsCUAGhXhcjIGwAfD5VaGnmZm5j1XgJczaXsMcPuEEFrkEA0PCjwLtQqGAldxBSWEyFkOSSsBKFSHbmhHUDsMBx8OcNrlcLIG6C0LGy510Mzz-Xyi+TkCh0YmhEoK0GrFWDsFghWBqGSIcKcPwBcJkRCmcNJELA8KY26W8L5SkX8PaP7HeSzngX9SqH2EUL00QFwwYihFbhBCe1yndxOFZwwHgCiD6ygEbxD0EEWDKFyEUHz3UC0EWyEEWElXwyFFrBxkOhcnxDAC2OX0EFBCIKhFmU0BNFyi8TFHKE5Hu1iMsErQIXl0LAePwPmJz0o3zzyELxhwYkaz-ljBMhuiYOKjEXuNUybyMAyGbjGlylsHeMBljz0kpTu1RGMj9jz16yDTP2LjSzKQUheXs05TxVBLtSlmDEdAxEUFjBqFSEBWSGdhgkd3yFlyBJr1pLrRhX8jhV80RRZIxJD3MhJVUEsGUVX1A3SjkBZFSFRDlnmFCNnXDUjVZKcSfg5AMRUGqIxELU4h2GGjUVyWOBRJEjpLnXWmNIVOXzEPxw4Vwz3DHWFF7jsjECqHzngzekc3lICy9Pbm2BWFgiESqDyEbGBjUElXhwqFUDLzqEkxRhk1GRNMXHbhZGxOtzFEbH7hJlB0BkpUcFeONBT2dNrWhVKyyzs1lMjMaULO2i7D-xBAtHAhsD9n6jJWyVrP3h1G+ybIG0V2f0wG7IzXsGXkIKNFDLWEoNUBDFRDmDrOHTUA9z2znP+0B3YAXMSX2D4XGi7BsHUHXJh2SEhBWHyQezmEBLp2BJnM9yPNZzuW-DPImRsCzTXj1EWFkEmIQHjyHMpST2tFkAPL+29193oX-LhGNGbl4TBBMDSDdksjLxDHbDRCOGULFFTxPJQuMEcgYmNCMmxI3270skrFbHMkA07BPzFNHwHAv0iU4Gv2nhQuPHlmPUtNz1tG3DyBDDWAWEow3Fpw2KIXQIgKN2wPQlgPIptB+N3lSVC2hyKCNBsgH1sGP2GnM2nI+DULYI4PROjPwKZAyDBwBHkFyloJkMsmovKAPC70yFTmsHqNSKaPSMEKstqy9JWAFCFHbztB5NdlkJshMHUCFF3k5EYPYraB6M4H8rrhaKkjaPQnIuW19mFCyAjE4lsCixZAHmLOZGtCtBcBcCAA */ | ||||
|     /** @xstate-layout N4IgpgJg5mDOIC5QFkD2EwBsCWA7KAxAMICGuAxlgNoAMAuoqAA6qzYAu2qujIAHogC0AdgCsAZgB04gEyjhADnEA2GgoUAWJQBoQAT0QBGGuICckmoZkbTM42YWKAvk91oMOfAQDKYdgAJYLDByTm5aBiQQFjYwniiBBFtpGhlxDRphGg1ZURlhXQMEcRLDSVF5UwV84XEK8Rc3dCw8KElsCEwwAgBRXHYwACdAgGs-cgALCN4Yji540ETBGWVhSStlU2UtK0NDVY1CxBlTDUkcnOtDcRoqzcaQdxb8ds7uvoHh2DH2SapDSLMVhzbi8JbWGjnfaWURqG4KTJHBAZGSSWzCZSiDYKa445QPJ6eNodLq9PjsQYAVww0yisziYKEFTWJxook2og0ohxSL2sMkMgUygUplMsJsGg0hgJzSJr1JPT4LEG7FpQNi80ZCGW6gF1RRdgl3KRwmEZVWEg0mIR+S5Mo8rUk3h+k2IZEomGd4ym9BmwIZCSMhiF60ykoRCLMsl5wbKe3Uexuwe59uebS9vwmTpdEwAkm8CMgSGNAmAoABbMD9fwANyGnHIJEwaui-s1gYQVlMceqmgxmlSBX0RilUhuNFS9XySg0qblGcm2e9+dJRZLQQrVYCE1Qg2wAC9uOwmy36e3FkZbZJVEK2Xt8qsZLzxHtylysvsX2y8nPHQus-+K7dEQ3CwBSJB4P4O57oe-RNv4EDYGB7pgKebagh2VichYGhpLcM7duoSJpFsFgVLC2SiN21y-i8-5LpmQHEKB4GQXWKrYI2mAIUhx4UKhvp0uhCz8JewponkChYtsxiaE+w4IDI+RSHkUp9qaKycrR6Y5gxkxMSBuBgYMEG4P4ACCABC3j+AAGmhGoYRenaCqI5ziGIhg2C+JxpMRYpnGKqwIpOpi1A0riPLKf66YBBaGcZpkWdZ-gAJoOSCImJFYUkCrh+xyIY7LsvJRTwgokjCHIFTQo4kraXpAE5gZLEmZBZBQF0GUBs51w5NISi9vIOLCKYxGwiy3LyKa7KjWFDX0XFpIJaxZldPg7A+oCraOVlRglGsKjsrc1xpEVxHKMGlWbFKo3hiowgLbFzXxa1SVMEMH24Ih5CUpgJDDIhyH8d156iS5grnKKPkZMGqjKMROJnGaVRhVVkoaGIT3eo1LVGatUG7geR5Npgej+E22BQLgoNOeDGyolJmhSeknI0Mo4i8tJApiup+TsiU2OZrjr3421ZnsQ2pPk5T1O03tLmje5gqjdJZhYryyhyOUIqjUK1RVLUQuLktwFvZBsC4CQTD+OwqB2fLWrXNcAocxGYXVGYxHZKiSnssoAdso4SnG01y6i4lFtWzbdtpY7mEvlImQc4YpoecYhwKYaaycsHVGp1aOShyLy3m2ZYAAI6UvB61QJt8e9ScKleaaYVsnIHm8rUkI5CYE5spi9yRYSMU46bzFi+9AOk9QgnqplTuyGsqQrC+VHpAXvKmB5FilJdoqZGoojF+PK3i-4gxgOWqB1v45Bl+wsAN-T1gVZd1glZoRU6ApwYvhY+xbDZBKGoLWJ8XqkgAEpgEEGAPgIRKQDGftlOoUhriqCwsydkSItjKHKGjOoOITgZ3AeHBUVdsA2wADJ4DALbVAqBmxzx2gvTCIo8ESBUDYMU6QiIKSUG5Ca1g-ZWixNKYe0U6LPTIe8ChMcOpbmwPBAG5B6HIOOPsCq4gpIp0xIoVBMZLDXVvMNdIhdi7mQAO4QW3ETWCx5uJAz4pQfweAABmqACAQG4GAdouAayoDGJIGA7BBDQWJnBTAgh3GoHUYpG4ZxJTbFuLUUUadiJSnMFJQUZ1NhyFWBY6xHBCYwRJo43iKFXG4A8QQIYgxdySCYP9dgHjBjlmCX4MJdiynROqbE5hZ46aJDwm5PYkpExckFGFYi2jUTZHZhOOoD5xFNAdFInGVibG1nrJxeCTjKkxK8T4vxASgkhMEJLXZUSYlxLOqia4ih7CrEMOiYigopCODyHka4L4qqFK2ZcriPFgYuMOXUhpTSSAtN3O085gKmy9I8bcnykgETyHzmITQbzcLrEHqkbRvYlD-OKVZGytkqk1O8bgXxeBTm+POSQAARrAQQfBEX9O2oMhWuTroyAnH7Qh-ktbrAyFKbYsgtZGwkWsnSGyikBFJXZClnjwWDEac01psLOlMpZWym5AzhJajOmsHIBK6imDZFofy+xpDckxF+TGNhiUKpSqlZVRzqUnMCfS7VzLBB6HZcim1dR0VUSbsILkSIbirChiKWE3IbhaWlWmRqkhNkktde61V6qoWao6aEnV-rA0Gt2ka35FgHweRWBzMVUb9hnHZrgv2DyZDOopvgLo7qqU0v8d6-NggOpdGLZyw1HZjUpETF80UGss4VDwdvNklgip2tNG2wddCwWDHqWqyF0K2n9vXcOv0pax0vgqlkKUgorBqG7EOIohU1iTqyHIduBTk3zl0umgItdNpduObSvt5yf0TCPUJE9zkzpuUuGYW6uFBwXVyuzUotRLpmlbe+0ewsv3+GA1mrdEKNUwv7cB0D88ergx5cYEw3JNGY1KscbYqIA6v1OGIGa+IMPrKw-K-wH1BhfR+n9AGwLnEbr6R6ntdL+18YE5xITgxBD7P4qRlh5HhkrFRLzWQVolLCjkIjbW6crSYmnR3NtMmqyCf+oDCp-E8PbpzXurVoSLPfTk9ZxTtnKAqa5UajTEklDWC1isaoohiKmkOmaYzVExRmc47K4WNDqX0MYb0ORtsFH9CUdxFRaiS2sIg6aM4RUzATnUKjBGCk0buQ8o4LIWxi5JboXbRhkhcy4A4AQW5aQ8GDSxAiU4cgxoKWFGUNIIpSseVGmkRrtCUuYDax19gXWATHoKxR-IqJYRXpknyzYJolZWlQbYdkFq2SzeSy1hbAA5e2AAFVAeBH4EHMhACAgRjwql449-otyA6BQHlRXCCJthIgDuYHIYU26KEGhd5rDCbv3Z+890g-EmEjvA-TTQ5gTDjb5acJmCgTRGJsPvUogoObF3a51uJxgbVaHkCYVQeRbiZyKFYEo5xVha3hqkGSVOlsrYx+t7Kd5UUWotUdvllg2eXm7JVFmVoNjeQ46slN9EAAqmXODKMGKou2aXKSUP8E1+bf3JTuUyFJMU7N6NJCtJVKqaRGfsytI9eLqbKTfXtshFU-4ADyuAJNeqCeZbwGvBBe+8YIX37AA803y2p-a2jyjKWopKOoagkTckhFsF5ahMbaIOqHfwuB7ZuJIJQHwwRQgITABXv6ARIXUricsV8wZThTa0AcMUYOVioo5zkOaGInUe7INgcsULWj0N4-9alweANBPH5PgYgg7aCAr5QOJNV1gIkUPMleiIRuQx8iAqZEb1ANWX1P-AM+4G8Wn98b0C-e1L46yvmB6+n+ZjiVrNyfP8gqgioh87cz1rxVhtEthytMYFAXBIoy8MB4AogR58A1sk9tQEQyhch9EbxmZOZf5xIld1Ah88l0M1c5QSQwA0CwYlhtgKpZowptEuQ+wkQExyh4QSsOdLBTBQ5qChlEAoDzgpIsgVB25Wdxo3I88LVuR8dMgaIPdFoIEqCwMRcRwTAeYI1ptNBKgidf4XlitL1o0DoTAIpyDMNFxsNwl7E9kvMxMPE+CFY4YKpJQUlHArBTRUgZlkhWZxxwd8gVkooZVU1sN4VykQU7DUAHCnZ1B7lVBLBUEtZuw3l+RUg1IvIA4ao21FVyUYkoi2Ft4LAlAuwVA7AJxht71t4yguRgoMFmdqgsjM1ciVD0CO8ttBQchgwfYo12Y3I-YsEqgDY10O0Ii8jeo41rxxxTUqg8ht5xo1BUVxs51k5nc21cMmiyMaCjARQe5qNrQXkVAA4LoVBKpuxDZVhTgBZzNPpLN3NhMlNQU+lRisd2QTjMQC9sgtgShwscRKoXlrdl1RpVdAj1ddJTcrsnjhl7ALBwopIbB1BhRs8fjt4gDUhbhNJTDgSP0cYwSEdFsOAISNEsRoSShYSBiESRtkh9ga0jtJch4zCuNFwcTWsy82kmwCTFIbATU05WRIwlJicqipDychRZA4d5tJBbt-AHsnskCNj+COSJA9QXwdhccVB8CigMRfYcRExgwR8cQBd8TmjNjOw2RzBthOQIQi9uxZdOxYx1hgwtSkwmZi4td8BFFdd9dIjDS5S5BNBHdV4tZhDRReR25XYhRhRGcsQ5Bi4o8fdPs48cxA92TNFzAJBc4BYE0Ths8HcldbAuRZodES8y9-BN9lDZSFZlgMhUUJU2NMYC8wsFIuQzh40ztjBW5VIr938b8oAZ9m9SzVMjTPw0QMRJcxQ0N2Y+88ETB1AMRc54R3d6S2hr9OBb9Y578wJH8cwkywpKp-ZGdDQXxbBEZIRA5tiIQhRBQ4CnAgA */ | ||||
|     id: 'Modeling', | ||||
|  | ||||
|     tsTypes: {} as import('./modelingMachine.typegen').Typegen0, | ||||
| @ -166,12 +166,6 @@ export const modelingMachine = createMachine( | ||||
|     states: { | ||||
|       idle: { | ||||
|         on: { | ||||
|           'Set selection': { | ||||
|             target: 'idle', | ||||
|             internal: true, | ||||
|             actions: 'Set selection', | ||||
|           }, | ||||
|  | ||||
|           'Enter sketch': [ | ||||
|             { | ||||
|               target: 'animating to existing sketch', | ||||
| @ -202,12 +196,6 @@ export const modelingMachine = createMachine( | ||||
|         states: { | ||||
|           SketchIdle: { | ||||
|             on: { | ||||
|               'Set selection': { | ||||
|                 target: 'SketchIdle', | ||||
|                 internal: true, | ||||
|                 actions: 'Set selection', | ||||
|               }, | ||||
|  | ||||
|               'Make segment vertical': { | ||||
|                 cond: 'Can make selection vertical', | ||||
|                 target: 'SketchIdle', | ||||
| @ -411,12 +399,6 @@ export const modelingMachine = createMachine( | ||||
|             exit: [], | ||||
|  | ||||
|             on: { | ||||
|               'Set selection': { | ||||
|                 target: 'Line tool', | ||||
|                 description: `This is just here to stop one of the higher level "Set selections" firing when we are just trying to set the IDE code without triggering a full engine-execute`, | ||||
|                 internal: true, | ||||
|               }, | ||||
|  | ||||
|               'Equip tangential arc to': { | ||||
|                 target: 'Tangential arc to', | ||||
|                 cond: 'is editing existing sketch', | ||||
| @ -435,14 +417,7 @@ export const modelingMachine = createMachine( | ||||
|                 ], | ||||
|               }, | ||||
|  | ||||
|               normal: { | ||||
|                 on: { | ||||
|                   'Set selection': { | ||||
|                     target: 'normal', | ||||
|                     internal: true, | ||||
|                   }, | ||||
|                 }, | ||||
|               }, | ||||
|               normal: {}, | ||||
|  | ||||
|               'No Points': { | ||||
|                 entry: 'setup noPoints onClick listener', | ||||
| @ -475,11 +450,6 @@ export const modelingMachine = createMachine( | ||||
|             entry: 'set up draft arc', | ||||
|  | ||||
|             on: { | ||||
|               'Set selection': { | ||||
|                 target: 'Tangential arc to', | ||||
|                 internal: true, | ||||
|               }, | ||||
|  | ||||
|               'Equip Line tool': 'Line tool', | ||||
|             }, | ||||
|           }, | ||||
| @ -519,11 +489,6 @@ export const modelingMachine = createMachine( | ||||
|             target: 'animating to plane', | ||||
|             actions: ['reset sketch metadata'], | ||||
|           }, | ||||
|  | ||||
|           'Set selection': { | ||||
|             target: 'Sketch no face', | ||||
|             internal: true, | ||||
|           }, | ||||
|         }, | ||||
|       }, | ||||
|  | ||||
| @ -537,13 +502,6 @@ export const modelingMachine = createMachine( | ||||
|           }, | ||||
|         }, | ||||
|  | ||||
|         on: { | ||||
|           'Set selection': { | ||||
|             target: 'animating to plane', | ||||
|             internal: true, | ||||
|           }, | ||||
|         }, | ||||
|  | ||||
|         entry: 'clientToEngine cam sync direction', | ||||
|       }, | ||||
|  | ||||
| @ -836,6 +794,7 @@ export const modelingMachine = createMachine( | ||||
|           if (Object.keys(sceneEntitiesManager.activeSegments).length > 0) { | ||||
|             await sceneEntitiesManager.tearDownSketch({ removeAxis: false }) | ||||
|           } | ||||
|           sceneInfra.resetMouseListeners() | ||||
|           await sceneEntitiesManager.setupSketch({ | ||||
|             sketchPathToNode: sketchDetails?.sketchPathToNode || [], | ||||
|             forward: sketchDetails.zAxis, | ||||
| @ -843,9 +802,13 @@ export const modelingMachine = createMachine( | ||||
|             position: sketchDetails.origin, | ||||
|             maybeModdedAst: kclManager.ast, | ||||
|           }) | ||||
|           sceneEntitiesManager.setupSketchIdleCallbacks( | ||||
|             sketchDetails?.sketchPathToNode || [] | ||||
|           ) | ||||
|           sceneInfra.resetMouseListeners() | ||||
|           sceneEntitiesManager.setupSketchIdleCallbacks({ | ||||
|             pathToNode: sketchDetails?.sketchPathToNode || [], | ||||
|             forward: sketchDetails.zAxis, | ||||
|             up: sketchDetails.yAxis, | ||||
|             position: sketchDetails.origin, | ||||
|           }) | ||||
|         })() | ||||
|       }, | ||||
|       'animate after sketch': () => { | ||||
|  | ||||
| @ -576,6 +576,8 @@ pub struct TangentialArcInfoOutput { | ||||
|     pub end_angle: f64, | ||||
|     /// If the arc is counter-clockwise. | ||||
|     pub ccw: i32, | ||||
|     /// The length of the arc. | ||||
|     pub arc_length: f64, | ||||
| } | ||||
|  | ||||
| // tanPreviousPoint and arcStartPoint make up a straight segment leading into the arc (of which the arc should be tangential). The arc should start at arcStartPoint and end at, arcEndPoint | ||||
| @ -626,6 +628,17 @@ pub fn get_tangential_arc_to_info(input: TangentialArcInfoInput) -> TangentialAr | ||||
|     let end_angle = (input.arc_end_point[1] - center[1]).atan2(input.arc_end_point[0] - center[0]); | ||||
|     let ccw = is_points_ccw(&[input.arc_start_point, arc_mid_point, input.arc_end_point]); | ||||
|  | ||||
|     let arc_mid_angle = (arc_mid_point[1] - center[1]).atan2(arc_mid_point[0] - center[0]); | ||||
|     let start_to_mid_arc_length = radius | ||||
|         * delta(Angle::from_radians(start_angle), Angle::from_radians(arc_mid_angle)) | ||||
|             .radians() | ||||
|             .abs(); | ||||
|     let mid_to_end_arc_length = radius | ||||
|         * delta(Angle::from_radians(arc_mid_angle), Angle::from_radians(end_angle)) | ||||
|             .radians() | ||||
|             .abs(); | ||||
|     let arc_length = start_to_mid_arc_length + mid_to_end_arc_length; | ||||
|  | ||||
|     TangentialArcInfoOutput { | ||||
|         center, | ||||
|         radius, | ||||
| @ -633,6 +646,7 @@ pub fn get_tangential_arc_to_info(input: TangentialArcInfoInput) -> TangentialAr | ||||
|         start_angle, | ||||
|         end_angle, | ||||
|         ccw, | ||||
|         arc_length, | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -758,6 +772,58 @@ mod get_tangential_arc_to_info_tests { | ||||
|         assert_relative_eq!(result.end_angle, -PI / 2.0); | ||||
|         assert_eq!(result.ccw, 1); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_arc_length_obtuse_cw() { | ||||
|         let result = get_tangential_arc_to_info(TangentialArcInfoInput { | ||||
|             tan_previous_point: [-1.0, -1.0], | ||||
|             arc_start_point: [-1.0, 0.0], | ||||
|             arc_end_point: [0.0, -1.0], | ||||
|             obtuse: true, | ||||
|         }); | ||||
|         let circumference = 2.0 * PI * result.radius; | ||||
|         let expected_length = circumference * 3.0 / 4.0; // 3 quarters of a circle circle | ||||
|         assert_relative_eq!(result.arc_length, expected_length); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_arc_length_acute_cw() { | ||||
|         let result = get_tangential_arc_to_info(TangentialArcInfoInput { | ||||
|             tan_previous_point: [-1.0, -1.0], | ||||
|             arc_start_point: [-1.0, 0.0], | ||||
|             arc_end_point: [0.0, 1.0], | ||||
|             obtuse: true, | ||||
|         }); | ||||
|         let circumference = 2.0 * PI * result.radius; | ||||
|         let expected_length = circumference / 4.0; // 1 quarters of a circle circle | ||||
|         assert_relative_eq!(result.arc_length, expected_length); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_arc_length_obtuse_ccw() { | ||||
|         let result = get_tangential_arc_to_info(TangentialArcInfoInput { | ||||
|             tan_previous_point: [1.0, -1.0], | ||||
|             arc_start_point: [1.0, 0.0], | ||||
|             arc_end_point: [0.0, -1.0], | ||||
|             obtuse: true, | ||||
|         }); | ||||
|         let circumference = 2.0 * PI * result.radius; | ||||
|         let expected_length = circumference * 3.0 / 4.0; // 1 quarters of a circle circle | ||||
|         assert_relative_eq!(result.arc_length, expected_length); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_arc_length_acute_ccw() { | ||||
|         let result = get_tangential_arc_to_info(TangentialArcInfoInput { | ||||
|             tan_previous_point: [1.0, -1.0], | ||||
|             arc_start_point: [1.0, 0.0], | ||||
|             arc_end_point: [0.0, 1.0], | ||||
|             obtuse: true, | ||||
|         }); | ||||
|         let circumference = 2.0 * PI * result.radius; | ||||
|         let expected_length = circumference / 4.0; // 1 quarters of a circle circle | ||||
|         assert_relative_eq!(result.arc_length, expected_length); | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub fn get_tangent_point_from_previous_arc( | ||||
|  | ||||
| @ -333,6 +333,8 @@ pub struct TangentialArcInfoOutputWasm { | ||||
|     pub end_angle: f64, | ||||
|     /// Flag to determine if the arc is counter clockwise. | ||||
|     pub ccw: i32, | ||||
|     /// The length of the arc. | ||||
|     pub arc_length: f64, | ||||
| } | ||||
|  | ||||
| #[wasm_bindgen] | ||||
| @ -362,6 +364,7 @@ pub fn get_tangential_arc_to_info( | ||||
|         start_angle: result.start_angle, | ||||
|         end_angle: result.end_angle, | ||||
|         ccw: result.ccw, | ||||
|         arc_length: result.arc_length, | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
