Merge remote-tracking branch 'origin/main' into jess/changes-import

* origin/main: (26 commits)
  attempt to import win-ca on windows (#6136)
  Upgrade e2e-tests windows runner from 4 cores to 8 (#6166)
  Follow-up fixes after bearing sample rename (#6164)
  Add test for #5799: "Only showing axis planes when there are no errors" (#6007)
  Wait for export button to make test more reliable (#6143)
  sketching on a mirror2d thats been extruded fixed! (#6149)
  Bump vite from 5.4.16 to 5.4.17 in /packages/codemirror-lang-kcl in the security group (#6150)
  Bump vite from 5.4.16 to 5.4.17 in the security group (#6151)
  Update all KCL-Samples to be more ME friendly (#6132)
  Shorten feedback cycle for legitimate failures (#6146)
  Remove the camera projection toggle from the UI (#6077)
  Use all available CPUs to run tests on CI (#6138)
  [fix] Get rid of risky useEffect in restart onboarding flow (#6133)
  Feature: Traditional menu actions in desktop application part II (#6030)
  [Bug] fix some UI friction from imports (#6139)
  Use scene fixture to make test more reliable on macOS (#6140)
  Fix: function composition during playwright setup created a massive page.reload loop (#6137)
  Alternative way to make appMachine spawned children type safe (#5890)
  [BUG] mutate ast to keep comments for pipe split ast-mod (#6128)
  Rename the app to Zoo Design Studio (#5974)
  ...
This commit is contained in:
Jess Frazelle
2025-04-05 09:33:50 -07:00
349 changed files with 50442 additions and 44887 deletions

View File

@ -164,7 +164,7 @@ impl CoreDumpInfo {
![Screenshot]({screenshot_url})
> _Note: If you are capturing from a browser there is limited support for screenshots, only captures the modeling scene.
If you are on MacOS native screenshots may be disabled by default. To enable native screenshots add Zoo Modeling App to System Settings -> Screen & SystemAudio Recording for native screenshots._
If you are on MacOS native screenshots may be disabled by default. To enable native screenshots add Zoo Design Studio to System Settings -> Screen & SystemAudio Recording for native screenshots._
<details>
<summary><b>Core Dump</b></summary>

View File

@ -242,7 +242,7 @@ fn init_handlebars() -> Result<handlebars::Handlebars<'static>> {
out: &mut dyn handlebars::Output|
-> handlebars::HelperResult {
let param = h.param(0).and_then(|v| v.value().as_str()).unwrap_or("");
let basename = param.split('/').last().unwrap_or("");
let basename = param.split('/').next_back().unwrap_or("");
out.write(&format!("`{}`", basename))?;
Ok(())
},

View File

@ -612,7 +612,7 @@ pub fn get_description_string_from_schema(schema: &schemars::schema::RootSchema)
}
if let Some(reference) = &schema.schema.reference {
if let Some(definition) = schema.definitions.get(reference.split('/').last().unwrap_or("")) {
if let Some(definition) = schema.definitions.get(reference.split('/').next_back().unwrap_or("")) {
let schemars::schema::Schema::Object(definition) = definition else {
return None;
};

View File

@ -1,6 +1,6 @@
---
title: "KCL Standard Library"
excerpt: "Documentation for the KCL standard library for the Zoo Modeling App."
excerpt: "Documentation for the KCL standard library for the Zoo Design Studio."
layout: manual
---

View File

@ -3,12 +3,12 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use super::{types::NumericType, ArtifactId, KclValue};
use crate::{docs::StdLibFn, std::get_stdlib_fn, SourceRange};
use crate::{docs::StdLibFn, std::get_stdlib_fn, ModuleId, SourceRange};
/// A CAD modeling operation for display in the feature tree, AKA operations
/// timeline.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[ts(export_to = "Operation.ts")]
#[serde(tag = "type")]
pub enum Operation {
#[serde(rename_all = "camelCase")]
@ -40,7 +40,34 @@ pub enum Operation {
is_error: bool,
},
#[serde(rename_all = "camelCase")]
UserDefinedFunctionCall {
GroupBegin {
/// The details of the group.
group: Group,
/// The source range of the operation in the source code.
source_range: SourceRange,
},
GroupEnd,
}
impl Operation {
/// If the variant is `StdLibCall`, set the `is_error` field.
pub(crate) fn set_std_lib_call_is_error(&mut self, is_err: bool) {
match self {
Self::StdLibCall { ref mut is_error, .. } => *is_error = is_err,
Self::KclStdLibCall { ref mut is_error, .. } => *is_error = is_err,
Self::GroupBegin { .. } | Self::GroupEnd => {}
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export_to = "Operation.ts")]
#[serde(tag = "type")]
#[expect(clippy::large_enum_variant)]
pub enum Group {
/// A function call.
#[serde(rename_all = "camelCase")]
FunctionCall {
/// The name of the user-defined function being called. Anonymous
/// functions have no name.
name: Option<String>,
@ -51,26 +78,20 @@ pub enum Operation {
unlabeled_arg: Option<OpArg>,
/// The labeled keyword arguments to the function.
labeled_args: IndexMap<String, OpArg>,
/// The source range of the operation in the source code.
source_range: SourceRange,
},
UserDefinedFunctionReturn,
}
impl Operation {
/// If the variant is `StdLibCall`, set the `is_error` field.
pub(crate) fn set_std_lib_call_is_error(&mut self, is_err: bool) {
match self {
Self::StdLibCall { ref mut is_error, .. } => *is_error = is_err,
Self::KclStdLibCall { ref mut is_error, .. } => *is_error = is_err,
Self::UserDefinedFunctionCall { .. } | Self::UserDefinedFunctionReturn => {}
}
}
/// A whole-module import use.
#[serde(rename_all = "camelCase")]
ModuleInstance {
/// The name of the module being used.
name: String,
/// The ID of the module which can be used to determine its path.
module_id: ModuleId,
},
}
/// An argument to a CAD modeling operation.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[ts(export_to = "Operation.ts")]
#[serde(rename_all = "camelCase")]
pub struct OpArg {
/// The runtime value of the argument. Instead of using [`KclValue`], we
@ -90,7 +111,7 @@ impl OpArg {
/// A reference to a standard library function. This exists to implement
/// `PartialEq` and `Eq` for `Operation`.
#[derive(Debug, Clone, Deserialize, Serialize, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[ts(export_to = "Operation.ts")]
#[serde(rename_all = "camelCase")]
pub struct StdLibFnRef {
// The following doc comment gets inlined into Operation, overriding what's
@ -154,7 +175,7 @@ fn is_false(b: &bool) -> bool {
/// A KCL value used in Operations. `ArtifactId`s are used to refer to the
/// actual scene objects. Any data not needed in the UI may be omitted.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[ts(export_to = "Operation.ts")]
#[serde(tag = "type")]
pub enum OpKclValue {
Uuid {
@ -212,21 +233,21 @@ pub enum OpKclValue {
pub type OpKclObjectFields = IndexMap<String, OpKclValue>;
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[ts(export_to = "Operation.ts")]
#[serde(rename_all = "camelCase")]
pub struct OpSketch {
artifact_id: ArtifactId,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[ts(export_to = "Operation.ts")]
#[serde(rename_all = "camelCase")]
pub struct OpSolid {
artifact_id: ArtifactId,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[ts(export_to = "Operation.ts")]
#[serde(rename_all = "camelCase")]
pub struct OpHelix {
artifact_id: ArtifactId,

View File

@ -3,7 +3,7 @@ use std::collections::HashMap;
use async_recursion::async_recursion;
use indexmap::IndexMap;
use super::{kcl_value::TypeDef, types::PrimitiveType};
use super::{cad_op::Group, kcl_value::TypeDef, types::PrimitiveType};
use crate::{
engine::ExecutionKind,
errors::{KclError, KclErrorDetails},
@ -20,7 +20,7 @@ use crate::{
modules::{ModuleId, ModulePath, ModuleRepr},
parsing::ast::types::{
Annotation, ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem,
CallExpression, CallExpressionKw, Expr, FunctionExpression, IfExpression, ImportPath, ImportSelector,
BoxNode, CallExpression, CallExpressionKw, Expr, FunctionExpression, IfExpression, ImportPath, ImportSelector,
ItemVisibility, LiteralIdentifier, LiteralValue, MemberExpression, MemberObject, Name, Node, NodeRef,
ObjectExpression, PipeExpression, Program, TagDeclarator, Type, UnaryExpression, UnaryOperator,
},
@ -564,10 +564,19 @@ impl ExecutorContext {
async fn exec_module_for_result(
&self,
module_id: ModuleId,
module_name: &BoxNode<Name>,
exec_state: &mut ExecState,
exec_kind: ExecutionKind,
source_range: SourceRange,
) -> Result<Option<KclValue>, KclError> {
exec_state.global.operations.push(Operation::GroupBegin {
group: Group::ModuleInstance {
name: module_name.to_string(),
module_id,
},
source_range,
});
let path = exec_state.global.module_infos[&module_id].path.clone();
let mut repr = exec_state.global.module_infos[&module_id].take_repr();
// DON'T EARLY RETURN! We need to restore the module repr
@ -593,6 +602,9 @@ impl ExecutorContext {
};
exec_state.global.module_infos[&module_id].restore_repr(repr);
exec_state.global.operations.push(Operation::GroupEnd);
result
}
@ -644,7 +656,7 @@ impl ExecutorContext {
Expr::Name(name) => {
let value = name.get_result(exec_state, self).await?.clone();
if let KclValue::Module { value: module_id, meta } = value {
self.exec_module_for_result(module_id, exec_state, ExecutionKind::Normal, metadata.source_range)
self.exec_module_for_result(module_id, name, exec_state, ExecutionKind::Normal, metadata.source_range)
.await?
.unwrap_or_else(|| {
exec_state.warn(CompilationError::err(
@ -1401,7 +1413,7 @@ impl Node<CallExpressionKw> {
if matches!(fn_src, FunctionSource::User { .. }) && !ctx.is_isolated_execution().await {
// Track return operation.
exec_state.global.operations.push(Operation::UserDefinedFunctionReturn);
exec_state.global.operations.push(Operation::GroupEnd);
}
let result = return_value.ok_or_else(move || {
@ -1511,12 +1523,14 @@ impl Node<CallExpression> {
if !ctx.is_isolated_execution().await {
// Track call operation.
exec_state.global.operations.push(Operation::UserDefinedFunctionCall {
name: Some(fn_name.to_string()),
function_source_range: func.function_def_source_range().unwrap_or_default(),
unlabeled_arg: None,
// TODO: Add the arguments for legacy positional parameters.
labeled_args: Default::default(),
exec_state.global.operations.push(Operation::GroupBegin {
group: Group::FunctionCall {
name: Some(fn_name.to_string()),
function_source_range: func.function_def_source_range().unwrap_or_default(),
unlabeled_arg: None,
// TODO: Add the arguments for legacy positional parameters.
labeled_args: Default::default(),
},
source_range: callsite,
});
}
@ -1550,7 +1564,7 @@ impl Node<CallExpression> {
if !ctx.is_isolated_execution().await {
// Track return operation.
exec_state.global.operations.push(Operation::UserDefinedFunctionReturn);
exec_state.global.operations.push(Operation::GroupEnd);
}
Ok(result)
@ -2331,15 +2345,17 @@ impl FunctionSource {
.iter()
.map(|(k, arg)| (k.clone(), OpArg::new(OpKclValue::from(&arg.value), arg.source_range)))
.collect();
exec_state.global.operations.push(Operation::UserDefinedFunctionCall {
name: fn_name,
function_source_range: ast.as_source_range(),
unlabeled_arg: args
.kw_args
.unlabeled
.as_ref()
.map(|arg| OpArg::new(OpKclValue::from(&arg.value), arg.source_range)),
labeled_args: op_labeled_args,
exec_state.global.operations.push(Operation::GroupBegin {
group: Group::FunctionCall {
name: fn_name,
function_source_range: ast.as_source_range(),
unlabeled_arg: args
.kw_args
.unlabeled
.as_ref()
.map(|arg| OpArg::new(OpKclValue::from(&arg.value), arg.source_range)),
labeled_args: op_labeled_args,
},
source_range: callsite,
});
}

View File

@ -418,6 +418,9 @@ pub struct Sketch {
pub artifact_id: ArtifactId,
#[ts(skip)]
pub original_id: uuid::Uuid,
/// If the sketch includes a mirror.
#[serde(skip)]
pub mirror: Option<uuid::Uuid>,
pub units: UnitLen,
/// Metadata.
#[serde(skip)]

View File

@ -241,7 +241,7 @@ pub enum ContextType {
Live,
/// Completely mocked connection
/// Mock mode is only for the modeling app when they just want to mock engine calls and not
/// Mock mode is only for the Design Studio when they just want to mock engine calls and not
/// actually make them.
Mock,

View File

@ -799,7 +799,7 @@ impl Backend {
// We do not have project descriptions yet.
project_description: None,
project_name,
// The UUID for the modeling app.
// The UUID for the Design Studio.
// We can unwrap here because we know it will not panic.
source_id: uuid::Uuid::from_str("70178592-dfca-47b3-bd2d-6fce2bcaee04").unwrap(),
type_: kittycad::types::Type::ModelingAppEvent,
@ -1194,7 +1194,7 @@ impl LanguageServer for Backend {
// Get last word
let last_word = line_prefix
.split(|c: char| c.is_whitespace() || c.is_ascii_punctuation())
.last()
.next_back()
.unwrap_or("");
// If the last word starts with a digit, return no completions

View File

@ -148,10 +148,10 @@ pub fn generate_settings_docs() {
if let Some(metadata) = &obj.metadata {
metadata.description.clone().unwrap_or_default()
} else {
"Project specific settings for the KittyCAD modeling app.".to_string()
"Project specific settings for the Zoo Design Studio.".to_string()
}
} else {
"Project specific settings for the KittyCAD modeling app.".to_string()
"Project specific settings for the Zoo Design Studio.".to_string()
};
// Convert the schema to our template format
@ -183,10 +183,10 @@ pub fn generate_settings_docs() {
if let Some(metadata) = &obj.metadata {
metadata.description.clone().unwrap_or_default()
} else {
"User-specific configuration options for the KittyCAD modeling app.".to_string()
"User-specific configuration options for the Zoo Design Studio.".to_string()
}
} else {
"User-specific configuration options for the KittyCAD modeling app.".to_string()
"User-specific configuration options for the Zoo Design Studio.".to_string()
};
// Trim any trailing periods to avoid double periods

View File

@ -1,4 +1,4 @@
//! This module contains settings for kcl projects as well as the modeling app.
//! This module contains settings for kcl projects as well as the Design Studio.
pub mod types;

View File

@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize};
use validator::{Validate, ValidateRange};
const DEFAULT_THEME_COLOR: f64 = 264.5;
const DEFAULT_PROJECT_NAME_TEMPLATE: &str = "project-$nnn";
const DEFAULT_PROJECT_NAME_TEMPLATE: &str = "untitled";
/// User specific settings for the app.
/// These live in `user.toml` in the app's configuration directory.
@ -20,7 +20,7 @@ const DEFAULT_PROJECT_NAME_TEMPLATE: &str = "project-$nnn";
#[ts(export)]
#[serde(rename_all = "snake_case")]
pub struct Configuration {
/// The settings for the modeling app.
/// The settings for the Design Studio.
#[serde(default, skip_serializing_if = "is_default")]
#[validate(nested)]
pub settings: Settings,
@ -75,7 +75,7 @@ impl Configuration {
#[ts(export)]
#[serde(rename_all = "snake_case")]
pub struct Settings {
/// The settings for the modeling app.
/// The settings for the Design Studio.
#[serde(default, skip_serializing_if = "is_default")]
#[validate(nested)]
pub app: AppSettings,
@ -603,7 +603,7 @@ mouseControls = "KittyCAD"
showDebugPanel = true
[settings.projects]
defaultProjectName = "project-$nnn"
defaultProjectName = "untitled"
[settings.textEditor]
textWrapping = true
@ -860,7 +860,7 @@ projectDirectory = "/Users/macinatormax/Documents/kittycad-modeling-projects""#;
},
project: ProjectSettings {
directory: "/Users/macinatormax/Documents/kittycad-modeling-projects".into(),
default_project_name: "project-$nnn".to_string().into()
default_project_name: "untitled".to_string().into()
},
command_bar: CommandBarSettings {
include_settings: true.into()

View File

@ -60,7 +60,7 @@ impl ProjectConfiguration {
#[ts(export)]
#[serde(rename_all = "snake_case")]
pub struct PerProjectSettings {
/// The settings for the modeling app.
/// The settings for the Design Studio.
#[serde(default)]
#[validate(nested)]
pub app: ProjectAppSettings,

View File

@ -13,7 +13,7 @@ use kcmc::{
websocket::{ModelingCmdReq, OkWebSocketResponseData},
ModelingCmd,
};
use kittycad_modeling_cmds as kcmc;
use kittycad_modeling_cmds::{self as kcmc};
use uuid::Uuid;
use crate::{
@ -168,13 +168,18 @@ pub(crate) async fn do_post_extrude<'a>(
)
.await?;
// The "get extrusion face info" API call requires *any* edge on the sketch being extruded.
// So, let's just use the first one.
let Some(any_edge_id) = sketch.paths.first().map(|edge| edge.get_base().geo_meta.id) else {
return Err(KclError::Type(KclErrorDetails {
message: "Expected a non-empty sketch".to_string(),
source_ranges: vec![args.source_range],
}));
let any_edge_id = if let Some(edge_id) = sketch.mirror {
edge_id
} else {
// The "get extrusion face info" API call requires *any* edge on the sketch being extruded.
// So, let's just use the first one.
let Some(any_edge_id) = sketch.paths.first().map(|edge| edge.get_base().geo_meta.id) else {
return Err(KclError::Type(KclErrorDetails {
message: "Expected a non-empty sketch".to_string(),
source_ranges: vec![args.source_range],
}));
};
any_edge_id
};
let mut sketch = sketch.clone();

View File

@ -116,7 +116,7 @@ pub async fn import(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
/// are relative to the current project directory.
///
/// Note: The import command currently only works when using the native
/// Modeling App.
/// Design Studio.
///
/// ```no_run
/// model = import("tests/inputs/cube.obj")

View File

@ -2,10 +2,13 @@
use anyhow::Result;
use kcmc::{each_cmd as mcmd, ModelingCmd};
use kittycad_modeling_cmds::{self as kcmc, length_unit::LengthUnit, shared::Point3d};
use kittycad_modeling_cmds::{
self as kcmc, length_unit::LengthUnit, ok_response::OkModelingCmdResponse, output::EntityGetAllChildUuids,
shared::Point3d, websocket::OkWebSocketResponseData,
};
use crate::{
errors::KclError,
errors::{KclError, KclErrorDetails},
execution::{
types::{PrimitiveType, RuntimeType},
ExecState, KclValue, Sketch,
@ -31,13 +34,21 @@ pub async fn mirror_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValu
Ok(sketches.into())
}
/// Mirror a sketch.
///
/// Only works on unclosed sketches for now.
async fn inner_mirror_2d(
sketches: Vec<Sketch>,
axis: Axis2dOrEdgeReference,
exec_state: &mut ExecState,
args: Args,
) -> Result<Vec<Sketch>, KclError> {
let starting_sketches = sketches;
let mut starting_sketches = sketches.clone();
// Update all to have a mirror.
starting_sketches.iter_mut().for_each(|sketch| {
sketch.mirror = Some(exec_state.next_uuid());
});
if args.ctx.no_engine_commands().await {
return Ok(starting_sketches);
@ -77,5 +88,40 @@ async fn inner_mirror_2d(
}
};
// After the mirror, get the first child uuid for the path.
// The "get extrusion face info" API call requires *any* edge on the sketch being extruded.
// But if you mirror2d a sketch these IDs might change so we need to get the children versus
// using the IDs we already have.
// We only do this with mirrors because otherwise it is a waste of a websocket call.
for sketch in &mut starting_sketches {
let response = args
.send_modeling_cmd(
exec_state.next_uuid(),
ModelingCmd::from(mcmd::EntityGetAllChildUuids { entity_id: sketch.id }),
)
.await?;
let OkWebSocketResponseData::Modeling {
modeling_response:
OkModelingCmdResponse::EntityGetAllChildUuids(EntityGetAllChildUuids { entity_ids: child_ids }),
} = response
else {
return Err(KclError::Internal(KclErrorDetails {
message: "Expected a successful response from EntityGetAllChildUuids".to_string(),
source_ranges: vec![args.source_range],
}));
};
if child_ids.len() >= 2 {
// The first child is the original sketch, the second is the mirrored sketch.
let child_id = child_ids[1];
sketch.mirror = Some(child_id);
} else {
return Err(KclError::Type(KclErrorDetails {
message: "Expected child uuids to be >= 2".to_string(),
source_ranges: vec![args.source_range],
}));
}
}
Ok(starting_sketches)
}

View File

@ -1343,6 +1343,7 @@ pub(crate) async fn inner_start_profile_at(
on: sketch_surface.clone(),
paths: vec![],
units: sketch_surface.units(),
mirror: Default::default(),
meta: vec![args.source_range.into()],
tags: if let Some(tag) = &tag {
let mut tag_identifier: TagIdentifier = tag.into();