Snap to origin and axis behavior for profile starts and segments (#4344)
* Visualize draft point when near axes (only works on XY rn due to quaternion rotation issue) * Slightly better quaternion rotation * Actually snap new profiles to the X and Y axis * Add snapping behavior while dragging * Fix flickering on non-XY planes * Add some fixture additions to support click-and-drag tests * Add new test to verify snapping behavior * Make the editor test fixture auto-open and close as needed * All feedback except absolute lines * Use `lineTo` for lines that have snapped * Get other existing tests passing after switching to `lineTo` when snapping * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest) * Re-run CI --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
@ -86,7 +86,7 @@ async function doBasicSketch(page: Page, openPanes: string[]) {
|
|||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${commonPoints.num1}, 0], %)
|
|> line([${commonPoints.num1}, 0], %)
|
||||||
|> line([0, ${commonPoints.num1 + 0.01}], %)
|
|> line([0, ${commonPoints.num1 + 0.01}], %)
|
||||||
|> line([-${commonPoints.num2}, 0], %)`)
|
|> lineTo([0, ${commonPoints.num3}], %)`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// deselect line tool
|
// deselect line tool
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
import type { Page, Locator } from '@playwright/test'
|
import type { Page, Locator } from '@playwright/test'
|
||||||
import { expect } from '@playwright/test'
|
import { expect } from '@playwright/test'
|
||||||
import { sansWhitespace } from '../test-utils'
|
import {
|
||||||
|
closePane,
|
||||||
|
checkIfPaneIsOpen,
|
||||||
|
openPane,
|
||||||
|
sansWhitespace,
|
||||||
|
} from '../test-utils'
|
||||||
|
|
||||||
interface EditorState {
|
interface EditorState {
|
||||||
activeLines: Array<string>
|
activeLines: Array<string>
|
||||||
@ -11,6 +16,7 @@ interface EditorState {
|
|||||||
export class EditorFixture {
|
export class EditorFixture {
|
||||||
public page: Page
|
public page: Page
|
||||||
|
|
||||||
|
private paneButtonTestId = 'code-pane-button'
|
||||||
private diagnosticsTooltip!: Locator
|
private diagnosticsTooltip!: Locator
|
||||||
private diagnosticsGutterIcon!: Locator
|
private diagnosticsGutterIcon!: Locator
|
||||||
private codeContent!: Locator
|
private codeContent!: Locator
|
||||||
@ -31,19 +37,32 @@ export class EditorFixture {
|
|||||||
|
|
||||||
private _expectEditorToContain =
|
private _expectEditorToContain =
|
||||||
(not = false) =>
|
(not = false) =>
|
||||||
(
|
async (
|
||||||
code: string,
|
code: string,
|
||||||
{
|
{
|
||||||
shouldNormalise = false,
|
shouldNormalise = false,
|
||||||
timeout = 5_000,
|
timeout = 5_000,
|
||||||
}: { shouldNormalise?: boolean; timeout?: number } = {}
|
}: { shouldNormalise?: boolean; timeout?: number } = {}
|
||||||
) => {
|
) => {
|
||||||
|
const wasPaneOpen = await this.checkIfPaneIsOpen()
|
||||||
|
if (!wasPaneOpen) {
|
||||||
|
await this.openPane()
|
||||||
|
}
|
||||||
|
const resetPane = async () => {
|
||||||
|
if (!wasPaneOpen) {
|
||||||
|
await this.closePane()
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!shouldNormalise) {
|
if (!shouldNormalise) {
|
||||||
const expectStart = expect(this.codeContent)
|
const expectStart = expect(this.codeContent)
|
||||||
if (not) {
|
if (not) {
|
||||||
return expectStart.not.toContainText(code, { timeout })
|
const result = await expectStart.not.toContainText(code, { timeout })
|
||||||
|
await resetPane()
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
return expectStart.toContainText(code, { timeout })
|
const result = await expectStart.toContainText(code, { timeout })
|
||||||
|
await resetPane()
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
const normalisedCode = code.replaceAll(/\s+/g, '').trim()
|
const normalisedCode = code.replaceAll(/\s+/g, '').trim()
|
||||||
const expectStart = expect.poll(
|
const expectStart = expect.poll(
|
||||||
@ -56,9 +75,13 @@ export class EditorFixture {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
if (not) {
|
if (not) {
|
||||||
return expectStart.not.toContain(normalisedCode)
|
const result = await expectStart.not.toContain(normalisedCode)
|
||||||
|
await resetPane()
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
return expectStart.toContain(normalisedCode)
|
const result = await expectStart.toContain(normalisedCode)
|
||||||
|
await resetPane()
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
expectEditor = {
|
expectEditor = {
|
||||||
toContain: this._expectEditorToContain(),
|
toContain: this._expectEditorToContain(),
|
||||||
@ -115,4 +138,13 @@ export class EditorFixture {
|
|||||||
code = code.replace(findCode, replaceCode)
|
code = code.replace(findCode, replaceCode)
|
||||||
await this.codeContent.fill(code)
|
await this.codeContent.fill(code)
|
||||||
}
|
}
|
||||||
|
checkIfPaneIsOpen() {
|
||||||
|
return checkIfPaneIsOpen(this.page, this.paneButtonTestId)
|
||||||
|
}
|
||||||
|
closePane() {
|
||||||
|
return closePane(this.page, this.paneButtonTestId)
|
||||||
|
}
|
||||||
|
openPane() {
|
||||||
|
return openPane(this.page, this.paneButtonTestId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ export class AuthenticatedApp {
|
|||||||
public readonly page: Page
|
public readonly page: Page
|
||||||
public readonly context: BrowserContext
|
public readonly context: BrowserContext
|
||||||
public readonly testInfo: TestInfo
|
public readonly testInfo: TestInfo
|
||||||
|
public readonly viewPortSize = { width: 1000, height: 500 }
|
||||||
|
|
||||||
constructor(context: BrowserContext, page: Page, testInfo: TestInfo) {
|
constructor(context: BrowserContext, page: Page, testInfo: TestInfo) {
|
||||||
this.page = page
|
this.page = page
|
||||||
@ -36,7 +37,7 @@ export class AuthenticatedApp {
|
|||||||
;(window as any).playwrightSkipFilePicker = true
|
;(window as any).playwrightSkipFilePicker = true
|
||||||
}, code)
|
}, code)
|
||||||
|
|
||||||
await this.page.setViewportSize({ width: 1000, height: 500 })
|
await this.page.setViewportSize(this.viewPortSize)
|
||||||
|
|
||||||
await u.waitForAuthSkipAppStart()
|
await u.waitForAuthSkipAppStart()
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,13 @@ import {
|
|||||||
} from '../test-utils'
|
} from '../test-utils'
|
||||||
|
|
||||||
type mouseParams = {
|
type mouseParams = {
|
||||||
pixelDiff: number
|
pixelDiff?: number
|
||||||
|
}
|
||||||
|
type mouseDragToParams = mouseParams & {
|
||||||
|
fromPoint: { x: number; y: number }
|
||||||
|
}
|
||||||
|
type mouseDragFromParams = mouseParams & {
|
||||||
|
toPoint: { x: number; y: number }
|
||||||
}
|
}
|
||||||
|
|
||||||
type SceneSerialised = {
|
type SceneSerialised = {
|
||||||
@ -20,6 +26,13 @@ type SceneSerialised = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ClickHandler = (clickParams?: mouseParams) => Promise<void | boolean>
|
||||||
|
type MoveHandler = (moveParams?: mouseParams) => Promise<void | boolean>
|
||||||
|
type DragToHandler = (dragParams: mouseDragToParams) => Promise<void | boolean>
|
||||||
|
type DragFromHandler = (
|
||||||
|
dragParams: mouseDragFromParams
|
||||||
|
) => Promise<void | boolean>
|
||||||
|
|
||||||
export class SceneFixture {
|
export class SceneFixture {
|
||||||
public page: Page
|
public page: Page
|
||||||
|
|
||||||
@ -55,7 +68,7 @@ export class SceneFixture {
|
|||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
{ steps }: { steps: number } = { steps: 20 }
|
{ steps }: { steps: number } = { steps: 20 }
|
||||||
) =>
|
): [ClickHandler, MoveHandler] =>
|
||||||
[
|
[
|
||||||
(clickParams?: mouseParams) => {
|
(clickParams?: mouseParams) => {
|
||||||
if (clickParams?.pixelDiff) {
|
if (clickParams?.pixelDiff) {
|
||||||
@ -78,6 +91,47 @@ export class SceneFixture {
|
|||||||
return this.page.mouse.move(x, y, { steps })
|
return this.page.mouse.move(x, y, { steps })
|
||||||
},
|
},
|
||||||
] as const
|
] as const
|
||||||
|
makeDragHelpers = (
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
{ steps }: { steps: number } = { steps: 20 }
|
||||||
|
): [DragToHandler, DragFromHandler] =>
|
||||||
|
[
|
||||||
|
(dragToParams: mouseDragToParams) => {
|
||||||
|
if (dragToParams?.pixelDiff) {
|
||||||
|
return doAndWaitForImageDiff(
|
||||||
|
this.page,
|
||||||
|
() =>
|
||||||
|
this.page.dragAndDrop('#stream', '#stream', {
|
||||||
|
sourcePosition: dragToParams.fromPoint,
|
||||||
|
targetPosition: { x, y },
|
||||||
|
}),
|
||||||
|
dragToParams.pixelDiff
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return this.page.dragAndDrop('#stream', '#stream', {
|
||||||
|
sourcePosition: dragToParams.fromPoint,
|
||||||
|
targetPosition: { x, y },
|
||||||
|
})
|
||||||
|
},
|
||||||
|
(dragFromParams: mouseDragFromParams) => {
|
||||||
|
if (dragFromParams?.pixelDiff) {
|
||||||
|
return doAndWaitForImageDiff(
|
||||||
|
this.page,
|
||||||
|
() =>
|
||||||
|
this.page.dragAndDrop('#stream', '#stream', {
|
||||||
|
sourcePosition: { x, y },
|
||||||
|
targetPosition: dragFromParams.toPoint,
|
||||||
|
}),
|
||||||
|
dragFromParams.pixelDiff
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return this.page.dragAndDrop('#stream', '#stream', {
|
||||||
|
sourcePosition: { x, y },
|
||||||
|
targetPosition: dragFromParams.toPoint,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
] as const
|
||||||
|
|
||||||
/** Likely no where, there's a chance it will click something in the scene, depending what you have in the scene.
|
/** Likely no where, there's a chance it will click something in the scene, depending what you have in the scene.
|
||||||
*
|
*
|
||||||
|
@ -7,6 +7,7 @@ export class ToolbarFixture {
|
|||||||
|
|
||||||
extrudeButton!: Locator
|
extrudeButton!: Locator
|
||||||
startSketchBtn!: Locator
|
startSketchBtn!: Locator
|
||||||
|
lineBtn!: Locator
|
||||||
rectangleBtn!: Locator
|
rectangleBtn!: Locator
|
||||||
exitSketchBtn!: Locator
|
exitSketchBtn!: Locator
|
||||||
editSketchBtn!: Locator
|
editSketchBtn!: Locator
|
||||||
@ -24,6 +25,7 @@ export class ToolbarFixture {
|
|||||||
this.page = page
|
this.page = page
|
||||||
this.extrudeButton = page.getByTestId('extrude')
|
this.extrudeButton = page.getByTestId('extrude')
|
||||||
this.startSketchBtn = page.getByTestId('sketch')
|
this.startSketchBtn = page.getByTestId('sketch')
|
||||||
|
this.lineBtn = page.getByTestId('line')
|
||||||
this.rectangleBtn = page.getByTestId('corner-rectangle')
|
this.rectangleBtn = page.getByTestId('corner-rectangle')
|
||||||
this.exitSketchBtn = page.getByTestId('sketch-exit')
|
this.exitSketchBtn = page.getByTestId('sketch-exit')
|
||||||
this.editSketchBtn = page.getByText('Edit Sketch')
|
this.editSketchBtn = page.getByText('Edit Sketch')
|
||||||
|
@ -451,3 +451,103 @@ sketch002 = startSketchOn(extrude001, seg03)
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test(`Verify axis and origin snapping`, async ({
|
||||||
|
app,
|
||||||
|
editor,
|
||||||
|
toolbar,
|
||||||
|
scene,
|
||||||
|
}) => {
|
||||||
|
// Constants and locators
|
||||||
|
// These are mappings from screenspace to KCL coordinates,
|
||||||
|
// until we merge in our coordinate system helpers
|
||||||
|
const xzPlane = [
|
||||||
|
app.viewPortSize.width * 0.65,
|
||||||
|
app.viewPortSize.height * 0.3,
|
||||||
|
] as const
|
||||||
|
const originSloppy = {
|
||||||
|
screen: [
|
||||||
|
app.viewPortSize.width / 2 + 3, // 3px off the center of the screen
|
||||||
|
app.viewPortSize.height / 2,
|
||||||
|
],
|
||||||
|
kcl: [0, 0],
|
||||||
|
} as const
|
||||||
|
const xAxisSloppy = {
|
||||||
|
screen: [
|
||||||
|
app.viewPortSize.width * 0.75,
|
||||||
|
app.viewPortSize.height / 2 - 3, // 3px off the X-axis
|
||||||
|
],
|
||||||
|
kcl: [16.95, 0],
|
||||||
|
} as const
|
||||||
|
const offYAxis = {
|
||||||
|
screen: [
|
||||||
|
app.viewPortSize.width * 0.6, // Well off the Y-axis, out of snapping range
|
||||||
|
app.viewPortSize.height * 0.3,
|
||||||
|
],
|
||||||
|
kcl: [6.78, 6.78],
|
||||||
|
} as const
|
||||||
|
const yAxisSloppy = {
|
||||||
|
screen: [
|
||||||
|
app.viewPortSize.width / 2 + 5, // 5px off the Y-axis
|
||||||
|
app.viewPortSize.height * 0.3,
|
||||||
|
],
|
||||||
|
kcl: [0, 6.78],
|
||||||
|
} as const
|
||||||
|
const [clickOnXzPlane, moveToXzPlane] = scene.makeMouseHelpers(...xzPlane)
|
||||||
|
const [clickOriginSloppy] = scene.makeMouseHelpers(...originSloppy.screen)
|
||||||
|
const [clickXAxisSloppy, moveXAxisSloppy] = scene.makeMouseHelpers(
|
||||||
|
...xAxisSloppy.screen
|
||||||
|
)
|
||||||
|
const [dragToOffYAxis, dragFromOffAxis] = scene.makeDragHelpers(
|
||||||
|
...offYAxis.screen
|
||||||
|
)
|
||||||
|
|
||||||
|
const expectedCodeSnippets = {
|
||||||
|
sketchOnXzPlane: `sketch001 = startSketchOn('XZ')`,
|
||||||
|
pointAtOrigin: `startProfileAt([${originSloppy.kcl[0]}, ${originSloppy.kcl[1]}], %)`,
|
||||||
|
segmentOnXAxis: `lineTo([${xAxisSloppy.kcl[0]}, ${xAxisSloppy.kcl[1]}], %)`,
|
||||||
|
afterSegmentDraggedOffYAxis: `startProfileAt([${offYAxis.kcl[0]}, ${offYAxis.kcl[1]}], %)`,
|
||||||
|
afterSegmentDraggedOnYAxis: `startProfileAt([${yAxisSloppy.kcl[0]}, ${yAxisSloppy.kcl[1]}], %)`,
|
||||||
|
}
|
||||||
|
|
||||||
|
await app.initialise()
|
||||||
|
|
||||||
|
await test.step(`Start a sketch on the XZ plane`, async () => {
|
||||||
|
await editor.closePane()
|
||||||
|
await toolbar.startSketchPlaneSelection()
|
||||||
|
await moveToXzPlane()
|
||||||
|
await clickOnXzPlane()
|
||||||
|
// timeout wait for engine animation is unavoidable
|
||||||
|
await app.page.waitForTimeout(600)
|
||||||
|
await editor.expectEditor.toContain(expectedCodeSnippets.sketchOnXzPlane)
|
||||||
|
})
|
||||||
|
await test.step(`Place a point a few pixels off the middle, verify it still snaps to 0,0`, async () => {
|
||||||
|
await clickOriginSloppy()
|
||||||
|
await editor.expectEditor.toContain(expectedCodeSnippets.pointAtOrigin)
|
||||||
|
})
|
||||||
|
await test.step(`Add a segment on x-axis after moving the mouse a bit, verify it snaps`, async () => {
|
||||||
|
await moveXAxisSloppy()
|
||||||
|
await clickXAxisSloppy()
|
||||||
|
await editor.expectEditor.toContain(expectedCodeSnippets.segmentOnXAxis)
|
||||||
|
})
|
||||||
|
await test.step(`Unequip line tool`, async () => {
|
||||||
|
await toolbar.lineBtn.click()
|
||||||
|
await expect(toolbar.lineBtn).not.toHaveAttribute('aria-pressed', 'true')
|
||||||
|
})
|
||||||
|
await test.step(`Drag the origin point up and to the right, verify it's past snapping`, async () => {
|
||||||
|
await dragToOffYAxis({
|
||||||
|
fromPoint: { x: originSloppy.screen[0], y: originSloppy.screen[1] },
|
||||||
|
})
|
||||||
|
await editor.expectEditor.toContain(
|
||||||
|
expectedCodeSnippets.afterSegmentDraggedOffYAxis
|
||||||
|
)
|
||||||
|
})
|
||||||
|
await test.step(`Drag the origin point left to the y-axis, verify it snaps back`, async () => {
|
||||||
|
await dragFromOffAxis({
|
||||||
|
toPoint: { x: yAxisSloppy.screen[0], y: yAxisSloppy.screen[1] },
|
||||||
|
})
|
||||||
|
await editor.expectEditor.toContain(
|
||||||
|
expectedCodeSnippets.afterSegmentDraggedOnYAxis
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
@ -637,7 +637,6 @@ test.describe('Sketch tests', () => {
|
|||||||
|> revolve({ axis: "X" }, %)`)
|
|> revolve({ axis: "X" }, %)`)
|
||||||
})
|
})
|
||||||
test('Can add multiple sketches', async ({ page }) => {
|
test('Can add multiple sketches', async ({ page }) => {
|
||||||
test.skip(process.platform === 'darwin', 'Can add multiple sketches')
|
|
||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
const viewportSize = { width: 1200, height: 500 }
|
const viewportSize = { width: 1200, height: 500 }
|
||||||
await page.setViewportSize(viewportSize)
|
await page.setViewportSize(viewportSize)
|
||||||
@ -675,15 +674,16 @@ test.describe('Sketch tests', () => {
|
|||||||
|
|
||||||
await click00r(50, 0)
|
await click00r(50, 0)
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
codeStr += ` |> line(${toSU([50, 0])}, %)`
|
codeStr += ` |> lineTo(${toSU([50, 0])}, %)`
|
||||||
await expect(u.codeLocator).toHaveText(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
|
|
||||||
await click00r(0, 50)
|
await click00r(0, 50)
|
||||||
codeStr += ` |> line(${toSU([0, 50])}, %)`
|
codeStr += ` |> line(${toSU([0, 50])}, %)`
|
||||||
await expect(u.codeLocator).toHaveText(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
|
|
||||||
await click00r(-50, 0)
|
let clickCoords = await click00r(-50, 0)
|
||||||
codeStr += ` |> line(${toSU([-50, 0])}, %)`
|
expect(clickCoords).not.toBeUndefined()
|
||||||
|
codeStr += ` |> lineTo(${toSU(clickCoords!)}, %)`
|
||||||
await expect(u.codeLocator).toHaveText(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
|
|
||||||
// exit the sketch, reset relative clicker
|
// exit the sketch, reset relative clicker
|
||||||
@ -709,8 +709,10 @@ test.describe('Sketch tests', () => {
|
|||||||
codeStr += ` |> startProfileAt([2.03, 0], %)`
|
codeStr += ` |> startProfileAt([2.03, 0], %)`
|
||||||
await expect(u.codeLocator).toHaveText(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
|
|
||||||
|
// TODO: I couldn't use `toSU` here because of some rounding error causing
|
||||||
|
// it to be off by 0.01
|
||||||
await click00r(30, 0)
|
await click00r(30, 0)
|
||||||
codeStr += ` |> line([2.04, 0], %)`
|
codeStr += ` |> lineTo([4.07, 0], %)`
|
||||||
await expect(u.codeLocator).toHaveText(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
|
|
||||||
await click00r(0, 30)
|
await click00r(0, 30)
|
||||||
|
@ -219,7 +219,7 @@ test.describe('Test network and connection issues', () => {
|
|||||||
|> startProfileAt([12.34, -12.34], %)
|
|> startProfileAt([12.34, -12.34], %)
|
||||||
|> line([12.34, 0], %)
|
|> line([12.34, 0], %)
|
||||||
|> line([-12.34, 12.34], %)
|
|> line([-12.34, 12.34], %)
|
||||||
|> line([-12.34, 0], %)
|
|> lineTo([0, -12.34], %)
|
||||||
|
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
@ -45,7 +45,9 @@ export const commonPoints = {
|
|||||||
startAt: '[7.19, -9.7]',
|
startAt: '[7.19, -9.7]',
|
||||||
num1: 7.25,
|
num1: 7.25,
|
||||||
num2: 14.44,
|
num2: 14.44,
|
||||||
}
|
/** The Y-value of a common lineTo move we perform in tests */
|
||||||
|
num3: -2.44,
|
||||||
|
} as const
|
||||||
|
|
||||||
/** A semi-reliable color to check the default XZ plane on
|
/** A semi-reliable color to check the default XZ plane on
|
||||||
* in dark mode in the default camera position
|
* in dark mode in the default camera position
|
||||||
@ -118,15 +120,32 @@ async function waitForDefaultPlanesToBeVisible(page: Page) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openPane(page: Page, testId: string) {
|
export async function checkIfPaneIsOpen(page: Page, testId: string) {
|
||||||
const locator = page.getByTestId(testId)
|
const paneButtonLocator = page.getByTestId(testId)
|
||||||
await expect(locator).toBeVisible()
|
await expect(paneButtonLocator).toBeVisible()
|
||||||
const isOpen = (await locator?.getAttribute('aria-pressed')) === 'true'
|
return (await paneButtonLocator?.getAttribute('aria-pressed')) === 'true'
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function openPane(page: Page, testId: string) {
|
||||||
|
const paneButtonLocator = page.getByTestId(testId)
|
||||||
|
await expect(paneButtonLocator).toBeVisible()
|
||||||
|
const isOpen = await checkIfPaneIsOpen(page, testId)
|
||||||
|
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
await locator.click()
|
await paneButtonLocator.click()
|
||||||
await expect(locator).toHaveAttribute('aria-pressed', 'true')
|
|
||||||
}
|
}
|
||||||
|
await expect(paneButtonLocator).toHaveAttribute('aria-pressed', 'true')
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function closePane(page: Page, testId: string) {
|
||||||
|
const paneButtonLocator = page.getByTestId(testId)
|
||||||
|
await expect(paneButtonLocator).toBeVisible()
|
||||||
|
const isOpen = await checkIfPaneIsOpen(page, testId)
|
||||||
|
|
||||||
|
if (isOpen) {
|
||||||
|
await paneButtonLocator.click()
|
||||||
|
}
|
||||||
|
await expect(paneButtonLocator).toHaveAttribute('aria-pressed', 'false')
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openKclCodePanel(page: Page) {
|
async function openKclCodePanel(page: Page) {
|
||||||
|
@ -96,7 +96,7 @@ test.describe('Testing selections', () => {
|
|||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${commonPoints.num1}, 0], %)
|
|> line([${commonPoints.num1}, 0], %)
|
||||||
|> line([0, ${commonPoints.num1 + 0.01}], %)
|
|> line([0, ${commonPoints.num1 + 0.01}], %)
|
||||||
|> line([-${commonPoints.num2}, 0], %)`)
|
|> lineTo([0, ${commonPoints.num3}], %)`)
|
||||||
|
|
||||||
// deselect line tool
|
// deselect line tool
|
||||||
await page.getByRole('button', { name: 'line Line', exact: true }).click()
|
await page.getByRole('button', { name: 'line Line', exact: true }).click()
|
||||||
@ -157,7 +157,9 @@ test.describe('Testing selections', () => {
|
|||||||
await emptySpaceClick()
|
await emptySpaceClick()
|
||||||
|
|
||||||
// check the same selection again by putting cursor in code first then selecting axis
|
// check the same selection again by putting cursor in code first then selecting axis
|
||||||
await page.getByText(` |> line([-${commonPoints.num2}, 0], %)`).click()
|
await page
|
||||||
|
.getByText(` |> lineTo([0, ${commonPoints.num3}], %)`)
|
||||||
|
.click()
|
||||||
await page.keyboard.down('Shift')
|
await page.keyboard.down('Shift')
|
||||||
await constrainButton.click()
|
await constrainButton.click()
|
||||||
await expect(absYButton).toBeDisabled()
|
await expect(absYButton).toBeDisabled()
|
||||||
@ -180,7 +182,9 @@ test.describe('Testing selections', () => {
|
|||||||
process.platform === 'linux' ? 'Control' : 'Meta'
|
process.platform === 'linux' ? 'Control' : 'Meta'
|
||||||
)
|
)
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
await page.getByText(` |> line([-${commonPoints.num2}, 0], %)`).click()
|
await page
|
||||||
|
.getByText(` |> lineTo([0, ${commonPoints.num3}], %)`)
|
||||||
|
.click()
|
||||||
|
|
||||||
await expect(page.locator('.cm-cursor')).toHaveCount(2)
|
await expect(page.locator('.cm-cursor')).toHaveCount(2)
|
||||||
await page.waitForTimeout(500)
|
await page.waitForTimeout(500)
|
||||||
|
@ -19,6 +19,8 @@ import {
|
|||||||
import {
|
import {
|
||||||
ARROWHEAD,
|
ARROWHEAD,
|
||||||
AXIS_GROUP,
|
AXIS_GROUP,
|
||||||
|
DRAFT_POINT,
|
||||||
|
DRAFT_POINT_GROUP,
|
||||||
getSceneScale,
|
getSceneScale,
|
||||||
INTERSECTION_PLANE_LAYER,
|
INTERSECTION_PLANE_LAYER,
|
||||||
OnClickCallbackArgs,
|
OnClickCallbackArgs,
|
||||||
@ -53,7 +55,7 @@ import {
|
|||||||
editorManager,
|
editorManager,
|
||||||
} from 'lib/singletons'
|
} from 'lib/singletons'
|
||||||
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
|
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
|
||||||
import { executeAst } from 'lang/langHelpers'
|
import { executeAst, ToolTip } from 'lang/langHelpers'
|
||||||
import {
|
import {
|
||||||
createProfileStartHandle,
|
createProfileStartHandle,
|
||||||
SegmentUtils,
|
SegmentUtils,
|
||||||
@ -314,6 +316,27 @@ export class SceneEntities {
|
|||||||
const intersectionPlane = this.scene.getObjectByName(RAYCASTABLE_PLANE)
|
const intersectionPlane = this.scene.getObjectByName(RAYCASTABLE_PLANE)
|
||||||
if (intersectionPlane) this.scene.remove(intersectionPlane)
|
if (intersectionPlane) this.scene.remove(intersectionPlane)
|
||||||
}
|
}
|
||||||
|
getDraftPoint() {
|
||||||
|
return this.scene.getObjectByName(DRAFT_POINT)
|
||||||
|
}
|
||||||
|
createDraftPoint({ point, group }: { point: Vector2; group: Group }) {
|
||||||
|
const dummy = new Mesh()
|
||||||
|
dummy.position.set(0, 0, 0)
|
||||||
|
const scale = sceneInfra.getClientSceneScaleFactor(dummy)
|
||||||
|
|
||||||
|
const draftPoint = createProfileStartHandle({
|
||||||
|
isDraft: true,
|
||||||
|
from: [point.x, point.y],
|
||||||
|
scale,
|
||||||
|
theme: sceneInfra._theme,
|
||||||
|
})
|
||||||
|
draftPoint.layers.set(SKETCH_LAYER)
|
||||||
|
group.add(draftPoint)
|
||||||
|
}
|
||||||
|
removeDraftPoint() {
|
||||||
|
const draftPoint = this.getDraftPoint()
|
||||||
|
if (draftPoint) draftPoint.removeFromParent()
|
||||||
|
}
|
||||||
|
|
||||||
setupNoPointsListener({
|
setupNoPointsListener({
|
||||||
sketchDetails,
|
sketchDetails,
|
||||||
@ -322,22 +345,78 @@ export class SceneEntities {
|
|||||||
sketchDetails: SketchDetails
|
sketchDetails: SketchDetails
|
||||||
afterClick: (args: OnClickCallbackArgs) => void
|
afterClick: (args: OnClickCallbackArgs) => void
|
||||||
}) {
|
}) {
|
||||||
// Create a THREEjs plane to raycast clicks onto
|
// TODO: Consolidate shared logic between this and setupSketch
|
||||||
|
// Which should just fire when the sketch mode is entered,
|
||||||
|
// instead of in these two separate XState states.
|
||||||
this.createIntersectionPlane()
|
this.createIntersectionPlane()
|
||||||
|
const draftPointGroup = new Group()
|
||||||
|
draftPointGroup.name = DRAFT_POINT_GROUP
|
||||||
|
sketchDetails.origin &&
|
||||||
|
draftPointGroup.position.set(...sketchDetails.origin)
|
||||||
|
if (!(sketchDetails.yAxis && sketchDetails)) {
|
||||||
|
console.error('No sketch quaternion or sketch details found')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.currentSketchQuaternion = quaternionFromUpNForward(
|
||||||
|
new Vector3(...sketchDetails.yAxis),
|
||||||
|
new Vector3(...sketchDetails.zAxis)
|
||||||
|
)
|
||||||
|
draftPointGroup.setRotationFromQuaternion(this.currentSketchQuaternion)
|
||||||
|
this.scene.add(draftPointGroup)
|
||||||
|
|
||||||
const quaternion = quaternionFromUpNForward(
|
const quaternion = quaternionFromUpNForward(
|
||||||
new Vector3(...sketchDetails.yAxis),
|
new Vector3(...sketchDetails.yAxis),
|
||||||
new Vector3(...sketchDetails.zAxis)
|
new Vector3(...sketchDetails.zAxis)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Position the click raycast plane
|
// Position the click raycast plane
|
||||||
if (this.intersectionPlane) {
|
this.intersectionPlane!.setRotationFromQuaternion(quaternion)
|
||||||
this.intersectionPlane.setRotationFromQuaternion(quaternion)
|
this.intersectionPlane!.position.copy(
|
||||||
this.intersectionPlane.position.copy(
|
new Vector3(...(sketchDetails?.origin || [0, 0, 0]))
|
||||||
new Vector3(...(sketchDetails?.origin || [0, 0, 0]))
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
sceneInfra.setCallbacks({
|
sceneInfra.setCallbacks({
|
||||||
|
onMove: (args) => {
|
||||||
|
if (!args.intersects.length) return
|
||||||
|
const axisIntersection = args.intersects.find(
|
||||||
|
(sceneObject) =>
|
||||||
|
sceneObject.object.name === X_AXIS ||
|
||||||
|
sceneObject.object.name === Y_AXIS
|
||||||
|
)
|
||||||
|
if (!axisIntersection) return
|
||||||
|
const { intersectionPoint } = args
|
||||||
|
// We're hovering over an axis, so we should show a draft point
|
||||||
|
const snappedPoint = intersectionPoint.twoD.clone()
|
||||||
|
if (axisIntersection.object.name === X_AXIS) {
|
||||||
|
snappedPoint.setComponent(1, 0)
|
||||||
|
} else {
|
||||||
|
snappedPoint.setComponent(0, 0)
|
||||||
|
}
|
||||||
|
// Either create a new one or update the existing one
|
||||||
|
const draftPoint = this.getDraftPoint()
|
||||||
|
|
||||||
|
if (!draftPoint) {
|
||||||
|
this.createDraftPoint({
|
||||||
|
point: snappedPoint,
|
||||||
|
group: draftPointGroup,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Ignore if there are huge jumps in the mouse position,
|
||||||
|
// that is likely a strange behavior
|
||||||
|
if (
|
||||||
|
draftPoint.position.distanceTo(
|
||||||
|
new Vector3(snappedPoint.x, snappedPoint.y, 0)
|
||||||
|
) > 100
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
draftPoint.position.set(snappedPoint.x, snappedPoint.y, 0)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onMouseLeave: () => {
|
||||||
|
this.removeDraftPoint()
|
||||||
|
},
|
||||||
onClick: async (args) => {
|
onClick: async (args) => {
|
||||||
|
this.removeDraftPoint()
|
||||||
if (!args) return
|
if (!args) return
|
||||||
// If there is a valid camera interaction that matches, do that instead
|
// If there is a valid camera interaction that matches, do that instead
|
||||||
const interaction = sceneInfra.camControls.getInteractionType(
|
const interaction = sceneInfra.camControls.getInteractionType(
|
||||||
@ -347,10 +426,25 @@ export class SceneEntities {
|
|||||||
if (args.mouseEvent.which !== 1) return
|
if (args.mouseEvent.which !== 1) return
|
||||||
const { intersectionPoint } = args
|
const { intersectionPoint } = args
|
||||||
if (!intersectionPoint?.twoD || !sketchDetails?.sketchPathToNode) return
|
if (!intersectionPoint?.twoD || !sketchDetails?.sketchPathToNode) return
|
||||||
|
|
||||||
|
// Snap to either or both axes
|
||||||
|
// if the click intersects their meshes
|
||||||
|
const yAxisIntersection = args.intersects.find(
|
||||||
|
(sceneObject) => sceneObject.object.name === Y_AXIS
|
||||||
|
)
|
||||||
|
const xAxisIntersection = args.intersects.find(
|
||||||
|
(sceneObject) => sceneObject.object.name === X_AXIS
|
||||||
|
)
|
||||||
|
|
||||||
|
const snappedClickPoint = {
|
||||||
|
x: yAxisIntersection ? 0 : intersectionPoint.twoD.x,
|
||||||
|
y: xAxisIntersection ? 0 : intersectionPoint.twoD.y,
|
||||||
|
}
|
||||||
|
|
||||||
const addStartProfileAtRes = addStartProfileAt(
|
const addStartProfileAtRes = addStartProfileAt(
|
||||||
kclManager.ast,
|
kclManager.ast,
|
||||||
sketchDetails.sketchPathToNode,
|
sketchDetails.sketchPathToNode,
|
||||||
[intersectionPoint.twoD.x, intersectionPoint.twoD.y]
|
[snappedClickPoint.x, snappedClickPoint.y]
|
||||||
)
|
)
|
||||||
|
|
||||||
if (trap(addStartProfileAtRes)) return
|
if (trap(addStartProfileAtRes)) return
|
||||||
@ -358,6 +452,7 @@ export class SceneEntities {
|
|||||||
|
|
||||||
await kclManager.updateAst(modifiedAst, false)
|
await kclManager.updateAst(modifiedAst, false)
|
||||||
this.removeIntersectionPlane()
|
this.removeIntersectionPlane()
|
||||||
|
this.scene.remove(draftPointGroup)
|
||||||
|
|
||||||
// Now perform the caller-specified action
|
// Now perform the caller-specified action
|
||||||
afterClick(args)
|
afterClick(args)
|
||||||
@ -430,12 +525,7 @@ export class SceneEntities {
|
|||||||
const dummy = new Mesh()
|
const dummy = new Mesh()
|
||||||
// TODO: When we actually have sketch positions and rotations we can use them here.
|
// TODO: When we actually have sketch positions and rotations we can use them here.
|
||||||
dummy.position.set(0, 0, 0)
|
dummy.position.set(0, 0, 0)
|
||||||
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
|
const scale = sceneInfra.getClientSceneScaleFactor(dummy)
|
||||||
const factor =
|
|
||||||
(sceneInfra.camControls.camera instanceof OrthographicCamera
|
|
||||||
? orthoFactor
|
|
||||||
: perspScale(sceneInfra.camControls.camera, dummy)) /
|
|
||||||
sceneInfra._baseUnitMultiplier
|
|
||||||
|
|
||||||
const segPathToNode = getNodePathFromSourceRange(
|
const segPathToNode = getNodePathFromSourceRange(
|
||||||
maybeModdedAst,
|
maybeModdedAst,
|
||||||
@ -446,8 +536,9 @@ export class SceneEntities {
|
|||||||
from: sketch.start.from,
|
from: sketch.start.from,
|
||||||
id: sketch.start.__geoMeta.id,
|
id: sketch.start.__geoMeta.id,
|
||||||
pathToNode: segPathToNode,
|
pathToNode: segPathToNode,
|
||||||
scale: factor,
|
scale,
|
||||||
theme: sceneInfra._theme,
|
theme: sceneInfra._theme,
|
||||||
|
isDraft: false,
|
||||||
})
|
})
|
||||||
_profileStart.layers.set(SKETCH_LAYER)
|
_profileStart.layers.set(SKETCH_LAYER)
|
||||||
_profileStart.traverse((child) => {
|
_profileStart.traverse((child) => {
|
||||||
@ -523,7 +614,7 @@ export class SceneEntities {
|
|||||||
id: segment.__geoMeta.id,
|
id: segment.__geoMeta.id,
|
||||||
pathToNode: segPathToNode,
|
pathToNode: segPathToNode,
|
||||||
isDraftSegment,
|
isDraftSegment,
|
||||||
scale: factor,
|
scale,
|
||||||
texture: sceneInfra.extraSegmentTexture,
|
texture: sceneInfra.extraSegmentTexture,
|
||||||
theme: sceneInfra._theme,
|
theme: sceneInfra._theme,
|
||||||
isSelected,
|
isSelected,
|
||||||
@ -660,12 +751,14 @@ export class SceneEntities {
|
|||||||
|
|
||||||
const { intersectionPoint } = args
|
const { intersectionPoint } = args
|
||||||
let intersection2d = intersectionPoint?.twoD
|
let intersection2d = intersectionPoint?.twoD
|
||||||
const profileStart = args.intersects
|
const intersectsProfileStart = args.intersects
|
||||||
.map(({ object }) => getParentGroup(object, [PROFILE_START]))
|
.map(({ object }) => getParentGroup(object, [PROFILE_START]))
|
||||||
.find((a) => a?.name === PROFILE_START)
|
.find((a) => a?.name === PROFILE_START)
|
||||||
|
|
||||||
let modifiedAst
|
let modifiedAst
|
||||||
if (profileStart) {
|
|
||||||
|
// Snapping logic for the profile start handle
|
||||||
|
if (intersectsProfileStart) {
|
||||||
const lastSegment = sketch.paths.slice(-1)[0]
|
const lastSegment = sketch.paths.slice(-1)[0]
|
||||||
modifiedAst = addCallExpressionsToPipe({
|
modifiedAst = addCallExpressionsToPipe({
|
||||||
node: kclManager.ast,
|
node: kclManager.ast,
|
||||||
@ -698,19 +791,39 @@ export class SceneEntities {
|
|||||||
})
|
})
|
||||||
if (trap(modifiedAst)) return Promise.reject(modifiedAst)
|
if (trap(modifiedAst)) return Promise.reject(modifiedAst)
|
||||||
} else if (intersection2d) {
|
} else if (intersection2d) {
|
||||||
|
const intersectsYAxis = args.intersects.find(
|
||||||
|
(sceneObject) => sceneObject.object.name === Y_AXIS
|
||||||
|
)
|
||||||
|
const intersectsXAxis = args.intersects.find(
|
||||||
|
(sceneObject) => sceneObject.object.name === X_AXIS
|
||||||
|
)
|
||||||
|
|
||||||
const lastSegment = sketch.paths.slice(-1)[0]
|
const lastSegment = sketch.paths.slice(-1)[0]
|
||||||
|
const snappedPoint = {
|
||||||
|
x: intersectsYAxis ? 0 : intersection2d.x,
|
||||||
|
y: intersectsXAxis ? 0 : intersection2d.y,
|
||||||
|
}
|
||||||
|
|
||||||
|
let resolvedFunctionName: ToolTip = 'line'
|
||||||
|
|
||||||
|
// This might need to become its own function if we want more
|
||||||
|
// case-based logic for different segment types
|
||||||
|
if (lastSegment.type === 'TangentialArcTo') {
|
||||||
|
resolvedFunctionName = 'tangentialArcTo'
|
||||||
|
} else if (snappedPoint.x === 0 || snappedPoint.y === 0) {
|
||||||
|
// We consider a point placed on axes or origin to be absolute
|
||||||
|
resolvedFunctionName = 'lineTo'
|
||||||
|
}
|
||||||
|
|
||||||
const tmp = addNewSketchLn({
|
const tmp = addNewSketchLn({
|
||||||
node: kclManager.ast,
|
node: kclManager.ast,
|
||||||
programMemory: kclManager.programMemory,
|
programMemory: kclManager.programMemory,
|
||||||
input: {
|
input: {
|
||||||
type: 'straight-segment',
|
type: 'straight-segment',
|
||||||
from: [lastSegment.to[0], lastSegment.to[1]],
|
from: [lastSegment.to[0], lastSegment.to[1]],
|
||||||
to: [intersection2d.x, intersection2d.y],
|
to: [snappedPoint.x, snappedPoint.y],
|
||||||
},
|
},
|
||||||
fnName:
|
fnName: resolvedFunctionName,
|
||||||
lastSegment.type === 'TangentialArcTo'
|
|
||||||
? 'tangentialArcTo'
|
|
||||||
: 'line',
|
|
||||||
pathToNode: sketchPathToNode,
|
pathToNode: sketchPathToNode,
|
||||||
})
|
})
|
||||||
if (trap(tmp)) return Promise.reject(tmp)
|
if (trap(tmp)) return Promise.reject(tmp)
|
||||||
@ -722,7 +835,7 @@ export class SceneEntities {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await kclManager.executeAstMock(modifiedAst)
|
await kclManager.executeAstMock(modifiedAst)
|
||||||
if (profileStart) {
|
if (intersectsProfileStart) {
|
||||||
sceneInfra.modelingSend({ type: 'CancelSketch' })
|
sceneInfra.modelingSend({ type: 'CancelSketch' })
|
||||||
} else {
|
} else {
|
||||||
await this.setUpDraftSegment(
|
await this.setUpDraftSegment(
|
||||||
@ -1229,15 +1342,30 @@ export class SceneEntities {
|
|||||||
variableDeclarationName: string
|
variableDeclarationName: string
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
const profileStart =
|
const intersectsProfileStart =
|
||||||
draftInfo &&
|
draftInfo &&
|
||||||
intersects
|
intersects
|
||||||
.map(({ object }) => getParentGroup(object, [PROFILE_START]))
|
.map(({ object }) => getParentGroup(object, [PROFILE_START]))
|
||||||
.find((a) => a?.name === PROFILE_START)
|
.find((a) => a?.name === PROFILE_START)
|
||||||
const intersection2d = profileStart
|
const intersection2d = intersectsProfileStart
|
||||||
? new Vector2(profileStart.position.x, profileStart.position.y)
|
? new Vector2(
|
||||||
|
intersectsProfileStart.position.x,
|
||||||
|
intersectsProfileStart.position.y
|
||||||
|
)
|
||||||
: _intersection2d
|
: _intersection2d
|
||||||
|
|
||||||
|
const intersectsYAxis = intersects.find(
|
||||||
|
(sceneObject) => sceneObject.object.name === Y_AXIS
|
||||||
|
)
|
||||||
|
const intersectsXAxis = intersects.find(
|
||||||
|
(sceneObject) => sceneObject.object.name === X_AXIS
|
||||||
|
)
|
||||||
|
|
||||||
|
const snappedPoint = new Vector2(
|
||||||
|
intersectsYAxis ? 0 : intersection2d.x,
|
||||||
|
intersectsXAxis ? 0 : intersection2d.y
|
||||||
|
)
|
||||||
|
|
||||||
const group = getParentGroup(object, SEGMENT_BODIES_PLUS_PROFILE_START)
|
const group = getParentGroup(object, SEGMENT_BODIES_PLUS_PROFILE_START)
|
||||||
const subGroup = getParentGroup(object, [ARROWHEAD, CIRCLE_CENTER_HANDLE])
|
const subGroup = getParentGroup(object, [ARROWHEAD, CIRCLE_CENTER_HANDLE])
|
||||||
if (!group) return
|
if (!group) return
|
||||||
@ -1257,7 +1385,7 @@ export class SceneEntities {
|
|||||||
group.userData.from[0],
|
group.userData.from[0],
|
||||||
group.userData.from[1],
|
group.userData.from[1],
|
||||||
]
|
]
|
||||||
const dragTo: [number, number] = [intersection2d.x, intersection2d.y]
|
const dragTo: [number, number] = [snappedPoint.x, snappedPoint.y]
|
||||||
let modifiedAst = draftInfo ? draftInfo.truncatedAst : { ...kclManager.ast }
|
let modifiedAst = draftInfo ? draftInfo.truncatedAst : { ...kclManager.ast }
|
||||||
|
|
||||||
const _node = getNodeFromPath<Node<CallExpression>>(
|
const _node = getNodeFromPath<Node<CallExpression>>(
|
||||||
|
@ -30,6 +30,7 @@ import { MouseState, SegmentOverlayPayload } from 'machines/modelingMachine'
|
|||||||
import { getAngle, throttle } from 'lib/utils'
|
import { getAngle, throttle } from 'lib/utils'
|
||||||
import { Themes } from 'lib/theme'
|
import { Themes } from 'lib/theme'
|
||||||
import { CSS2DRenderer } from 'three/examples/jsm/renderers/CSS2DRenderer'
|
import { CSS2DRenderer } from 'three/examples/jsm/renderers/CSS2DRenderer'
|
||||||
|
import { orthoScale, perspScale } from './helpers'
|
||||||
|
|
||||||
type SendType = ReturnType<typeof useModelingContext>['send']
|
type SendType = ReturnType<typeof useModelingContext>['send']
|
||||||
|
|
||||||
@ -49,6 +50,10 @@ export const RAYCASTABLE_PLANE = 'raycastable-plane'
|
|||||||
|
|
||||||
export const X_AXIS = 'xAxis'
|
export const X_AXIS = 'xAxis'
|
||||||
export const Y_AXIS = 'yAxis'
|
export const Y_AXIS = 'yAxis'
|
||||||
|
/** the THREEjs representation of the group surrounding a "snapped" point that is not yet placed */
|
||||||
|
export const DRAFT_POINT_GROUP = 'draft-point-group'
|
||||||
|
/** the THREEjs representation of a "snapped" point that is not yet placed */
|
||||||
|
export const DRAFT_POINT = 'draft-point'
|
||||||
export const AXIS_GROUP = 'axisGroup'
|
export const AXIS_GROUP = 'axisGroup'
|
||||||
export const SKETCH_GROUP_SEGMENTS = 'sketch-group-segments'
|
export const SKETCH_GROUP_SEGMENTS = 'sketch-group-segments'
|
||||||
export const ARROWHEAD = 'arrowhead'
|
export const ARROWHEAD = 'arrowhead'
|
||||||
@ -60,6 +65,11 @@ export interface OnMouseEnterLeaveArgs {
|
|||||||
selected: Object3D<Object3DEventMap>
|
selected: Object3D<Object3DEventMap>
|
||||||
dragSelected?: Object3D<Object3DEventMap>
|
dragSelected?: Object3D<Object3DEventMap>
|
||||||
mouseEvent: MouseEvent
|
mouseEvent: MouseEvent
|
||||||
|
/** The intersection of the mouse with the THREEjs raycast plane */
|
||||||
|
intersectionPoint?: {
|
||||||
|
twoD?: Vector2
|
||||||
|
threeD?: Vector3
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OnDragCallbackArgs extends OnMouseEnterLeaveArgs {
|
interface OnDragCallbackArgs extends OnMouseEnterLeaveArgs {
|
||||||
@ -348,29 +358,42 @@ export class SceneInfra {
|
|||||||
window.removeEventListener('resize', this.onWindowResize)
|
window.removeEventListener('resize', this.onWindowResize)
|
||||||
// Dispose of any other resources like geometries, materials, textures
|
// Dispose of any other resources like geometries, materials, textures
|
||||||
}
|
}
|
||||||
|
getClientSceneScaleFactor(meshOrGroup: Mesh | Group) {
|
||||||
|
const orthoFactor = orthoScale(this.camControls.camera)
|
||||||
|
const factor =
|
||||||
|
(this.camControls.camera instanceof OrthographicCamera
|
||||||
|
? orthoFactor
|
||||||
|
: perspScale(this.camControls.camera, meshOrGroup)) /
|
||||||
|
this._baseUnitMultiplier
|
||||||
|
return factor
|
||||||
|
}
|
||||||
getPlaneIntersectPoint = (): {
|
getPlaneIntersectPoint = (): {
|
||||||
twoD?: Vector2
|
twoD?: Vector2
|
||||||
threeD?: Vector3
|
threeD?: Vector3
|
||||||
intersection: Intersection<Object3D<Object3DEventMap>>
|
intersection: Intersection<Object3D<Object3DEventMap>>
|
||||||
} | null => {
|
} | null => {
|
||||||
|
// Get the orientations from the camera and mouse position
|
||||||
this.planeRaycaster.setFromCamera(
|
this.planeRaycaster.setFromCamera(
|
||||||
this.currentMouseVector,
|
this.currentMouseVector,
|
||||||
this.camControls.camera
|
this.camControls.camera
|
||||||
)
|
)
|
||||||
|
// Get the intersection of the ray with the default planes
|
||||||
const planeIntersects = this.planeRaycaster.intersectObjects(
|
const planeIntersects = this.planeRaycaster.intersectObjects(
|
||||||
this.scene.children,
|
this.scene.children,
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
const recastablePlaneIntersect = planeIntersects.find(
|
if (!planeIntersects.length) return null
|
||||||
|
|
||||||
|
// Find the intersection with the raycastable (or sketch) plane
|
||||||
|
const raycastablePlaneIntersection = planeIntersects.find(
|
||||||
(intersect) => intersect.object.name === RAYCASTABLE_PLANE
|
(intersect) => intersect.object.name === RAYCASTABLE_PLANE
|
||||||
)
|
)
|
||||||
if (!planeIntersects.length) return null
|
if (!raycastablePlaneIntersection)
|
||||||
if (!recastablePlaneIntersect) return { intersection: planeIntersects[0] }
|
return { intersection: planeIntersects[0] }
|
||||||
const planePosition = planeIntersects[0].object.position
|
const planePosition = raycastablePlaneIntersection.object.position
|
||||||
const inversePlaneQuaternion = planeIntersects[0].object.quaternion
|
const inversePlaneQuaternion =
|
||||||
.clone()
|
raycastablePlaneIntersection.object.quaternion.clone().invert()
|
||||||
.invert()
|
const intersectPoint = raycastablePlaneIntersection.point
|
||||||
const intersectPoint = planeIntersects[0].point
|
|
||||||
let transformedPoint = intersectPoint.clone()
|
let transformedPoint = intersectPoint.clone()
|
||||||
if (transformedPoint) {
|
if (transformedPoint) {
|
||||||
transformedPoint.applyQuaternion(inversePlaneQuaternion)
|
transformedPoint.applyQuaternion(inversePlaneQuaternion)
|
||||||
@ -447,18 +470,26 @@ export class SceneInfra {
|
|||||||
|
|
||||||
if (intersects[0]) {
|
if (intersects[0]) {
|
||||||
const firstIntersectObject = intersects[0].object
|
const firstIntersectObject = intersects[0].object
|
||||||
|
const planeIntersectPoint = this.getPlaneIntersectPoint()
|
||||||
|
const intersectionPoint = {
|
||||||
|
twoD: planeIntersectPoint?.twoD,
|
||||||
|
threeD: planeIntersectPoint?.threeD,
|
||||||
|
}
|
||||||
|
|
||||||
if (this.hoveredObject !== firstIntersectObject) {
|
if (this.hoveredObject !== firstIntersectObject) {
|
||||||
const hoveredObj = this.hoveredObject
|
const hoveredObj = this.hoveredObject
|
||||||
this.hoveredObject = null
|
this.hoveredObject = null
|
||||||
await this.onMouseLeave({
|
await this.onMouseLeave({
|
||||||
selected: hoveredObj,
|
selected: hoveredObj,
|
||||||
mouseEvent: mouseEvent,
|
mouseEvent: mouseEvent,
|
||||||
|
intersectionPoint,
|
||||||
})
|
})
|
||||||
this.hoveredObject = firstIntersectObject
|
this.hoveredObject = firstIntersectObject
|
||||||
await this.onMouseEnter({
|
await this.onMouseEnter({
|
||||||
selected: this.hoveredObject,
|
selected: this.hoveredObject,
|
||||||
dragSelected: this.selected?.object,
|
dragSelected: this.selected?.object,
|
||||||
mouseEvent: mouseEvent,
|
mouseEvent: mouseEvent,
|
||||||
|
intersectionPoint,
|
||||||
})
|
})
|
||||||
if (!this.selected)
|
if (!this.selected)
|
||||||
this.updateMouseState({
|
this.updateMouseState({
|
||||||
|
@ -45,6 +45,7 @@ import {
|
|||||||
import { getTangentPointFromPreviousArc } from 'lib/utils2d'
|
import { getTangentPointFromPreviousArc } from 'lib/utils2d'
|
||||||
import {
|
import {
|
||||||
ARROWHEAD,
|
ARROWHEAD,
|
||||||
|
DRAFT_POINT,
|
||||||
SceneInfra,
|
SceneInfra,
|
||||||
SEGMENT_LENGTH_LABEL,
|
SEGMENT_LENGTH_LABEL,
|
||||||
SEGMENT_LENGTH_LABEL_OFFSET_PX,
|
SEGMENT_LENGTH_LABEL_OFFSET_PX,
|
||||||
@ -686,19 +687,20 @@ class CircleSegment implements SegmentUtils {
|
|||||||
|
|
||||||
export function createProfileStartHandle({
|
export function createProfileStartHandle({
|
||||||
from,
|
from,
|
||||||
id,
|
isDraft = false,
|
||||||
pathToNode,
|
|
||||||
scale = 1,
|
scale = 1,
|
||||||
theme,
|
theme,
|
||||||
isSelected,
|
isSelected,
|
||||||
|
...rest
|
||||||
}: {
|
}: {
|
||||||
from: Coords2d
|
from: Coords2d
|
||||||
id: string
|
|
||||||
pathToNode: PathToNode
|
|
||||||
scale?: number
|
scale?: number
|
||||||
theme: Themes
|
theme: Themes
|
||||||
isSelected?: boolean
|
isSelected?: boolean
|
||||||
}) {
|
} & (
|
||||||
|
| { isDraft: true }
|
||||||
|
| { isDraft: false; id: string; pathToNode: PathToNode }
|
||||||
|
)) {
|
||||||
const group = new Group()
|
const group = new Group()
|
||||||
|
|
||||||
const geometry = new BoxGeometry(12, 12, 12) // in pixels scaled later
|
const geometry = new BoxGeometry(12, 12, 12) // in pixels scaled later
|
||||||
@ -711,13 +713,12 @@ export function createProfileStartHandle({
|
|||||||
|
|
||||||
group.userData = {
|
group.userData = {
|
||||||
type: PROFILE_START,
|
type: PROFILE_START,
|
||||||
id,
|
|
||||||
from,
|
from,
|
||||||
pathToNode,
|
|
||||||
isSelected,
|
isSelected,
|
||||||
baseColor,
|
baseColor,
|
||||||
|
...rest,
|
||||||
}
|
}
|
||||||
group.name = PROFILE_START
|
group.name = isDraft ? DRAFT_POINT : PROFILE_START
|
||||||
group.position.set(from[0], from[1], 0)
|
group.position.set(from[0], from[1], 0)
|
||||||
group.scale.set(scale, scale, scale)
|
group.scale.set(scale, scale, scale)
|
||||||
return group
|
return group
|
||||||
|
@ -684,6 +684,14 @@ const transformMap: TransformMap = {
|
|||||||
tag
|
tag
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
xAbs: {
|
||||||
|
tooltip: 'lineTo',
|
||||||
|
createNode: setAbsDistanceCreateNode('x'),
|
||||||
|
},
|
||||||
|
yAbs: {
|
||||||
|
tooltip: 'lineTo',
|
||||||
|
createNode: setAbsDistanceCreateNode('y'),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
xAbsolute: {
|
xAbsolute: {
|
||||||
equalLength: {
|
equalLength: {
|
||||||
|
Reference in New Issue
Block a user