[Feature]: Initialize engine with view_isometric as a user. (#6604)

* feature: initialize system to view_isometric, playwright(e2e) does zoom_to_fit

* fix: PR fixes

* fix: removing testing code

* fix: I definitely bricked my .spacemacs config and it is using the wrong eslint when I save files :(

* fix: typo

* fix: typoos

* fix: fuking hack

* chore: move exported var from e2e to src then reimport

* fix: got em

* fix: remove console log

* fix: how did this get in that file?? moved it

* fix: location for scene empty check zzz:

* fix: removed debugging code

* fix: forgot the hack for the load view without geometry workflow

* fix: copy
This commit is contained in:
Kevin Nadro
2025-05-01 12:42:44 -05:00
committed by GitHub
parent 89bae66257
commit ed406734a7
11 changed files with 210 additions and 35 deletions

View File

@ -3342,7 +3342,7 @@ profile001 = startProfile(sketch001, at = [-20, 20])
const testPoint = { x: 590, y: 400 } const testPoint = { x: 590, y: 400 }
const extrudeColor: [number, number, number] = [100, 100, 100] const extrudeColor: [number, number, number] = [100, 100, 100]
const sketchColor: [number, number, number] = [140, 140, 140] const sketchColor: [number, number, number] = [140, 140, 140]
const defaultPlaneColor: [number, number, number] = [50, 50, 100] const defaultPlaneColor: [number, number, number] = [88, 44, 45]
const deleteOperation = async (operationButton: Locator) => { const deleteOperation = async (operationButton: Locator) => {
if (shouldUseKeyboard) { if (shouldUseKeyboard) {
@ -3386,7 +3386,11 @@ profile001 = startProfile(sketch001, at = [-20, 20])
) )
await deleteOperation(operationButton) await deleteOperation(operationButton)
await editor.expectEditor.toContain('') await editor.expectEditor.toContain('')
await scene.expectPixelColor(defaultPlaneColor, testPoint, 20) // Cannot use test point anymore because the camera's position has been
// reset and the rest of the test doesn't need to change just to check
// if the scene is cleared.
// Check that the scene is cleared
await scene.expectPixelColor(defaultPlaneColor, { x: 574, y: 342 }, 20)
}) })
}) })
}) })

View File

@ -5,8 +5,6 @@ import { onboardingPaths } from '@src/routes/Onboarding/paths'
import type { Settings } from '@rust/kcl-lib/bindings/Settings' import type { Settings } from '@rust/kcl-lib/bindings/Settings'
export const IS_PLAYWRIGHT_KEY = 'playwright'
export const TEST_SETTINGS_KEY = '/settings.toml' export const TEST_SETTINGS_KEY = '/settings.toml'
export const TEST_SETTINGS: DeepPartial<Settings> = { export const TEST_SETTINGS: DeepPartial<Settings> = {
app: { app: {

View File

@ -18,11 +18,8 @@ import type { ProjectConfiguration } from '@rust/kcl-lib/bindings/ProjectConfigu
import { isErrorWhitelisted } from '@e2e/playwright/lib/console-error-whitelist' import { isErrorWhitelisted } from '@e2e/playwright/lib/console-error-whitelist'
import { secrets } from '@e2e/playwright/secrets' import { secrets } from '@e2e/playwright/secrets'
import { import { TEST_SETTINGS, TEST_SETTINGS_KEY } from '@e2e/playwright/storageStates'
IS_PLAYWRIGHT_KEY, import { IS_PLAYWRIGHT_KEY } from '@src/lib/constants'
TEST_SETTINGS,
TEST_SETTINGS_KEY,
} from '@e2e/playwright/storageStates'
import { test } from '@e2e/playwright/zoo-test' import { test } from '@e2e/playwright/zoo-test'
const toNormalizedCode = (text: string) => { const toNormalizedCode = (text: string) => {

View File

@ -29,6 +29,13 @@ import { useSelector } from '@xstate/react'
import type { MouseEventHandler } from 'react' import type { MouseEventHandler } from 'react'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { useRouteLoaderData } from 'react-router-dom' import { useRouteLoaderData } from 'react-router-dom'
import { isPlaywright } from '@src/lib/isPlaywright'
import {
engineStreamZoomToFit,
engineViewIsometricWithGeometryPresent,
engineViewIsometricWithoutGeometryPresent,
} from '@src/lib/utils'
import { DEFAULT_DEFAULT_LENGTH_UNIT } from '@src/lib/constants'
export const EngineStream = (props: { export const EngineStream = (props: {
pool: string | null pool: string | null
@ -89,21 +96,33 @@ export const EngineStream = (props: {
console.log('scene is ready, fire!') console.log('scene is ready, fire!')
kmp kmp
.then(() => .then(async () => {
// It makes sense to also call zoom to fit here, when a new file is // Gotcha: Playwright E2E tests will be zoom_to_fit, when you try to recreate the e2e test manually
// loaded for the first time, but not overtaking the work kevin did // your localhost will do view_isometric. Turn this boolean on to have the same experience when manually
// so the camera isn't moving all the time. // debugging e2e tests
engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req', // We need a padding of 0.1 for zoom_to_fit for all E2E tests since they were originally
cmd_id: uuidv4(), // written with zoom_to_fit with padding 0.1
cmd: { const padding = 0.1
type: 'zoom_to_fit', if (isPlaywright()) {
object_ids: [], // leave empty to zoom to all objects await engineStreamZoomToFit({ engineCommandManager, padding })
padding: 0.1, // padding around the objects } else {
animated: false, // don't animate the zoom for now // If the scene is empty you cannot use view_isometric, it will not move the camera
}, if (kclManager.isAstBodyEmpty(kclManager.ast)) {
await engineViewIsometricWithoutGeometryPresent({
engineCommandManager,
unit:
kclManager.fileSettings.defaultLengthUnit ||
DEFAULT_DEFAULT_LENGTH_UNIT,
})
} else {
await engineViewIsometricWithGeometryPresent({
engineCommandManager,
padding,
})
}
}
}) })
)
.catch(trap) .catch(trap)
} }

View File

@ -738,6 +738,18 @@ export class KclManager {
return ast.start === 0 && ast.end === 0 && ast.body.length === 0 return ast.start === 0 && ast.end === 0 && ast.body.length === 0
} }
/**
* Determines if there is no code to execute. If there is a @settings annotation
* that adds to the overall ast.start and ast.end but not the body which is the program
*
*
* If you need to know if there is any program code or not, use this function otherwise
* use _isAstEmpty
*/
isAstBodyEmpty(ast: Node<Program>) {
return ast.body.length === 0
}
get fileSettings() { get fileSettings() {
return this._fileSettings return this._fileSettings
} }

View File

@ -187,3 +187,6 @@ export type ExecutionType =
| typeof EXECUTION_TYPE_REAL | typeof EXECUTION_TYPE_REAL
| typeof EXECUTION_TYPE_MOCK | typeof EXECUTION_TYPE_MOCK
| typeof EXECUTION_TYPE_NONE | typeof EXECUTION_TYPE_NONE
/** Key for setting window.localStorage.setItem and .getItem to determine if the runtime is playwright for browsers */
export const IS_PLAYWRIGHT_KEY = 'playwright'

10
src/lib/isPlaywright.ts Normal file
View File

@ -0,0 +1,10 @@
import { IS_PLAYWRIGHT_KEY } from '@src/lib/constants'
export function isPlaywright(): boolean {
// ? checks for browser
const electronRunTimePlaywright =
window?.electron?.process?.env.IS_PLAYWRIGHT == 'true'
const browserRuntimePlaywright =
localStorage.getItem(IS_PLAYWRIGHT_KEY) === 'true'
return electronRunTimePlaywright || browserRuntimePlaywright
}

View File

@ -1,8 +1,6 @@
import type { PlatformPath } from 'path' import type { PlatformPath } from 'path'
import type { Configuration } from '@rust/kcl-lib/bindings/Configuration' import type { Configuration } from '@rust/kcl-lib/bindings/Configuration'
import { IS_PLAYWRIGHT_KEY } from '@src/lib/constants'
import { IS_PLAYWRIGHT_KEY } from '@e2e/playwright/storageStates'
import { import {
BROWSER_FILE_NAME, BROWSER_FILE_NAME,

View File

@ -6,6 +6,14 @@ import type { CallExpressionKw, SourceRange } from '@src/lang/wasm'
import { isDesktop } from '@src/lib/isDesktop' import { isDesktop } from '@src/lib/isDesktop'
import type { AsyncFn } from '@src/lib/types' import type { AsyncFn } from '@src/lib/types'
import * as THREE from 'three'
import type { EngineCommandManager } from '@src/lang/std/engineConnection'
import type {
CameraViewState_type,
UnitLength_type,
} from '@kittycad/lib/dist/types/src/models'
export const uuidv4 = v4 export const uuidv4 = v4
/** /**
@ -533,3 +541,132 @@ export function getInVariableCase(name: string, prefixIfDigit = 'm') {
return likelyPascalCase.slice(0, 1).toLowerCase() + likelyPascalCase.slice(1) return likelyPascalCase.slice(0, 1).toLowerCase() + likelyPascalCase.slice(1)
} }
export function computeIsometricQuaternionForEmptyScene() {
// Create the direction vector you want to look from
const isoDir = new THREE.Vector3(1, 1, 1).normalize() // isometric look direction
// Target is the point you want to look at (e.g., origin)
const target = new THREE.Vector3(0, 0, 0)
// Compute quaternion for isometric view
const up = new THREE.Vector3(0, 0, 1) // default up direction
const quaternion = new THREE.Quaternion()
quaternion.setFromUnitVectors(new THREE.Vector3(0, 0, 1), isoDir) // align -Z with isoDir
// Align up vector using a lookAt matrix
const m = new THREE.Matrix4()
m.lookAt(new THREE.Vector3().addVectors(target, isoDir), target, up)
quaternion.setFromRotationMatrix(m)
return quaternion
}
export async function engineStreamZoomToFit({
engineCommandManager,
padding,
}: {
engineCommandManager: EngineCommandManager
padding: number
}) {
// It makes sense to also call zoom to fit here, when a new file is
// loaded for the first time, but not overtaking the work kevin did
// so the camera isn't moving all the time.
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'zoom_to_fit',
object_ids: [], // leave empty to zoom to all objects
padding, // padding around the objects
animated: false, // don't animate the zoom for now
},
})
}
export async function engineViewIsometricWithGeometryPresent({
engineCommandManager,
padding,
}: {
engineCommandManager: EngineCommandManager
padding: number
}) {
/**
* Default all users to view_isometric when loading into the engine.
* This works for perspective projection and orthographic projection
* This does not change the projection of the camera only the view direction which makes
* it safe to use with either projection defaulted
*/
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'view_isometric',
padding, // padding around the objects
},
})
/**
* HACK: We need to update the gizmo, the command above doesn't trigger gizmo
* to render which makes the axis point in an old direction.
*/
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
}
export async function engineViewIsometricWithoutGeometryPresent({
engineCommandManager,
unit,
}: {
engineCommandManager: EngineCommandManager
unit?: UnitLength_type
}) {
// If you load an empty scene with any file unit it will have an eye offset of this
const MAGIC_ENGINE_EYE_OFFSET = 1378.0057
const quat = computeIsometricQuaternionForEmptyScene()
const isometricView: CameraViewState_type = {
pivot_rotation: {
x: quat.x,
y: quat.y,
z: quat.z,
w: quat.w,
},
pivot_position: {
x: 0,
y: 0,
z: 0,
},
eye_offset: MAGIC_ENGINE_EYE_OFFSET,
fov_y: 45,
ortho_scale_factor: 1.4063792,
is_ortho: true,
ortho_scale_enabled: true,
world_coord_system: 'right_handed_up_z',
}
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_set_view',
view: {
...isometricView,
},
},
})
/**
* HACK: We need to update the gizmo, the command above doesn't trigger gizmo
* to render which makes the axis point in an old direction.
*/
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
}

View File

@ -2435,7 +2435,9 @@ export const modelingMachine = setup({
'VariableDeclarator' 'VariableDeclarator'
) )
if (err(extrudeNode)) { if (err(extrudeNode)) {
return new Error("Couldn't find extrude node", { cause: extrudeNode }) return new Error("Couldn't find extrude node", {
cause: extrudeNode,
})
} }
// Perform the shell op // Perform the shell op
@ -3326,11 +3328,7 @@ export const modelingMachine = setup({
'Artifact graph emptied': 'hidePlanes', 'Artifact graph emptied': 'hidePlanes',
}, },
entry: [ entry: ['show default planes', 'set selection filter to curves only'],
'show default planes',
'reset camera position',
'set selection filter to curves only',
],
description: `We want to disable selections and hover highlights here, because users can't do anything with that information until they actually add something to the scene. The planes are just for orientation here.`, description: `We want to disable selections and hover highlights here, because users can't do anything with that information until they actually add something to the scene. The planes are just for orientation here.`,
exit: 'set selection filter to defaults', exit: 'set selection filter to defaults',
}, },

View File

@ -1,7 +1,6 @@
import { NODE_ENV } from '@src/env' import { NODE_ENV } from '@src/env'
import { isDesktop } from '@src/lib/isDesktop' import { isDesktop } from '@src/lib/isDesktop'
import { IS_PLAYWRIGHT_KEY } from '@src/lib/constants'
import { IS_PLAYWRIGHT_KEY } from '@e2e/playwright/storageStates'
const isTestEnv = window?.localStorage.getItem(IS_PLAYWRIGHT_KEY) === 'true' const isTestEnv = window?.localStorage.getItem(IS_PLAYWRIGHT_KEY) === 'true'