New segments can be added in the middle of a sketch (#1953)

* get branch up to where it was before

* setup dots properly

* only show extra handle on hover

* use partical texture for plus button

* fix regression

* fix deleted line

* fix sketch on face test

* caluclate segment length in screen-space/in-pixels

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

* side small segment handles on resize

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

* Make sure this works on setup and update of segments

* Add to tangential arcs

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

This reverts commit 5dc1adacae.

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

This reverts commit b8ceea179c.

* try and fix sketch on face in CI

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

* more test fix

* convert scaling to be based on pixels

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

* trigger ci

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

This reverts commit 6287c943dd.

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

This reverts commit 1baa3819db.

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

* Update src/clientSideScene/segments.ts

Co-authored-by: Frank Noirot <frank@zoo.dev>

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

* reduce line thickness

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

* trigger CI

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

* trigger CI

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

* trigger CI

* try putting init script back in

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Frank Noirot <frank@zoo.dev>
This commit is contained in:
Kurt Hutten
2024-04-03 13:22:56 +11:00
committed by GitHub
parent 221f037eaa
commit b045a89854
22 changed files with 484 additions and 67 deletions

View File

@ -1496,9 +1496,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], {
@ -1512,7 +1516,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(%)`)
@ -1535,7 +1541,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, %)`)

View File

@ -320,6 +320,22 @@ test('extrude on each default plane should be stable', async ({
page,
context,
}) => {
await context.addInitScript(async () => {
localStorage.setItem(
'SETTINGS_PERSIST_KEY',
JSON.stringify({
baseUnit: 'in',
cameraControls: 'KittyCAD',
defaultDirectory: '',
defaultProjectName: 'project-$nnn',
onboardingStatus: 'dismissed',
showDebugPanel: true,
textWrapping: 'On',
theme: 'dark',
unitSystem: 'imperial',
})
)
})
const u = getUtils(page)
const makeCode = (plane = 'XY') => `const part001 = startSketchOn('${plane}')
|> startProfileAt([7.00, 4.40], %)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 B

View File

@ -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)

View File

@ -12,6 +12,7 @@ import {
OrthographicCamera,
PerspectiveCamera,
PlaneGeometry,
Points,
Quaternion,
Scene,
Shape,
@ -87,14 +88,18 @@ 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
export const SEGMENT_WIDTH_PX = 1.6
// This singleton Class is responsible for all of the things the user sees and interacts with.
// That mostly mean sketch elements.
@ -111,8 +116,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 +291,6 @@ export class SceneEntities {
sketchGroup: SketchGroup
variableDeclarationName: string
}> {
sceneInfra.resetMouseListeners()
this.createIntersectionPlane()
const { truncatedAst, programMemoryOverride, variableDeclarationName } =
@ -295,7 +303,7 @@ export class SceneEntities {
})
const sketchGroup = sketchGroupFromPathToNode({
pathToNode: sketchPathToNode,
ast: kclManager.ast,
ast: maybeModdedAst,
programMemory,
})
if (!Array.isArray(sketchGroup?.value))
@ -383,6 +391,7 @@ export class SceneEntities {
pathToNode: segPathToNode,
isDraftSegment,
scale: factor,
texture: sceneInfra.extraSegmentTexture,
})
} else {
seg = straightSegment({
@ -393,6 +402,7 @@ export class SceneEntities {
isDraftSegment,
scale: factor,
callExpName,
texture: sceneInfra.extraSegmentTexture,
})
}
seg.layers.set(SKETCH_LAYER)
@ -435,6 +445,7 @@ export class SceneEntities {
) => {
await kclManager.updateAst(modifiedAst, false)
await this.tearDownSketch({ removeAxis: false })
sceneInfra.resetMouseListeners()
await this.setupSketch({
sketchPathToNode,
forward,
@ -442,7 +453,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 +483,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 +566,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 +863,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 +881,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 +961,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, (-SEGMENT_WIDTH_PX / 2) * scale) // The width of the line in px (2.4px in this case)
shape.lineTo(0, (SEGMENT_WIDTH_PX / 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 +983,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 +1316,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 +1420,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 +1442,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 +1466,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

View File

@ -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: () => {},
@ -210,6 +222,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
}
@ -358,6 +377,7 @@ export class SceneInfra {
this.hoveredObject = firstIntersectObject
this.onMouseEnter({
selected: this.hoveredObject,
dragSelected: this.selected?.object,
mouseEvent: mouseEvent,
})
}
@ -365,6 +385,7 @@ export class SceneInfra {
if (this.hoveredObject) {
this.onMouseLeave({
selected: this.hoveredObject,
dragSelected: this.selected?.object,
mouseEvent: mouseEvent,
})
this.hoveredObject = null
@ -453,8 +474,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({

View File

@ -12,15 +12,22 @@ 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,
SEGMENT_WIDTH_PX,
STRAIGHT_SEGMENT,
STRAIGHT_SEGMENT_BODY,
STRAIGHT_SEGMENT_DASH,
@ -44,7 +51,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 +78,7 @@ export function straightSegment({
isDraftSegment,
scale = 1,
callExpName,
texture,
}: {
from: Coords2d
to: Coords2d
@ -79,12 +87,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, (-SEGMENT_WIDTH_PX / 2) * scale)
shape.lineTo(0, (SEGMENT_WIDTH_PX / 2) * scale)
let geometry
if (isDraftSegment) {
@ -122,24 +131,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 +179,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 +217,7 @@ export function tangentialArcToSegment({
pathToNode,
isDraftSegment,
scale = 1,
texture,
}: {
prevSegment: SketchGroup['value'][number]
from: Coords2d
@ -166,6 +226,7 @@ export function tangentialArcToSegment({
pathToNode: PathToNode
isDraftSegment?: boolean
scale?: number
texture: Texture
}): Group {
const group = new Group()
@ -178,12 +239,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 +281,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 +324,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 +347,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, (-SEGMENT_WIDTH_PX / 2) * scale)
shape.lineTo(0, (SEGMENT_WIDTH_PX / 2) * scale) // The width of the line
if (!isDashed) {
const points = arcStart.getPoints(50)
@ -281,7 +363,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 +371,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 +396,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 +442,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: gapSize is not respected
const dashLine = new LineCurve3(
new Vector3(from[0], from[1], 0),
new Vector3(to[0], to[1], 0)

View File

@ -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,
})
}

View File

@ -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 {

View File

@ -252,6 +252,7 @@ export function getTangentialArcToInfo({
startAngle: number
endAngle: number
ccw: boolean
arcLength: number
} {
const result = get_tangential_arc_to_info(
arcStartPoint[0],
@ -269,6 +270,7 @@ export function getTangentialArcToInfo({
startAngle: result.start_angle,
endAngle: result.end_angle,
ccw: result.ccw > 0,
arcLength: result.arc_length,
}
}

View File

@ -793,6 +793,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,
@ -800,9 +801,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': () => {

View File

@ -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(

View File

@ -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,
}
}