From acfa95f728a804e3d5e85dbca257faaeacac7e77 Mon Sep 17 00:00:00 2001 From: Kurt Hutten Irev-Dev Date: Sat, 24 Aug 2024 21:08:33 +1000 Subject: [PATCH] basic circle edit working --- src/clientSideScene/sceneEntities.ts | 435 +++++++++++++++++++++++++-- src/clientSideScene/segments.ts | 125 +++++++- src/lang/std/sketch.ts | 31 ++ src/wasm-lib/kcl/src/executor.rs | 17 ++ src/wasm-lib/kcl/src/std/extrude.rs | 2 +- src/wasm-lib/kcl/src/std/shapes.rs | 80 ++++- 6 files changed, 649 insertions(+), 41 deletions(-) diff --git a/src/clientSideScene/sceneEntities.ts b/src/clientSideScene/sceneEntities.ts index d677d50ef..17ee8f4f6 100644 --- a/src/clientSideScene/sceneEntities.ts +++ b/src/clientSideScene/sceneEntities.ts @@ -62,9 +62,10 @@ import { import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst' import { executeAst } from 'lang/langHelpers' import { + circleSegment, createArcGeometry, dashedStraight, - profileStart, + createProfileStartHandle, straightSegment, tangentialArcToSegment, } from './segments' @@ -72,6 +73,7 @@ import { addCallExpressionsToPipe, addCloseToPipe, addNewSketchLn, + changeCircleArguments, changeSketchArguments, updateStartProfileAtArgs, } from 'lang/std/sketch' @@ -119,6 +121,10 @@ export const TANGENTIAL_ARC_TO__SEGMENT_DASH = 'tangential-arc-to-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 CIRCLE_SEGMENT = 'circle-segment' +export const CIRCLE_SEGMENT_BODY = 'circle-segment-body' +export const CIRCLE_SEGMENT_DASH = 'circle-segment-body-dashed' +export const CIRCLE_CENTER_HANDLE = 'circle-center-handle' export const SEGMENT_WIDTH_PX = 1.6 export const HIDE_SEGMENT_LENGTH = 75 // in pixels export const HIDE_HOVER_SEGMENT_LENGTH = 60 // in pixels @@ -186,6 +192,27 @@ export class SceneEntities { }) ) } + if ( + segment.userData.from && + segment.userData.to && + segment.userData.center && + segment.userData.radius && + segment.userData.prevSegment && + segment.userData.type === CIRCLE_SEGMENT + ) { + callbacks.push( + this.updateCircleSegment({ + prevSegment: segment.userData.prevSegment, + from: segment.userData.from, + to: segment.userData.to, + center: segment.userData.center, + radius: segment.userData.radius, + group: segment, + scale: factor, + }) + ) + } + if (segment.name === PROFILE_START) { segment.scale.set(factor, factor, factor) } @@ -421,19 +448,21 @@ export class SceneEntities { maybeModdedAst, sketchGroup.start.__geoMeta.sourceRange ) - const _profileStart = profileStart({ - from: sketchGroup.start.from, - id: sketchGroup.start.__geoMeta.id, - pathToNode: segPathToNode, - scale: factor, - theme: sceneInfra._theme, - }) - _profileStart.layers.set(SKETCH_LAYER) - _profileStart.traverse((child) => { - child.layers.set(SKETCH_LAYER) - }) - group.add(_profileStart) - this.activeSegments[JSON.stringify(segPathToNode)] = _profileStart + if (sketchGroup?.value?.[0].type !== 'Circle') { + const _profileStart = createProfileStartHandle({ + from: sketchGroup.start.from, + id: sketchGroup.start.__geoMeta.id, + pathToNode: segPathToNode, + scale: factor, + theme: sceneInfra._theme, + }) + _profileStart.layers.set(SKETCH_LAYER) + _profileStart.traverse((child) => { + child.layers.set(SKETCH_LAYER) + }) + group.add(_profileStart) + this.activeSegments[JSON.stringify(segPathToNode)] = _profileStart + } const callbacks: (() => SegmentOverlayPayload | null)[] = [] sketchGroup.value.forEach((segment, index) => { let segPathToNode = getNodePathFromSourceRange( @@ -498,6 +527,32 @@ export class SceneEntities { scale: factor, }) ) + } else if (segment.type === 'Circle') { + seg = circleSegment({ + prevSegment: sketchGroup.value[index - 1], + from: segment.from, + to: segment.to, + center: segment.center, + radius: segment.radius, + id: segment.__geoMeta.id, + pathToNode: segPathToNode, + isDraftSegment, + scale: factor, + texture: sceneInfra.extraSegmentTexture, + theme: sceneInfra._theme, + isSelected, + }) + callbacks.push( + this.updateCircleSegment({ + prevSegment: sketchGroup.value[index - 1], + from: segment.from, + to: segment.to, + center: segment.center, + radius: segment.radius, + group: seg, + scale: factor, + }) + ) } else { seg = straightSegment({ from: segment.from, @@ -587,6 +642,7 @@ export class SceneEntities { segmentName: 'line' | 'tangentialArcTo' = 'line', shouldTearDown = true ) => { + // try { const _ast = structuredClone(kclManager.ast) const _node1 = getNodeFromPath( @@ -602,11 +658,10 @@ export class SceneEntities { kclManager.programMemory.get(variableDeclarationName), variableDeclarationName ) - if (err(sg)) return sg - const lastSeg = sg.value?.slice(-1)[0] || sg.start + if (err(sg)) return Promise.reject(sg) + const lastSeg = sg?.value?.slice(-1)[0] || sg.start const index = sg.value.length // because we've added a new segment that's not in the memory yet, no need for `-1` - const mod = addNewSketchLn({ node: _ast, programMemory: kclManager.programMemory, @@ -621,7 +676,7 @@ export class SceneEntities { const draftExpressionsIndices = { start: index, end: index } - if (shouldTearDown) await this.tearDownSketch({ removeAxis: false }) + // if (shouldTearDown) await this.tearDownSketch({ removeAxis: false }) sceneInfra.resetMouseListeners() const { truncatedAst, programMemoryOverride, sketchGroup } = @@ -725,6 +780,9 @@ export class SceneEntities { }, ...this.mouseEnterLeaveCallbacks(), }) + // } catch (e) { + // console.log('yo wtf', e) + // } } setupDraftRectangle = async ( sketchPathToNode: PathToNode, @@ -746,6 +804,164 @@ export class SceneEntities { const startSketchOn = _node1.node?.declarations const startSketchOnInit = startSketchOn?.[0]?.init + const sg = sketchGroupFromKclValue( + kclManager.programMemory.get(variableDeclarationName), + variableDeclarationName + ) + if (err(sg)) return sg + const tags: [string, string, string] = [ + findUniqueName(_ast, 'rectangleSegmentA'), + findUniqueName(_ast, 'rectangleSegmentB'), + findUniqueName(_ast, 'rectangleSegmentC'), + ] + + startSketchOn[0].init = createPipeExpression([ + startSketchOnInit, + ...getRectangleCallExpressions(rectangleOrigin, tags), + ]) + + let _recastAst = parse(recast(_ast)) + if (trap(_recastAst)) return Promise.reject(_recastAst) + _ast = _recastAst + + const { programMemoryOverride, truncatedAst } = await this.setupSketch({ + sketchPathToNode, + forward, + up, + position: sketchOrigin, + maybeModdedAst: _ast, + draftExpressionsIndices: { start: 0, end: 3 }, + }) + + sceneInfra.setCallbacks({ + onMove: async (args) => { + // Update the width and height of the draft rectangle + const pathToNodeTwo = structuredClone(sketchPathToNode) + pathToNodeTwo[1][0] = 0 + + const _node = getNodeFromPath( + truncatedAst, + pathToNodeTwo || [], + 'VariableDeclaration' + ) + if (trap(_node)) return Promise.reject(_node) + const sketchInit = _node.node?.declarations?.[0]?.init + + const x = (args.intersectionPoint.twoD.x || 0) - rectangleOrigin[0] + const y = (args.intersectionPoint.twoD.y || 0) - rectangleOrigin[1] + + if (sketchInit.type === 'PipeExpression') { + updateRectangleSketch(sketchInit, x, y, tags[0]) + } + + const { programMemory } = await executeAst({ + ast: truncatedAst, + useFakeExecutor: true, + engineCommandManager: this.engineCommandManager, + programMemoryOverride, + }) + this.sceneProgramMemory = programMemory + const sketchGroup = sketchGroupFromKclValue( + programMemory.get(variableDeclarationName), + variableDeclarationName + ) + if (err(sketchGroup)) return Promise.reject(sketchGroup) + const sgPaths = sketchGroup.value + const orthoFactor = orthoScale(sceneInfra.camControls.camera) + + this.updateSegment( + sketchGroup.start, + 0, + 0, + _ast, + orthoFactor, + sketchGroup + ) + sgPaths.forEach((seg, index) => + this.updateSegment(seg, index, 0, _ast, orthoFactor, sketchGroup) + ) + }, + onClick: async (args) => { + // Commit the rectangle to the full AST/code and return to sketch.idle + const cornerPoint = args.intersectionPoint?.twoD + if (!cornerPoint || args.mouseEvent.button !== 0) return + + const x = roundOff((cornerPoint.x || 0) - rectangleOrigin[0]) + const y = roundOff((cornerPoint.y || 0) - rectangleOrigin[1]) + + const _node = getNodeFromPath( + _ast, + sketchPathToNode || [], + 'VariableDeclaration' + ) + if (trap(_node)) return Promise.reject(_node) + const sketchInit = _node.node?.declarations?.[0]?.init + + if (sketchInit.type === 'PipeExpression') { + updateRectangleSketch(sketchInit, x, y, tags[0]) + + let _recastAst = parse(recast(_ast)) + if (trap(_recastAst)) return Promise.reject(_recastAst) + _ast = _recastAst + + // Update the primary AST and unequip the rectangle tool + await kclManager.executeAstMock(_ast) + sceneInfra.modelingSend({ type: 'Finish rectangle' }) + + const { programMemory } = await executeAst({ + ast: _ast, + useFakeExecutor: true, + engineCommandManager: this.engineCommandManager, + programMemoryOverride, + }) + + // Prepare to update the THREEjs scene + this.sceneProgramMemory = programMemory + const sketchGroup = sketchGroupFromKclValue( + programMemory.get( + variableDeclarationName + ), variableDeclarationName) + if (err(sketchGroup)) return Promise.reject(sketchGroup) + const sgPaths = sketchGroup.value + const orthoFactor = orthoScale(sceneInfra.camControls.camera) + + // Update the starting segment of the THREEjs scene + this.updateSegment( + sketchGroup.start, + 0, + 0, + _ast, + orthoFactor, + sketchGroup + ) + // Update the rest of the segments of the THREEjs scene + sgPaths.forEach((seg, index) => + this.updateSegment(seg, index, 0, _ast, orthoFactor, sketchGroup) + ) + } + }, + }) + } + setupDraftCircle = async ( + sketchPathToNode: PathToNode, + forward: [number, number, number], + up: [number, number, number], + sketchOrigin: [number, number, number], + rectangleOrigin: [x: number, y: number] + ) => { + let _ast = structuredClone(kclManager.ast) + + const _node1 = getNodeFromPath( + _ast, + sketchPathToNode || [], + 'VariableDeclaration' + ) + if (trap(_node1)) return Promise.reject(_node1) + const variableDeclarationName = + _node1.node?.declarations?.[0]?.id?.name || '' + const startSketchOn = _node1.node?.declarations + const startSketchOnInit = startSketchOn?.[0]?.init + const tags: [string, string, string] = [ findUniqueName(_ast, 'rectangleSegmentA'), findUniqueName(_ast, 'rectangleSegmentB'), @@ -1047,7 +1263,9 @@ export class SceneEntities { STRAIGHT_SEGMENT, TANGENTIAL_ARC_TO_SEGMENT, PROFILE_START, + CIRCLE_SEGMENT, ]) + const subGroup = getParentGroup(object, [ARROWHEAD, CIRCLE_CENTER_HANDLE]) if (!group) return const pathToNode: PathToNode = structuredClone(group.userData.pathToNode) const varDecIndex = pathToNode[1][0] @@ -1065,7 +1283,7 @@ export class SceneEntities { group.userData.from[0], group.userData.from[1], ] - const to: [number, number] = [intersection2d.x, intersection2d.y] + const dragTo: [number, number] = [intersection2d.x, intersection2d.y] let modifiedAst = draftInfo ? draftInfo.truncatedAst : { ...kclManager.ast } const _node = getNodeFromPath( @@ -1088,16 +1306,42 @@ export class SceneEntities { modded = updateStartProfileAtArgs({ node: modifiedAst, pathToNode, - to, + to: dragTo, from, previousProgramMemory: kclManager.programMemory, }) + } else if (group.name === CIRCLE_SEGMENT && subGroup?.name === ARROWHEAD) { + // is dragging the radius handle + modded = changeCircleArguments( + modifiedAst, + kclManager.programMemory, + [node.start, node.end], + group.userData.center, + Math.sqrt( + (group.userData.center[0] - dragTo[0]) ** 2 + + (group.userData.center[0] - dragTo[0]) ** 2 + ) + ) + console.log('modded', modded) + } else if ( + group.name === CIRCLE_SEGMENT && + subGroup?.name === CIRCLE_CENTER_HANDLE + ) { + // is dragging the center handle + modded = changeCircleArguments( + modifiedAst, + kclManager.programMemory, + [node.start, node.end], + dragTo, + group.userData.radius + ) + console.log('modded', modded) } else { modded = changeSketchArguments( modifiedAst, kclManager.programMemory, [node.start, node.end], - to, + dragTo, from ) } @@ -1216,6 +1460,20 @@ export class SceneEntities { group, scale: factor, }) + } else if ( + type === CIRCLE_SEGMENT && + 'center' in segment && + 'radius' in segment + ) { + return this.updateCircleSegment({ + prevSegment: sgPaths[index - 1], + from: segment.from, + to: segment.to, + center: segment.center, + radius: segment.radius, + group, + scale: factor, + }) } else if (type === PROFILE_START) { group.position.set(segment.from[0], segment.from[1], 0) group.scale.set(factor, factor, factor) @@ -1241,6 +1499,9 @@ export class SceneEntities { group.userData.prevSegment = prevSegment const arrowGroup = group.getObjectByName(ARROWHEAD) as Group const extraSegmentGroup = group.getObjectByName(EXTRA_SEGMENT_HANDLE) + if (!prevSegment) { + console.trace('prevSegment is undefined') + } const previousPoint = prevSegment?.type === 'TangentialArcTo' @@ -1336,6 +1597,111 @@ export class SceneEntities { angle, }) } + updateCircleSegment({ + prevSegment, + from, + to, + center, + radius, + group, + scale = 1, + }: { + prevSegment: SketchGroup['value'][number] + from: [number, number] + to: [number, number] + center: [number, number] + radius: number + group: Group + scale?: number + }): () => SegmentOverlayPayload | null { + group.userData.from = from + group.userData.to = to + group.userData.center = center + group.userData.radius = radius + group.userData.prevSegment = prevSegment + const arrowGroup = group.getObjectByName(ARROWHEAD) as Group + const circleCenterHandle = group.getObjectByName( + CIRCLE_CENTER_HANDLE + ) as Group + + const pxLength = (2 * radius * Math.PI) / scale + const shouldHideIdle = pxLength < HIDE_SEGMENT_LENGTH + const shouldHideHover = pxLength < HIDE_HOVER_SEGMENT_LENGTH + + const hoveredParent = + sceneInfra.hoveredObject && + getParentGroup(sceneInfra.hoveredObject, [TANGENTIAL_ARC_TO_SEGMENT]) + let isHandlesVisible = !shouldHideIdle + if (hoveredParent && hoveredParent?.uuid === group?.uuid) { + isHandlesVisible = !shouldHideHover + } + + if (arrowGroup) { + arrowGroup.position.set( + center[0] + Math.cos(Math.PI / 4) * radius, + center[1] + Math.sin(Math.PI / 4) * radius, + 0 + ) + + const arrowheadAngle = Math.PI / 4 + 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 = isHandlesVisible + } + + if (circleCenterHandle) { + circleCenterHandle.position.set(center[0], center[1], 0) + circleCenterHandle.scale.set(scale, scale, scale) + circleCenterHandle.visible = isHandlesVisible + } + + const circleSegmentBody = group.children.find( + (child) => child.userData.type === CIRCLE_SEGMENT_BODY + ) as Mesh + + if (circleSegmentBody) { + const newGeo = createArcGeometry({ + radius, + center, + startAngle: 0, + endAngle: Math.PI * 2, + ccw: true, + scale, + }) + circleSegmentBody.geometry = newGeo + } + const circleSegmentBodyDashed = group.children.find( + (child) => child.userData.type === CIRCLE_SEGMENT_DASH + ) as Mesh + if (circleSegmentBodyDashed) { + // consider throttling the whole updateTangentialArcToSegment + // if there are more perf considerations going forward + this.throttledUpdateDashedArcGeo({ + // ...arcInfo, + center, + radius, + ccw: true, + startAngle: 0, + endAngle: 360, + mesh: circleSegmentBodyDashed, + isDashed: true, + scale, + }) + } + const angle = 0 + return () => + sceneInfra.updateOverlayDetails({ + arrowGroup, + group, + isHandlesVisible, + from, + to, + angle, + }) + } throttledUpdateDashedArcGeo = throttle( ( args: Parameters[0] & { @@ -1470,7 +1836,7 @@ export class SceneEntities { } private _tearDownSketch( callDepth = 0, - resolve: (val: unknown) => void, + resolve: any, reject: () => void, { removeAxis = true }: { removeAxis?: boolean } ) { @@ -1500,7 +1866,7 @@ export class SceneEntities { this._tearDownSketch(callDepth + 1, resolve, reject, { removeAxis }) }, delay) } else { - reject() + resolve(true) } } sceneInfra.camControls.enableRotate = true @@ -1512,7 +1878,7 @@ export class SceneEntities { removeAxis = true, }: { removeAxis?: boolean - } = {}) { + } = {}): Promise { // I think promisifying this is mostly a side effect of not having // "setupSketch" correctly capture a promise when it's done // so we're effectively waiting for to be finished setting up the scene just to tear it down @@ -1578,6 +1944,16 @@ export class SceneEntities { group: parent, scale: factor, }) + } else if (parent.name === CIRCLE_SEGMENT) { + this.updateCircleSegment({ + prevSegment: parent.userData.prevSegment, + from: parent.userData.from, + to: parent.userData.to, + center: parent.userData.center, + radius: parent.userData.radius, + group: parent, + scale: factor, + }) } return } @@ -1613,6 +1989,16 @@ export class SceneEntities { group: parent, scale: factor, }) + } else if (parent.name === CIRCLE_SEGMENT) { + this.updateCircleSegment({ + prevSegment: parent.userData.prevSegment, + from: parent.userData.from, + to: parent.userData.to, + center: parent.userData.center, + radius: parent.userData.radius, + group: parent, + scale: factor, + }) } } const isSelected = parent?.userData?.isSelected @@ -1825,6 +2211,7 @@ function colorSegment(object: any, color: number) { const straightSegmentBody = getParentGroup(object, [ STRAIGHT_SEGMENT, TANGENTIAL_ARC_TO_SEGMENT, + CIRCLE_SEGMENT, ]) if (straightSegmentBody) { straightSegmentBody.traverse((child) => { diff --git a/src/clientSideScene/segments.ts b/src/clientSideScene/segments.ts index e1d451fe0..369187727 100644 --- a/src/clientSideScene/segments.ts +++ b/src/clientSideScene/segments.ts @@ -24,6 +24,10 @@ import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer' import { PathToNode, SketchGroup, getTangentialArcToInfo } from 'lang/wasm' import { + CIRCLE_CENTER_HANDLE, + CIRCLE_SEGMENT, + CIRCLE_SEGMENT_BODY, + CIRCLE_SEGMENT_DASH, EXTRA_SEGMENT_HANDLE, EXTRA_SEGMENT_OFFSET_PX, HIDE_SEGMENT_LENGTH, @@ -46,7 +50,7 @@ import { import { Themes, getThemeColorForThreeJs } from 'lib/theme' import { roundOff } from 'lib/utils' -export function profileStart({ +export function createProfileStartHandle({ from, id, pathToNode, @@ -225,6 +229,28 @@ function createArrowhead(scale = 1, theme: Themes, color?: number): Group { arrowGroup.scale.set(scale, scale, scale) return arrowGroup } +function createCircleCenterHandle( + scale = 1, + theme: Themes, + color?: number +): Group { + const circleCenterGroup = new Group() + + const geometry = new BoxGeometry(12, 12, 12) // in pixels scaled later + const baseColor = getThemeColorForThreeJs(theme) + const body = new MeshBasicMaterial({ color }) + const mesh = new Mesh(geometry, body) + + circleCenterGroup.add(mesh) + + circleCenterGroup.userData = { + type: CIRCLE_CENTER_HANDLE, + baseColor, + } + circleCenterGroup.name = CIRCLE_CENTER_HANDLE + circleCenterGroup.scale.set(scale, scale, scale) + return circleCenterGroup +} function createExtraSegmentHandle( scale: number, @@ -300,6 +326,103 @@ function createLengthIndicator({ return lengthIndicatorGroup } +export function circleSegment({ + prevSegment, + from, + to, + center, + radius, + id, + pathToNode, + isDraftSegment, + scale = 1, + texture, + theme, + isSelected, +}: { + prevSegment: SketchGroup['value'][number] + from: Coords2d + center: Coords2d + radius: number + to: Coords2d + id: string + pathToNode: PathToNode + isDraftSegment?: boolean + scale?: number + texture: Texture + theme: Themes + isSelected?: boolean +}): Group { + const group = new Group() + + const geometry = createArcGeometry({ + center, + radius, + startAngle: 0, + endAngle: Math.PI * 2, + ccw: true, + isDashed: isDraftSegment, + scale, + }) + + const baseColor = getThemeColorForThreeJs(theme) + const color = isSelected ? 0x0000ff : baseColor + const body = new MeshBasicMaterial({ color }) + const mesh = new Mesh(geometry, body) + mesh.userData.type = isDraftSegment + ? CIRCLE_SEGMENT_DASH + : CIRCLE_SEGMENT_BODY + + group.userData = { + type: CIRCLE_SEGMENT, + id, + from, + to, + radius, + center, + ccw: true, + prevSegment, + pathToNode, + isSelected, + baseColor, + } + group.name = CIRCLE_SEGMENT + + const arrowGroup = createArrowhead(scale, theme, color) + arrowGroup.position.set( + center[0] + Math.cos(Math.PI / 4) * radius, + center[1] + Math.sin(Math.PI / 4) * radius, + 0 + ) + + const circleCenterGroup = createCircleCenterHandle(scale, theme, color) + circleCenterGroup.position.set(center[0], center[1], 0) + const arrowheadAngle = Math.PI / 4 + arrowGroup.quaternion.setFromUnitVectors( + new Vector3(0, 1, 0), + new Vector3(Math.cos(arrowheadAngle), Math.sin(arrowheadAngle), 0) + ) + const pxLength = (radius * 2 * Math.PI) / scale + const shouldHide = pxLength < HIDE_SEGMENT_LENGTH + + const extraSegmentGroup = createExtraSegmentHandle(scale, texture, theme) + const extraSegmentAngle = 0 + 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, circleCenterGroup, extraSegmentGroup) + + return group +} export function tangentialArcToSegment({ prevSegment, from, diff --git a/src/lang/std/sketch.ts b/src/lang/std/sketch.ts index 068c2fd63..44d334cdc 100644 --- a/src/lang/std/sketch.ts +++ b/src/lang/std/sketch.ts @@ -1611,6 +1611,37 @@ export const sketchLineHelperMap: { [key: string]: SketchLineHelper } = { tangentialArcTo, } as const +export function changeCircleArguments( + node: Program, + programMemory: ProgramMemory, + sourceRange: SourceRange, + center: [number, number], + radius: number +): { modifiedAst: Program; pathToNode: PathToNode } | Error { + const _node = { ...node } + const thePath = getNodePathFromSourceRange(_node, sourceRange) + const nodeMeta = getNodeFromPath(_node, thePath) + if (err(nodeMeta)) return nodeMeta + + const { node: callExpression, shallowPath } = nodeMeta + + if (callExpression?.callee?.name === 'circle') { + const newCenter = createArrayExpression([ + createLiteral(roundOff(center[0])), + createLiteral(roundOff(center[1])), + ]) + const newRadius = createLiteral(roundOff(radius)) + callExpression.arguments[0] = newCenter + callExpression.arguments[1] = newRadius + return { + modifiedAst: _node, + pathToNode: shallowPath, + } + } + + return new Error(`not a sketch line helper: ${callExpression?.callee?.name}`) +} + export function changeSketchArguments( node: Program, programMemory: ProgramMemory, diff --git a/src/wasm-lib/kcl/src/executor.rs b/src/wasm-lib/kcl/src/executor.rs index 87323c9ad..32887e143 100644 --- a/src/wasm-lib/kcl/src/executor.rs +++ b/src/wasm-lib/kcl/src/executor.rs @@ -1415,6 +1415,19 @@ pub enum Path { /// arc's direction ccw: bool, }, + /// a complete arc + Circle { + #[serde(flatten)] + base: BasePath, + /// the arc's center + #[ts(type = "[number, number]")] + center: [f64; 2], + /// the arc's radius + radius: f64, + /// arc's direction + // Maybe this one's not needed since it's a full revolution? + ccw: bool, + }, /// A path that is horizontal. Horizontal { #[serde(flatten)] @@ -1447,6 +1460,7 @@ impl Path { Path::Base { base } => base.geo_meta.id, Path::TangentialArcTo { base, .. } => base.geo_meta.id, Path::TangentialArc { base, .. } => base.geo_meta.id, + Path::Circle { base, .. } => base.geo_meta.id, } } @@ -1458,6 +1472,7 @@ impl Path { Path::Base { base } => base.tag.clone(), Path::TangentialArcTo { base, .. } => base.tag.clone(), Path::TangentialArc { base, .. } => base.tag.clone(), + Path::Circle { base, .. } => base.tag.clone(), } } @@ -1469,6 +1484,7 @@ impl Path { Path::Base { base } => base, Path::TangentialArcTo { base, .. } => base, Path::TangentialArc { base, .. } => base, + Path::Circle { base, .. } => base, } } @@ -1480,6 +1496,7 @@ impl Path { Path::Base { base } => Some(base), Path::TangentialArcTo { base, .. } => Some(base), Path::TangentialArc { base, .. } => Some(base), + Path::Circle { base, .. } => Some(base), } } } diff --git a/src/wasm-lib/kcl/src/std/extrude.rs b/src/wasm-lib/kcl/src/std/extrude.rs index 0a0081c4a..bfd9b3e3a 100644 --- a/src/wasm-lib/kcl/src/std/extrude.rs +++ b/src/wasm-lib/kcl/src/std/extrude.rs @@ -204,7 +204,7 @@ pub(crate) async fn do_post_extrude( for path in sketch_group.value.iter() { if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) { match path { - Path::TangentialArc { .. } | Path::TangentialArcTo { .. } => { + Path::TangentialArc { .. } | Path::TangentialArcTo { .. } | Path::Circle { .. } => { let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::executor::ExtrudeArc { face_id: *actual_face_id, tag: path.get_base().tag.clone(), diff --git a/src/wasm-lib/kcl/src/std/shapes.rs b/src/wasm-lib/kcl/src/std/shapes.rs index 1c80c2d9d..7e066adb1 100644 --- a/src/wasm-lib/kcl/src/std/shapes.rs +++ b/src/wasm-lib/kcl/src/std/shapes.rs @@ -2,16 +2,19 @@ use anyhow::Result; use derive_docs::stdlib; +use kittycad::types::{Angle, ModelingCmd}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use crate::{ ast::types::TagDeclarator, - errors::KclError, - executor::KclValue, - std::{Args, SketchGroup, SketchSurface}, + errors::{KclError, KclErrorDetails}, + executor::{BasePath, GeoMeta, KclValue, Path, Point2d, SketchGroup, SketchSurface}, + std::Args, }; +use super::utils::arc_center_and_end; + /// A sketch surface or a sketch group. #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] #[ts(export)] @@ -65,23 +68,70 @@ async fn inner_circle( SketchSurfaceOrGroup::SketchSurface(surface) => surface, SketchSurfaceOrGroup::SketchGroup(group) => group.on, }; - let mut sketch_group = + let sketch_group = crate::std::sketch::inner_start_profile_at([center[0] + radius, center[1]], sketch_surface, None, args.clone()) .await?; - // Call arc. - sketch_group = crate::std::sketch::inner_arc( - crate::std::sketch::ArcData::AnglesAndRadius { - angle_start: 0.0, - angle_end: 360.0, - radius, + let from: Point2d = sketch_group.current_pen_position()?; + + let angle_start = Angle::from_degrees(0.0); + let angle_end = Angle::from_degrees(360.0); + let (center, end) = arc_center_and_end(from, angle_start, angle_end, radius); + + if angle_start == angle_end { + return Err(KclError::Type(KclErrorDetails { + message: "Arc start and end angles must be different".to_string(), + source_ranges: vec![args.source_range], + })); + } + + let id = uuid::Uuid::new_v4(); + + args.batch_modeling_cmd( + id, + ModelingCmd::ExtendPath { + path: sketch_group.id, + segment: kittycad::types::PathSegment::Arc { + start: angle_start, + end: angle_end, + center: center.into(), + radius, + relative: false, + }, }, - sketch_group, - tag, - args.clone(), ) .await?; - // Call close. - crate::std::sketch::inner_close(sketch_group, None, args).await + let current_path = Path::Circle { + base: BasePath { + from: center.into(), + // to: end.into(), + to: center.into(), + tag: tag.clone(), + geo_meta: GeoMeta { + id, + metadata: args.source_range.into(), + }, + }, + radius, + center: center.into(), + ccw: angle_start.degrees() < angle_end.degrees(), + }; + + let mut new_sketch_group = sketch_group.clone(); + if let Some(tag) = &tag { + new_sketch_group.add_tag(tag, ¤t_path); + } + + new_sketch_group.value.push(current_path); + + args.batch_modeling_cmd( + id, + ModelingCmd::ClosePath { + path_id: new_sketch_group.id, + }, + ) + .await?; + + Ok(new_sketch_group) }