Compare commits
7 Commits
v0.19.2
...
url-scheme
Author | SHA1 | Date | |
---|---|---|---|
a21c2c50b3 | |||
aa52407fda | |||
e45be831d0 | |||
005944f3a3 | |||
755ef8ce7f | |||
005d1f0ca7 | |||
e158f6f513 |
2
.github/workflows/build-and-store-wasm.yml
vendored
2
.github/workflows/build-and-store-wasm.yml
vendored
@ -16,8 +16,6 @@ jobs:
|
||||
cache: 'yarn'
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
- name: Install Playwright Browsers
|
||||
run: yarn playwright install --with-deps
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Cache wasm
|
||||
|
80
.github/workflows/playwright.yml
vendored
80
.github/workflows/playwright.yml
vendored
@ -12,11 +12,31 @@ concurrency:
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
actions: read
|
||||
|
||||
|
||||
jobs:
|
||||
|
||||
check-rust-changes:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
rust-changed: ${{ steps.filter.outputs.rust }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- id: filter
|
||||
name: Check for Rust changes
|
||||
uses: dorny/paths-filter@v2
|
||||
with:
|
||||
filters: |
|
||||
rust:
|
||||
- 'src/wasm-lib/**'
|
||||
|
||||
playwright-ubuntu:
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
needs: check-rust-changes
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
@ -28,13 +48,39 @@ jobs:
|
||||
run: yarn
|
||||
- name: Install Playwright Browsers
|
||||
run: yarn playwright install --with-deps
|
||||
- name: download wasm
|
||||
id: download-wasm
|
||||
if: needs.check-rust-changes.outputs.rust-changed == 'false'
|
||||
uses: actions/download-artifact@v2
|
||||
continue-on-error: true
|
||||
with:
|
||||
name: wasm-bundle
|
||||
path: src/wasm-lib/pkg
|
||||
- name: copy wasm blob
|
||||
if: needs.check-rust-changes.outputs.rust-changed == 'false'
|
||||
run: cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
|
||||
continue-on-error: true
|
||||
- name: Setup Rust
|
||||
if: needs.check-rust-changes.outputs.rust-changed == 'true'
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Setup Rust
|
||||
if: steps.download-wasm.outcome == 'failure'
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Cache wasm
|
||||
if: needs.check-rust-changes.outputs.rust-changed == 'true'
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: './src/wasm-lib'
|
||||
- name: Cache wasm
|
||||
if: steps.download-wasm.outcome == 'failure'
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: './src/wasm-lib'
|
||||
- name: build wasm
|
||||
if: needs.check-rust-changes.outputs.rust-changed == 'true'
|
||||
run: yarn build:wasm
|
||||
- name: build wasm
|
||||
if: steps.download-wasm.outcome == 'failure'
|
||||
run: yarn build:wasm
|
||||
- name: build web
|
||||
run: yarn build:local
|
||||
@ -85,10 +131,16 @@ jobs:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: github.ref == 'refs/heads/main'
|
||||
with:
|
||||
name: wasm-bundle
|
||||
path: src/wasm-lib/pkg
|
||||
|
||||
playwright-macos:
|
||||
timeout-minutes: 60
|
||||
runs-on: macos-14
|
||||
needs: check-rust-changes
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
@ -99,13 +151,39 @@ jobs:
|
||||
run: yarn
|
||||
- name: Install Playwright Browsers
|
||||
run: yarn playwright install --with-deps
|
||||
- name: download wasm
|
||||
id: download-wasm
|
||||
if: needs.check-rust-changes.outputs.rust-changed == 'false'
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: wasm-bundle
|
||||
path: src/wasm-lib/pkg
|
||||
continue-on-error: true
|
||||
- name: copy wasm blob
|
||||
if: needs.check-rust-changes.outputs.rust-changed == 'false'
|
||||
run: cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
|
||||
continue-on-error: true
|
||||
- name: Setup Rust
|
||||
if: needs.check-rust-changes.outputs.rust-changed == 'true'
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Setup Rust
|
||||
if: steps.download-wasm.outcome == 'failure'
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Cache wasm
|
||||
if: needs.check-rust-changes.outputs.rust-changed == 'true'
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: './src/wasm-lib'
|
||||
- name: Cache wasm
|
||||
if: steps.download-wasm.outcome == 'failure'
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: './src/wasm-lib'
|
||||
- name: build wasm
|
||||
if: needs.check-rust-changes.outputs.rust-changed == 'true'
|
||||
run: yarn build:wasm
|
||||
- name: build wasm
|
||||
if: steps.download-wasm.outcome == 'failure'
|
||||
run: yarn build:wasm
|
||||
- name: build web
|
||||
run: yarn build:local
|
||||
@ -122,7 +200,7 @@ jobs:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
- uses: actions/upload-artifact@v2
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: github.ref == 'refs/heads/main'
|
||||
with:
|
||||
name: wasm-bundle
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "untitled-app",
|
||||
"version": "0.19.2",
|
||||
"version": "0.19.3",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.16.0",
|
||||
|
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
@ -2418,7 +2418,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kcl-lib"
|
||||
version = "0.1.52"
|
||||
version = "0.1.53"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"approx",
|
||||
|
@ -15,7 +15,7 @@ tauri-build = { version = "2.0.0-beta.13", features = [] }
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
kcl-lib = { version = "0.1.52", path = "../src/wasm-lib/kcl" }
|
||||
kcl-lib = { version = "0.1.53", path = "../src/wasm-lib/kcl" }
|
||||
kittycad = "0.3.0"
|
||||
oauth2 = "4.4.2"
|
||||
serde_json = "1.0"
|
||||
|
@ -11,7 +11,7 @@ use std::{
|
||||
|
||||
use anyhow::Result;
|
||||
use kcl_lib::settings::types::{
|
||||
file::{FileEntry, Project, ProjectState},
|
||||
file::{FileEntry, Project, ProjectRoute, ProjectState},
|
||||
project::ProjectConfiguration,
|
||||
Configuration, DEFAULT_PROJECT_KCL_FILE,
|
||||
};
|
||||
@ -209,6 +209,12 @@ async fn get_project_info(configuration: Configuration, project_path: &str) -> R
|
||||
.map_err(InvokeError::from_anyhow)
|
||||
}
|
||||
|
||||
/// Parse the project route.
|
||||
#[tauri::command]
|
||||
async fn parse_project_route(configuration: Configuration, route: &str) -> Result<ProjectRoute, InvokeError> {
|
||||
ProjectRoute::from_route(&configuration, route).map_err(InvokeError::from_anyhow)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn read_dir_recursive(path: &str) -> Result<FileEntry, InvokeError> {
|
||||
kcl_lib::settings::utils::walk_dir(&Path::new(path).to_path_buf())
|
||||
@ -346,17 +352,6 @@ fn show_in_folder(path: &str) -> Result<(), InvokeError> {
|
||||
|
||||
fn main() -> Result<()> {
|
||||
tauri::Builder::default()
|
||||
.setup(|_app| {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
_app.get_webview("main").unwrap().open_devtools();
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
_app.handle().plugin(tauri_plugin_updater::Builder::new().build())?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
get_state,
|
||||
set_state,
|
||||
@ -365,6 +360,7 @@ fn main() -> Result<()> {
|
||||
create_new_project_directory,
|
||||
list_projects,
|
||||
get_project_info,
|
||||
parse_project_route,
|
||||
get_user,
|
||||
login,
|
||||
read_dir_recursive,
|
||||
@ -376,6 +372,16 @@ fn main() -> Result<()> {
|
||||
])
|
||||
.plugin(tauri_plugin_cli::init())
|
||||
.setup(|app| {
|
||||
// Do update things.
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
app.get_webview("main").unwrap().open_devtools();
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
app.handle().plugin(tauri_plugin_updater::Builder::new().build())?;
|
||||
}
|
||||
|
||||
let mut verbose = false;
|
||||
let mut source_path: Option<PathBuf> = None;
|
||||
match app.cli().matches() {
|
||||
@ -423,6 +429,14 @@ fn main() -> Result<()> {
|
||||
|
||||
let runner: tauri::async_runtime::JoinHandle<Result<ProjectState>> =
|
||||
tauri::async_runtime::spawn(async move {
|
||||
// Fix for "." path, which is the current directory.
|
||||
let source_path = if source_path == Path::new(".") {
|
||||
std::env::current_dir()
|
||||
.map_err(|e| anyhow::anyhow!("Error getting the current directory: {:?}", e))?
|
||||
} else {
|
||||
source_path
|
||||
};
|
||||
|
||||
// If the path is a directory, let's assume it is a project directory.
|
||||
if source_path.is_dir() {
|
||||
// Load the details about the project from the path.
|
||||
@ -481,6 +495,13 @@ fn main() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.register_uri_scheme_protocol("zoo-modeling-app", |_app, request| {
|
||||
let path = request.uri().path();
|
||||
dbg!(path);
|
||||
println!("Requesting path: {}", path);
|
||||
|
||||
tauri::http::Response::builder().status(200).body(b"{}").unwrap()
|
||||
})
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
.plugin(tauri_plugin_http::init())
|
||||
|
@ -71,5 +71,5 @@
|
||||
}
|
||||
},
|
||||
"productName": "Zoo Modeling App",
|
||||
"version": "0.19.2"
|
||||
"version": "0.19.3"
|
||||
}
|
||||
|
@ -59,7 +59,6 @@ const router = createBrowserRouter([
|
||||
const appState = await getState()
|
||||
|
||||
if (appState) {
|
||||
console.log('appState', appState)
|
||||
// Reset the state.
|
||||
// We do this so that we load the initial state from the cli but everything
|
||||
// else we can ignore.
|
||||
|
@ -56,6 +56,7 @@ import toast from 'react-hot-toast'
|
||||
import { EditorSelection } from '@uiw/react-codemirror'
|
||||
import { CoreDumpManager } from 'lib/coredump'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
|
||||
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
@ -84,7 +85,12 @@ export const ModelingMachineProvider = ({
|
||||
} = useSettingsAuthContext()
|
||||
const token = auth?.context?.token
|
||||
const streamRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
let [searchParams] = useSearchParams()
|
||||
const pool = searchParams.get('pool')
|
||||
|
||||
useSetupEngineManager(streamRef, token, {
|
||||
pool: pool,
|
||||
theme: theme.current,
|
||||
highlightEdges: highlightEdges.current,
|
||||
enableSSAO: enableSSAO.current,
|
||||
|
@ -9,10 +9,12 @@ export function useSetupEngineManager(
|
||||
streamRef: React.RefObject<HTMLDivElement>,
|
||||
token?: string,
|
||||
settings = {
|
||||
pool: null,
|
||||
theme: Themes.System,
|
||||
highlightEdges: true,
|
||||
enableSSAO: true,
|
||||
} as {
|
||||
pool: string | null
|
||||
theme: Themes
|
||||
highlightEdges: boolean
|
||||
enableSSAO: boolean
|
||||
@ -35,6 +37,12 @@ export function useSetupEngineManager(
|
||||
|
||||
const hasSetNonZeroDimensions = useRef<boolean>(false)
|
||||
|
||||
if (settings.pool) {
|
||||
// override the pool param (?pool=) to request a specific engine instance
|
||||
// from a particular pool.
|
||||
engineCommandManager.pool = settings.pool
|
||||
}
|
||||
|
||||
useLayoutEffect(() => {
|
||||
// Load the engine command manager once with the initial width and height,
|
||||
// then we do not want to reload it.
|
||||
|
@ -888,6 +888,7 @@ export class EngineCommandManager {
|
||||
sceneCommandArtifacts: ArtifactMap = {}
|
||||
outSequence = 1
|
||||
inSequence = 1
|
||||
pool?: string
|
||||
engineConnection?: EngineConnection
|
||||
defaultPlanes: DefaultPlanes | null = null
|
||||
commandLogs: CommandLog[] = []
|
||||
@ -914,8 +915,9 @@ export class EngineCommandManager {
|
||||
callbacksEngineStateConnection: ((state: EngineConnectionState) => void)[] =
|
||||
[]
|
||||
|
||||
constructor() {
|
||||
constructor(pool?: string) {
|
||||
this.engineConnection = undefined
|
||||
this.pool = pool
|
||||
}
|
||||
|
||||
private _camControlsCameraChange = () => {}
|
||||
@ -972,7 +974,8 @@ export class EngineCommandManager {
|
||||
}
|
||||
|
||||
const additionalSettings = settings.enableSSAO ? '&post_effect=ssao' : ''
|
||||
const url = `${VITE_KC_API_WS_MODELING_URL}?video_res_width=${width}&video_res_height=${height}${additionalSettings}`
|
||||
const pool = this.pool == undefined ? '' : `&pool=${this.pool}`
|
||||
const url = `${VITE_KC_API_WS_MODELING_URL}?video_res_width=${width}&video_res_height=${height}${additionalSettings}${pool}`
|
||||
this.engineConnection = new EngineConnection({
|
||||
engineCommandManager: this,
|
||||
url,
|
||||
|
@ -14,6 +14,7 @@ import init, {
|
||||
parse_app_settings,
|
||||
parse_project_settings,
|
||||
default_project_settings,
|
||||
parse_project_route,
|
||||
} from '../wasm-lib/pkg/wasm_lib'
|
||||
import { KCLError } from './errors'
|
||||
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
|
||||
@ -31,6 +32,7 @@ import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
|
||||
import { TEST } from 'env'
|
||||
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
|
||||
import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration'
|
||||
import { ProjectRoute } from 'wasm-lib/kcl/bindings/ProjectRoute'
|
||||
|
||||
export type { Program } from '../wasm-lib/kcl/bindings/Program'
|
||||
export type { Value } from '../wasm-lib/kcl/bindings/Value'
|
||||
@ -389,3 +391,18 @@ export function parseProjectSettings(toml: string): ProjectConfiguration {
|
||||
throw new Error(`Error parsing project settings: ${e}`)
|
||||
}
|
||||
}
|
||||
|
||||
export function parseProjectRoute(
|
||||
configuration: Configuration,
|
||||
route_str: string
|
||||
): ProjectRoute {
|
||||
try {
|
||||
const route: ProjectRoute = parse_project_route(
|
||||
JSON.stringify(configuration),
|
||||
route_str
|
||||
)
|
||||
return route
|
||||
} catch (e: any) {
|
||||
throw new Error(`Error parsing project route: ${e}`)
|
||||
}
|
||||
}
|
||||
|
@ -49,6 +49,11 @@ export class CoreDumpManager {
|
||||
return APP_VERSION
|
||||
}
|
||||
|
||||
// Get the backend pool we've requested.
|
||||
pool(): string {
|
||||
return this.engineCommandManager.pool || ''
|
||||
}
|
||||
|
||||
// Get the os information.
|
||||
getOsInfo(): Promise<string> {
|
||||
if (this.isTauri()) {
|
||||
|
@ -1,7 +1,11 @@
|
||||
import { sep } from '@tauri-apps/api/path'
|
||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||
import { BROWSER_FILE_NAME, BROWSER_PROJECT_NAME, FILE_EXT } from './constants'
|
||||
import { isTauri } from './isTauri'
|
||||
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
|
||||
import { ProjectRoute } from 'wasm-lib/kcl/bindings/ProjectRoute'
|
||||
import { parseProjectRoute, readAppSettingsFile } from './tauri'
|
||||
import { parseProjectRoute as parseProjectRouteWasm } from 'lang/wasm'
|
||||
import { readLocalStorageAppSettingsFile } from './settings/settingsUtils'
|
||||
|
||||
const prependRoutes =
|
||||
(routesObject: Record<string, string>) => (prepend: string) => {
|
||||
@ -25,28 +29,23 @@ export const paths = {
|
||||
} as const
|
||||
export const BROWSER_PATH = `%2F${BROWSER_PROJECT_NAME}%2F${BROWSER_FILE_NAME}${FILE_EXT}`
|
||||
|
||||
export function getProjectMetaByRouteId(id?: string, defaultDir = '') {
|
||||
export async function getProjectMetaByRouteId(
|
||||
id?: string,
|
||||
configuration?: Configuration
|
||||
): Promise<ProjectRoute | undefined> {
|
||||
if (!id) return undefined
|
||||
const s = isTauri() ? sep() : '/'
|
||||
|
||||
const decodedId = decodeURIComponent(id).replace(/\/$/, '') // remove trailing slash
|
||||
const projectAndFile =
|
||||
defaultDir === '/'
|
||||
? decodedId.replace(defaultDir, '')
|
||||
: decodedId.replace(defaultDir + s, '')
|
||||
const filePathParts = projectAndFile.split(s)
|
||||
const projectName = filePathParts[0]
|
||||
const projectPath =
|
||||
(defaultDir === '/' ? defaultDir : defaultDir + s) + projectName
|
||||
const lastPathPart = filePathParts[filePathParts.length - 1]
|
||||
const currentFileName =
|
||||
lastPathPart === projectName ? undefined : lastPathPart
|
||||
const currentFilePath = lastPathPart === projectName ? undefined : decodedId
|
||||
const inTauri = isTauri()
|
||||
|
||||
return {
|
||||
projectName,
|
||||
projectPath,
|
||||
currentFileName,
|
||||
currentFilePath,
|
||||
if (!configuration) {
|
||||
configuration = inTauri
|
||||
? await readAppSettingsFile()
|
||||
: readLocalStorageAppSettingsFile()
|
||||
}
|
||||
|
||||
const route = inTauri
|
||||
? await parseProjectRoute(configuration, id)
|
||||
: parseProjectRouteWasm(configuration, id)
|
||||
|
||||
return route
|
||||
}
|
||||
|
@ -28,16 +28,18 @@ export const settingsLoader: LoaderFunction = async ({
|
||||
}): Promise<
|
||||
ReturnType<typeof createSettings> | ReturnType<typeof redirect>
|
||||
> => {
|
||||
let { settings } = await loadAndValidateSettings()
|
||||
let { settings, configuration } = await loadAndValidateSettings()
|
||||
|
||||
// I don't love that we have to read the settings again here,
|
||||
// but we need to get the project path to load the project settings
|
||||
if (params.id) {
|
||||
const defaultDir = settings.app.projectDirectory.current || ''
|
||||
const projectPathData = getProjectMetaByRouteId(params.id, defaultDir)
|
||||
const projectPathData = await getProjectMetaByRouteId(
|
||||
params.id,
|
||||
configuration
|
||||
)
|
||||
if (projectPathData) {
|
||||
const { projectName } = projectPathData
|
||||
const { settings: s } = await loadAndValidateSettings(projectName)
|
||||
const { project_name } = projectPathData
|
||||
const { settings: s } = await loadAndValidateSettings(project_name)
|
||||
settings = s
|
||||
}
|
||||
}
|
||||
@ -71,17 +73,19 @@ export const onboardingRedirectLoader: ActionFunction = async (args) => {
|
||||
export const fileLoader: LoaderFunction = async ({
|
||||
params,
|
||||
}): Promise<FileLoaderData | Response> => {
|
||||
let { settings } = await loadAndValidateSettings()
|
||||
let { configuration } = await loadAndValidateSettings()
|
||||
|
||||
const defaultDir = settings.app.projectDirectory.current || '/'
|
||||
const projectPathData = getProjectMetaByRouteId(params.id, defaultDir)
|
||||
const projectPathData = await getProjectMetaByRouteId(
|
||||
params.id,
|
||||
configuration
|
||||
)
|
||||
const isBrowserProject = params.id === decodeURIComponent(BROWSER_PATH)
|
||||
|
||||
if (!isBrowserProject && projectPathData) {
|
||||
const { projectName, projectPath, currentFileName, currentFilePath } =
|
||||
const { project_name, project_path, current_file_name, current_file_path } =
|
||||
projectPathData
|
||||
|
||||
if (!currentFileName || !currentFilePath) {
|
||||
if (!current_file_name || !current_file_path || !project_name) {
|
||||
return redirect(
|
||||
`${paths.FILE}/${encodeURIComponent(
|
||||
`${params.id}${isTauri() ? sep() : '/'}${PROJECT_ENTRYPOINT}`
|
||||
@ -91,33 +95,33 @@ export const fileLoader: LoaderFunction = async ({
|
||||
|
||||
// TODO: PROJECT_ENTRYPOINT is hardcoded
|
||||
// until we support setting a project's entrypoint file
|
||||
const code = await readTextFile(currentFilePath)
|
||||
const code = await readTextFile(current_file_path)
|
||||
|
||||
// Update both the state and the editor's code.
|
||||
// We explicitly do not write to the file here since we are loading from
|
||||
// the file system and not the editor.
|
||||
codeManager.updateCurrentFilePath(currentFilePath)
|
||||
codeManager.updateCurrentFilePath(current_file_path)
|
||||
codeManager.updateCodeStateEditor(code)
|
||||
kclManager.executeCode(true)
|
||||
|
||||
// Set the file system manager to the project path
|
||||
// So that WASM gets an updated path for operations
|
||||
fileSystemManager.dir = projectPath
|
||||
fileSystemManager.dir = project_path
|
||||
|
||||
const projectData: IndexLoaderData = {
|
||||
code,
|
||||
project: isTauri()
|
||||
? await getProjectInfo(projectPath)
|
||||
? await getProjectInfo(project_path, configuration)
|
||||
: {
|
||||
name: projectName,
|
||||
path: projectPath,
|
||||
name: project_name,
|
||||
path: project_path,
|
||||
children: [],
|
||||
kcl_file_count: 0,
|
||||
directory_count: 0,
|
||||
},
|
||||
file: {
|
||||
name: currentFileName,
|
||||
path: currentFilePath,
|
||||
name: current_file_name,
|
||||
path: current_file_path,
|
||||
children: [],
|
||||
},
|
||||
}
|
||||
|
@ -101,7 +101,7 @@ function localStorageProjectSettingsPath() {
|
||||
return '/' + BROWSER_PROJECT_NAME + '/project.toml'
|
||||
}
|
||||
|
||||
function readLocalStorageAppSettingsFile(): Configuration {
|
||||
export function readLocalStorageAppSettingsFile(): Configuration {
|
||||
// TODO: Remove backwards compatibility after a few releases.
|
||||
let stored =
|
||||
localStorage.getItem(localStorageAppSettingsPath()) ??
|
||||
|
@ -7,6 +7,7 @@ import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration
|
||||
import { Project } from 'wasm-lib/kcl/bindings/Project'
|
||||
import { FileEntry } from 'wasm-lib/kcl/bindings/FileEntry'
|
||||
import { ProjectState } from 'wasm-lib/kcl/bindings/ProjectState'
|
||||
import { ProjectRoute } from 'wasm-lib/kcl/bindings/ProjectRoute'
|
||||
|
||||
// Get the app state from tauri.
|
||||
export async function getState(): Promise<ProjectState | undefined> {
|
||||
@ -80,6 +81,16 @@ export async function login(host: string): Promise<string> {
|
||||
return await invoke('login', { host })
|
||||
}
|
||||
|
||||
export async function parseProjectRoute(
|
||||
configuration: Configuration,
|
||||
route: string
|
||||
): Promise<ProjectRoute> {
|
||||
return await invoke<ProjectRoute>('parse_project_route', {
|
||||
configuration,
|
||||
route,
|
||||
})
|
||||
}
|
||||
|
||||
export async function getUser(
|
||||
token: string | undefined,
|
||||
host: string
|
||||
|
2
src/wasm-lib/Cargo.lock
generated
2
src/wasm-lib/Cargo.lock
generated
@ -1895,7 +1895,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kcl-lib"
|
||||
version = "0.1.52"
|
||||
version = "0.1.53"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"approx 0.5.1",
|
||||
|
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "kcl-lib"
|
||||
description = "KittyCAD Language implementation and tools"
|
||||
version = "0.1.52"
|
||||
version = "0.1.53"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/KittyCAD/modeling-app"
|
||||
|
@ -33,6 +33,10 @@ impl CoreDump for CoreDumper {
|
||||
Ok(env!("CARGO_PKG_VERSION").to_string())
|
||||
}
|
||||
|
||||
fn pool(&self) -> Result<String> {
|
||||
Ok("".to_owned())
|
||||
}
|
||||
|
||||
async fn os(&self) -> Result<crate::coredump::OsInfo> {
|
||||
Ok(crate::coredump::OsInfo {
|
||||
platform: Some(std::env::consts::OS.to_string()),
|
||||
|
@ -19,6 +19,8 @@ pub trait CoreDump: Clone {
|
||||
|
||||
fn version(&self) -> Result<String>;
|
||||
|
||||
fn pool(&self) -> Result<String>;
|
||||
|
||||
async fn os(&self) -> Result<OsInfo>;
|
||||
|
||||
fn is_tauri(&self) -> Result<bool>;
|
||||
@ -71,6 +73,7 @@ pub trait CoreDump: Clone {
|
||||
os,
|
||||
webrtc_stats,
|
||||
github_issue_url: None,
|
||||
pool: self.pool()?,
|
||||
};
|
||||
app_info.set_github_issue_url(&screenshot_url)?;
|
||||
|
||||
@ -103,6 +106,9 @@ pub struct AppInfo {
|
||||
/// This gets prepoulated with all the core dump info.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub github_issue_url: Option<String>,
|
||||
|
||||
/// Engine pool the client is connected to.
|
||||
pub pool: String,
|
||||
}
|
||||
|
||||
impl AppInfo {
|
||||
|
@ -16,6 +16,9 @@ extern "C" {
|
||||
#[wasm_bindgen(method, js_name = baseApiUrl, catch)]
|
||||
fn baseApiUrl(this: &CoreDumpManager) -> Result<String, js_sys::Error>;
|
||||
|
||||
#[wasm_bindgen(method, js_name = pool, catch)]
|
||||
fn pool(this: &CoreDumpManager) -> Result<String, js_sys::Error>;
|
||||
|
||||
#[wasm_bindgen(method, js_name = version, catch)]
|
||||
fn version(this: &CoreDumpManager) -> Result<String, js_sys::Error>;
|
||||
|
||||
@ -66,6 +69,12 @@ impl CoreDump for CoreDumper {
|
||||
.map_err(|e| anyhow::anyhow!("Failed to get response from version: {:?}", e))
|
||||
}
|
||||
|
||||
fn pool(&self) -> Result<String> {
|
||||
self.manager
|
||||
.pool()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to get response from pool: {:?}", e))
|
||||
}
|
||||
|
||||
async fn os(&self) -> Result<crate::coredump::OsInfo> {
|
||||
let promise = self
|
||||
.manager
|
||||
|
@ -5,6 +5,8 @@ use parse_display::{Display, FromStr};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::Configuration;
|
||||
|
||||
/// State management for the application.
|
||||
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
|
||||
#[ts(export)]
|
||||
@ -14,6 +16,100 @@ pub struct ProjectState {
|
||||
pub current_file: Option<String>,
|
||||
}
|
||||
|
||||
/// Project route information.
|
||||
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct ProjectRoute {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub project_name: Option<String>,
|
||||
pub project_path: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub current_file_name: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub current_file_path: Option<String>,
|
||||
}
|
||||
|
||||
impl ProjectRoute {
|
||||
/// Get the project state from the url in the route.
|
||||
pub fn from_route(configuration: &Configuration, route: &str) -> Result<Self> {
|
||||
let path = std::path::Path::new(route);
|
||||
// Check if the default project path is in the route.
|
||||
let (project_path, project_name) = if path.starts_with(&configuration.settings.project.directory)
|
||||
&& configuration.settings.project.directory != std::path::PathBuf::default()
|
||||
{
|
||||
// Get the project name.
|
||||
if let Some(project_name) = path
|
||||
.strip_prefix(&configuration.settings.project.directory)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.next()
|
||||
{
|
||||
(
|
||||
configuration
|
||||
.settings
|
||||
.project
|
||||
.directory
|
||||
.join(project_name)
|
||||
.display()
|
||||
.to_string(),
|
||||
Some(project_name.to_string_lossy().to_string()),
|
||||
)
|
||||
} else {
|
||||
(configuration.settings.project.directory.display().to_string(), None)
|
||||
}
|
||||
} else {
|
||||
// Assume the project path is the parent directory of the file.
|
||||
let project_dir = if path.display().to_string().ends_with(".kcl") {
|
||||
path.parent()
|
||||
.ok_or_else(|| anyhow::anyhow!("Parent directory not found: {}", path.display()))?
|
||||
} else {
|
||||
path
|
||||
};
|
||||
|
||||
if project_dir == std::path::Path::new("/") {
|
||||
(
|
||||
path.display().to_string(),
|
||||
Some(
|
||||
path.file_name()
|
||||
.ok_or_else(|| anyhow::anyhow!("File name not found: {}", path.display()))?
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
),
|
||||
)
|
||||
} else if let Some(project_name) = project_dir.file_name() {
|
||||
(
|
||||
project_dir.display().to_string(),
|
||||
Some(project_name.to_string_lossy().to_string()),
|
||||
)
|
||||
} else {
|
||||
(project_dir.display().to_string(), None)
|
||||
}
|
||||
};
|
||||
|
||||
let (current_file_name, current_file_path) = if path.display().to_string() == project_path {
|
||||
(None, None)
|
||||
} else {
|
||||
(
|
||||
Some(
|
||||
path.file_name()
|
||||
.ok_or_else(|| anyhow::anyhow!("File name not found: {}", path.display()))?
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
),
|
||||
Some(path.display().to_string()),
|
||||
)
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
project_name,
|
||||
project_path,
|
||||
current_file_name,
|
||||
current_file_path,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about project.
|
||||
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
|
||||
#[ts(export)]
|
||||
@ -233,3 +329,141 @@ impl From<std::fs::Metadata> for FileMetadata {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_project_route_from_route_std_path() {
|
||||
let mut configuration = crate::settings::types::Configuration::default();
|
||||
configuration.settings.project.directory =
|
||||
std::path::PathBuf::from("/Users/macinatormax/Documents/kittycad-modeling-projects");
|
||||
|
||||
let route = "/Users/macinatormax/Documents/kittycad-modeling-projects/assembly/main.kcl";
|
||||
let state = super::ProjectRoute::from_route(&configuration, route).unwrap();
|
||||
assert_eq!(
|
||||
state,
|
||||
super::ProjectRoute {
|
||||
project_name: Some("assembly".to_string()),
|
||||
project_path: "/Users/macinatormax/Documents/kittycad-modeling-projects/assembly".to_string(),
|
||||
current_file_name: Some("main.kcl".to_string()),
|
||||
current_file_path: Some(
|
||||
"/Users/macinatormax/Documents/kittycad-modeling-projects/assembly/main.kcl".to_string()
|
||||
),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_project_route_from_route_std_path_dir() {
|
||||
let mut configuration = crate::settings::types::Configuration::default();
|
||||
configuration.settings.project.directory =
|
||||
std::path::PathBuf::from("/Users/macinatormax/Documents/kittycad-modeling-projects");
|
||||
|
||||
let route = "/Users/macinatormax/Documents/kittycad-modeling-projects/assembly";
|
||||
let state = super::ProjectRoute::from_route(&configuration, route).unwrap();
|
||||
assert_eq!(
|
||||
state,
|
||||
super::ProjectRoute {
|
||||
project_name: Some("assembly".to_string()),
|
||||
project_path: "/Users/macinatormax/Documents/kittycad-modeling-projects/assembly".to_string(),
|
||||
current_file_name: None,
|
||||
current_file_path: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_project_route_from_route_std_path_dir_empty() {
|
||||
let mut configuration = crate::settings::types::Configuration::default();
|
||||
configuration.settings.project.directory =
|
||||
std::path::PathBuf::from("/Users/macinatormax/Documents/kittycad-modeling-projects");
|
||||
|
||||
let route = "/Users/macinatormax/Documents/kittycad-modeling-projects";
|
||||
let state = super::ProjectRoute::from_route(&configuration, route).unwrap();
|
||||
assert_eq!(
|
||||
state,
|
||||
super::ProjectRoute {
|
||||
project_name: None,
|
||||
project_path: "/Users/macinatormax/Documents/kittycad-modeling-projects".to_string(),
|
||||
current_file_name: None,
|
||||
current_file_path: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_project_route_from_route_outside_std_path() {
|
||||
let mut configuration = crate::settings::types::Configuration::default();
|
||||
configuration.settings.project.directory =
|
||||
std::path::PathBuf::from("/Users/macinatormax/Documents/kittycad-modeling-projects");
|
||||
|
||||
let route = "/Users/macinatormax/kittycad/modeling-app/main.kcl";
|
||||
let state = super::ProjectRoute::from_route(&configuration, route).unwrap();
|
||||
assert_eq!(
|
||||
state,
|
||||
super::ProjectRoute {
|
||||
project_name: Some("modeling-app".to_string()),
|
||||
project_path: "/Users/macinatormax/kittycad/modeling-app".to_string(),
|
||||
current_file_name: Some("main.kcl".to_string()),
|
||||
current_file_path: Some("/Users/macinatormax/kittycad/modeling-app/main.kcl".to_string()),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_project_route_from_route_outside_std_path_dir() {
|
||||
let mut configuration = crate::settings::types::Configuration::default();
|
||||
configuration.settings.project.directory =
|
||||
std::path::PathBuf::from("/Users/macinatormax/Documents/kittycad-modeling-projects");
|
||||
|
||||
let route = "/Users/macinatormax/kittycad/modeling-app";
|
||||
let state = super::ProjectRoute::from_route(&configuration, route).unwrap();
|
||||
assert_eq!(
|
||||
state,
|
||||
super::ProjectRoute {
|
||||
project_name: Some("modeling-app".to_string()),
|
||||
project_path: "/Users/macinatormax/kittycad/modeling-app".to_string(),
|
||||
current_file_name: None,
|
||||
current_file_path: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_project_route_from_route_browser() {
|
||||
let mut configuration = crate::settings::types::Configuration::default();
|
||||
configuration.settings.project.directory = std::path::PathBuf::default();
|
||||
|
||||
let route = "/browser/main.kcl";
|
||||
let state = super::ProjectRoute::from_route(&configuration, route).unwrap();
|
||||
assert_eq!(
|
||||
state,
|
||||
super::ProjectRoute {
|
||||
project_name: Some("browser".to_string()),
|
||||
project_path: "/browser".to_string(),
|
||||
current_file_name: Some("main.kcl".to_string()),
|
||||
current_file_path: Some("/browser/main.kcl".to_string()),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_project_route_from_route_browser_no_path() {
|
||||
let mut configuration = crate::settings::types::Configuration::default();
|
||||
configuration.settings.project.directory = std::path::PathBuf::default();
|
||||
|
||||
let route = "/browser";
|
||||
let state = super::ProjectRoute::from_route(&configuration, route).unwrap();
|
||||
assert_eq!(
|
||||
state,
|
||||
super::ProjectRoute {
|
||||
project_name: Some("browser".to_string()),
|
||||
project_path: "/browser".to_string(),
|
||||
current_file_name: None,
|
||||
current_file_path: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -38,6 +38,11 @@ pub async fn walk_dir<P: AsRef<Path> + Send>(dir: P) -> Result<FileEntry> {
|
||||
|
||||
let mut entries = tokio::fs::read_dir(&dir.as_ref()).await?;
|
||||
while let Some(e) = entries.next_entry().await? {
|
||||
// ignore hidden files and directories (starting with a dot)
|
||||
if e.file_name().to_string_lossy().starts_with('.') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if e.file_type().await?.is_dir() {
|
||||
children.push(walk_dir(&e.path()).await?);
|
||||
} else {
|
||||
|
@ -507,3 +507,19 @@ pub fn parse_project_settings(toml_str: &str) -> Result<JsValue, String> {
|
||||
// gloo-serialize crate instead.
|
||||
JsValue::from_serde(&settings).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Parse the project route.
|
||||
#[wasm_bindgen]
|
||||
pub fn parse_project_route(configuration: &str, route: &str) -> Result<JsValue, String> {
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
let configuration: kcl_lib::settings::types::Configuration =
|
||||
serde_json::from_str(configuration).map_err(|e| e.to_string())?;
|
||||
|
||||
let route =
|
||||
kcl_lib::settings::types::file::ProjectRoute::from_route(&configuration, route).map_err(|e| e.to_string())?;
|
||||
|
||||
// The serde-wasm-bindgen does not work here because of weird HashMap issues so we use the
|
||||
// gloo-serialize crate instead.
|
||||
JsValue::from_serde(&route).map_err(|e| e.to_string())
|
||||
}
|
||||
|
Reference in New Issue
Block a user