diff --git a/docs/kcl-lang/settings/user.md b/docs/kcl-lang/settings/user.md index b557b6b9ce..2e358305d2 100644 --- a/docs/kcl-lang/settings/user.md +++ b/docs/kcl-lang/settings/user.md @@ -160,6 +160,14 @@ Whether or not to show a scale grid in the 3D modeling view **Default:** None +##### snap_to_grid + +Whether or not to snap to the scale grid in sketching mode. + + +**Default:** None + + #### text_editor Settings that affect the behavior of the KCL text editor. diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-1-Google-Chrome-linux.png index 934bb3a82a..e216fa23cf 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-1-Google-Chrome-linux.png index 449b960344..64bf287a57 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-circle-should-look-right-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-circle-should-look-right-1-Google-Chrome-linux.png index e868bce543..4ace12a346 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-circle-should-look-right-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-circle-should-look-right-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-rectangles-should-look-right-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-rectangles-should-look-right-1-Google-Chrome-linux.png index bc2944594f..82db5daeba 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-rectangles-should-look-right-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-rectangles-should-look-right-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-1-Google-Chrome-linux.png index 56d3549d5a..b2006815d7 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-2-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-2-Google-Chrome-linux.png index 13237c56e6..fe1b7e66f0 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-2-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-2-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-3-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-3-Google-Chrome-linux.png index bad756eba8..e6af2540c7 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-3-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-3-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-4-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-4-Google-Chrome-linux.png index 49e93225f3..de790178f9 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-4-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-4-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-5-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-5-Google-Chrome-linux.png index 036b70efec..68c30552a1 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-5-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-5-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Sketch-on-face-with-none-z-up-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Sketch-on-face-with-none-z-up-1-Google-Chrome-linux.png index 7d069310b5..198009c4d8 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Sketch-on-face-with-none-z-up-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Sketch-on-face-with-none-z-up-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/storageStates.ts b/e2e/playwright/storageStates.ts index be6d50b0e4..58b32bd3a1 100644 --- a/e2e/playwright/storageStates.ts +++ b/e2e/playwright/storageStates.ts @@ -12,13 +12,16 @@ export const TEST_SETTINGS: DeepPartial = { }, onboarding_status: 'dismissed', show_debug_panel: true, - fixed_size_grid: false, }, modeling: { enable_ssao: false, base_unit: 'in', mouse_controls: 'zoo', camera_projection: 'perspective', + // Tests were written before this setting existed. + // It's true by default because it's a good user experience, but + // these tests require it to be false. + fixed_size_grid: false, }, project: { default_project_name: 'untitled', diff --git a/e2e/playwright/test-utils.ts b/e2e/playwright/test-utils.ts index e5aa1aeb1e..ff69e2258f 100644 --- a/e2e/playwright/test-utils.ts +++ b/e2e/playwright/test-utils.ts @@ -952,10 +952,6 @@ export async function setup( }, ...TEST_SETTINGS.project, onboarding_status: 'dismissed', - // Tests were written before this setting existed. - // It's true by default because it's a good user experience, but - // these tests require it to be false. - fixed_size_grid: false, }, project: { ...TEST_SETTINGS.project, diff --git a/rust/kcl-lib/src/execution/mod.rs b/rust/kcl-lib/src/execution/mod.rs index 17e8d835e2..1de769fd6c 100644 --- a/rust/kcl-lib/src/execution/mod.rs +++ b/rust/kcl-lib/src/execution/mod.rs @@ -327,7 +327,7 @@ impl From for ExecutorSettings { replay: None, project_directory: None, current_file: None, - fixed_size_grid: settings.app.fixed_size_grid, + fixed_size_grid: settings.modeling.fixed_size_grid, } } } diff --git a/rust/kcl-lib/src/settings/types/mod.rs b/rust/kcl-lib/src/settings/types/mod.rs index 0d54c84548..fce032a881 100644 --- a/rust/kcl-lib/src/settings/types/mod.rs +++ b/rust/kcl-lib/src/settings/types/mod.rs @@ -94,10 +94,6 @@ pub struct AppSettings { /// of the app to aid in development. #[serde(default, skip_serializing_if = "is_default")] pub show_debug_panel: bool, - /// If true, the grid cells will be fixed-size, where the width is your default length unit. - /// If false, the grid will get larger as you zoom out, and smaller as you zoom in. - #[serde(default = "make_it_so", skip_serializing_if = "is_true")] - pub fixed_size_grid: bool, } /// Default to true. @@ -118,7 +114,6 @@ impl Default for AppSettings { stream_idle_mode: Default::default(), allow_orbit_in_sketch_mode: Default::default(), show_debug_panel: Default::default(), - fixed_size_grid: make_it_so(), } } } @@ -270,7 +265,7 @@ impl From for kittycad::types::Color { } /// Settings that affect the behavior while modeling. -#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Eq, Validate)] +#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Validate)] #[serde(rename_all = "snake_case")] #[ts(export)] pub struct ModelingSettings { @@ -298,6 +293,24 @@ pub struct ModelingSettings { /// Whether or not to show a scale grid in the 3D modeling view #[serde(default, skip_serializing_if = "is_default")] pub show_scale_grid: bool, + /// When enabled, the grid will use a fixed size based on your selected units rather than automatically scaling with zoom level. + /// If true, the grid cells will be fixed-size, where the width is your default length unit. + /// If false, the grid will get larger as you zoom out, and smaller as you zoom in. + #[serde(default = "make_it_so", skip_serializing_if = "is_true")] + pub fixed_size_grid: bool, + /// Whether or not to snap to the scale grid in sketching mode. + #[serde(default, skip_serializing_if = "is_default")] + pub snap_to_grid: bool, + /// The space between major grid lines, specified in the current unit + #[serde(default, skip_serializing_if = "is_default")] + pub major_grid_spacing: f64, + /// Specifies ow many minor grid lines to have per major grid line. + #[serde(default, skip_serializing_if = "is_default")] + pub minor_grids_per_major: f64, + /// The number of snaps to have between minor grid lines. 1 means snapping to the minor grid lines. + #[serde(default, skip_serializing_if = "is_default")] + pub snaps_per_minor: f64, + } #[derive(Debug, Copy, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Eq)] diff --git a/rust/kcl-lib/src/settings/types/project.rs b/rust/kcl-lib/src/settings/types/project.rs index 457c0a0496..3054263c93 100644 --- a/rust/kcl-lib/src/settings/types/project.rs +++ b/rust/kcl-lib/src/settings/types/project.rs @@ -120,7 +120,7 @@ pub struct ProjectAppearanceSettings { } /// Project specific settings that affect the behavior while modeling. -#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Eq, Validate)] +#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Validate)] #[serde(rename_all = "snake_case")] #[ts(export)] pub struct ProjectModelingSettings { @@ -133,6 +133,21 @@ pub struct ProjectModelingSettings { /// Whether or not Screen Space Ambient Occlusion (SSAO) is enabled. #[serde(default, skip_serializing_if = "is_default")] pub enable_ssao: DefaultTrue, + /// When enabled, the grid will use a fixed size based on your selected units rather than automatically scaling with zoom level. + #[serde(default, skip_serializing_if = "is_default")] + pub fixed_size_grid: DefaultTrue, + /// Whether or not to snap to the scale grid in sketching mode. + #[serde(default, skip_serializing_if = "is_default")] + pub snap_to_grid: bool, + /// The space between major grid lines, specified in the current unit + #[serde(default, skip_serializing_if = "is_default")] + pub major_grid_spacing: f64, + /// Specifies how many minor grid lines to have per major grid line. + #[serde(default, skip_serializing_if = "is_default")] + pub minor_grids_per_major: f64, + /// The number of snaps to have between minor grid lines. 1 means snapping to the minor grid lines. + #[serde(default, skip_serializing_if = "is_default")] + pub snaps_per_minor: f64, } fn named_view_point_version_one() -> f64 { @@ -331,6 +346,11 @@ color = 1567.4"#; base_unit: UnitLength::Yd, highlight_edges: Default::default(), enable_ssao: true.into(), + snap_to_grid: false, + major_grid_spacing: 1.0, + minor_grids_per_major: 4.0, + snaps_per_minor: 1.0, + fixed_size_grid: true.into(), }, text_editor: TextEditorSettings { text_wrapping: false.into(), diff --git a/src/clientSideScene/InfiniteGridRenderer.ts b/src/clientSideScene/InfiniteGridRenderer.ts new file mode 100644 index 0000000000..b7c326ffe6 --- /dev/null +++ b/src/clientSideScene/InfiniteGridRenderer.ts @@ -0,0 +1,229 @@ +import type { Camera } from 'three' +import { OrthographicCamera } from 'three' +import { GLSL3, LineSegments } from 'three' +import { BufferGeometry, RawShaderMaterial } from 'three' + +const vertexShader = `precision highp float; + +uniform int verticalLines; +uniform int horizontalLines; +uniform vec2 cameraPos; +uniform float worldToScreenX; +uniform float worldToScreenY; +uniform float minorSpacing; +uniform float majorSpacing; +uniform vec2 viewportPx; // (drawing buffer width, height) in pixels + +out float vLineType; + +float snapToPixel(float ndcCoord, float viewport) +{ + // NDC [-1,1] -> pixels [0, viewport] + float px = (ndcCoord * 0.5 + 0.5) * viewport; + + // Snap to pixel *centers* for crisp 1px lines + px = round(px - 0.5) + 0.5; + + // Back to NDC + return (px / viewport - 0.5) * 2.0; +} + +void main() +{ + int totalVerticalVerts = verticalLines * 2; + bool isVertical = gl_VertexID < totalVerticalVerts; + + vec2 screenPos; + + if (isVertical) + { + int lineIndex = gl_VertexID / 2; + int vertIndex = gl_VertexID % 2; + + float startWorldX = floor(cameraPos.x / minorSpacing) * minorSpacing - float(verticalLines / 2) * minorSpacing; + float worldX = startWorldX + float(lineIndex) * minorSpacing; + + float screenX = (worldX - cameraPos.x) * worldToScreenX; + float screenY = (vertIndex == 0) ? -1.0 : 1.0; + + // Snap X to integer pixels + screenX = snapToPixel(screenX, viewportPx.x); + + screenPos = vec2(screenX, screenY); + + vLineType = mod(abs(worldX), majorSpacing) < 0.001 ? 1.0 : 0.0; + } + else + { + int lineIndex = (gl_VertexID - totalVerticalVerts) / 2; + int vertIndex = (gl_VertexID - totalVerticalVerts) % 2; + + float startWorldY = floor(cameraPos.y / minorSpacing) * minorSpacing - float(horizontalLines / 2) * minorSpacing; + float worldY = startWorldY + float(lineIndex) * minorSpacing; + + float screenX = (vertIndex == 0) ? -1.0 : 1.0; + float screenY = (worldY - cameraPos.y) * worldToScreenY; + + // Snap Y to integer pixels + screenY = snapToPixel(screenY, viewportPx.y); + + screenPos = vec2(screenX, screenY); + + vLineType = mod(abs(worldY), majorSpacing) < 0.001 ? 1.0 : 0.0; + } + + gl_Position = vec4(screenPos, 0.0, 1.0); +}` + +const fragmentShader = `precision highp float; + +in float vLineType; +out vec4 fragColor; + +uniform vec4 uMajorColor; +uniform vec4 uMinorColor; + +void main() +{ + fragColor = vLineType > 0.5 ? uMajorColor : uMinorColor; +}` + +export class InfiniteGridRenderer extends LineSegments { + private minMinorGridPixelSpacing = 10 + private minMajorGridPixelSpacing = 10 + + constructor() { + const geometry = new BufferGeometry() + geometry.name = 'InfiniteGridGeometry' + + const material = new RawShaderMaterial({ + glslVersion: GLSL3, + vertexShader, + fragmentShader, + uniforms: { + verticalLines: { value: 50 }, + horizontalLines: { value: 50 }, + cameraPos: { value: [0.0, 0.0] }, + worldToScreenX: { value: 0.1 }, + worldToScreenY: { value: 0.1 }, + minorSpacing: { value: 1.0 }, + majorSpacing: { value: 4.0 }, + viewportPx: { value: [1.0, 1.0] }, // set real size in update() + uMajorColor: { value: [0.3, 0.3, 0.3, 1.0] }, + uMinorColor: { value: [0.2, 0.2, 0.2, 1.0] }, + }, + transparent: false, + depthTest: false, + depthWrite: false, + }) + + super(geometry, material) + this.name = 'InfiniteGridRenderer' + + this.renderOrder = -10 + this.frustumCulled = false + this.raycast = () => { + // Disable raycasting: there are no vertices so it wouldn't work anyway, we also don't want to pick the grid + } + } + + update( + camera: Camera, + majorSpacing: number, + minorPerMajor: number, + viewportWidthPx: number, + viewportHeightPx: number, + options: { + majorColor: [number, number, number, number] + minorColor: [number, number, number, number] + fixedSizeGrid?: boolean + majorPxRange?: [number, number] + } + ) { + if (!(camera instanceof OrthographicCamera)) { + console.log( + 'Only orthographic cameras are supported for GridHelperInfinite' + ) + return + } + + const material = this.material as RawShaderMaterial + + const zoom = camera.zoom + let effectiveMajorSpacing = majorSpacing + let minorSpacing = effectiveMajorSpacing / minorPerMajor + + const worldViewportWidth = (camera.right - camera.left) / zoom + const worldViewportHeight = (camera.top - camera.bottom) / zoom + + const worldToScreenX = 2 / worldViewportWidth + const worldToScreenY = 2 / worldViewportHeight + + const pxPerWorldX = viewportWidthPx / worldViewportWidth + const pxPerWorldY = viewportHeightPx / worldViewportHeight + + if (!options.fixedSizeGrid) { + const desiredMin = options.majorPxRange?.[0] ?? 40 + const desiredMax = options.majorPxRange?.[1] ?? 120 + const pxPerWorld = Math.min(pxPerWorldX, pxPerWorldY) + let majorPx = effectiveMajorSpacing * pxPerWorld + + while (majorPx < desiredMin) { + effectiveMajorSpacing *= 10 + majorPx *= 10 + } + while (majorPx > desiredMax) { + effectiveMajorSpacing /= 10 + majorPx /= 10 + } + minorSpacing = effectiveMajorSpacing / Math.max(1, minorPerMajor) + } + + const minorSpacingPx = Math.min( + minorSpacing * pxPerWorldX, + minorSpacing * pxPerWorldY + ) + const majorSpacingPx = Math.min( + effectiveMajorSpacing * pxPerWorldX, + effectiveMajorSpacing * pxPerWorldY + ) + + let effectiveMinorSpacing = minorSpacing + this.visible = true + if (options.fixedSizeGrid) { + // If major grid would be too dense on screen, hide the grid entirely + if (majorSpacingPx < this.minMajorGridPixelSpacing) { + this.visible = false + return + } + + // If minors are too small, collapse to majors only by using major spacing + if (minorSpacingPx < this.minMinorGridPixelSpacing) { + effectiveMinorSpacing = effectiveMajorSpacing + } + } + + const verticalLines = + Math.ceil(worldViewportWidth / effectiveMinorSpacing) + 2 + const horizontalLines = + Math.ceil(worldViewportHeight / effectiveMinorSpacing) + 2 + + material.uniforms.verticalLines.value = verticalLines + material.uniforms.horizontalLines.value = horizontalLines + material.uniforms.cameraPos.value = [camera.position.y, camera.position.z] + material.uniforms.worldToScreenX.value = worldToScreenX + material.uniforms.worldToScreenY.value = worldToScreenY + material.uniforms.minorSpacing.value = effectiveMinorSpacing + material.uniforms.majorSpacing.value = effectiveMajorSpacing + material.uniforms.viewportPx.value = [viewportWidthPx, viewportHeightPx] + + // Optional theme-based colors + if (options?.majorColor) + material.uniforms.uMajorColor.value = options.majorColor + if (options?.minorColor) + material.uniforms.uMinorColor.value = options.minorColor + + const totalVertices = verticalLines * 2 + horizontalLines * 2 + this.geometry.setDrawRange(0, totalVertices) + } +} diff --git a/src/clientSideScene/helpers.ts b/src/clientSideScene/helpers.ts index b99c3e9edd..c3da52eede 100644 --- a/src/clientSideScene/helpers.ts +++ b/src/clientSideScene/helpers.ts @@ -1,31 +1,8 @@ import type { Group, Mesh, OrthographicCamera, Quaternion } from 'three' -import { - GridHelper, - LineBasicMaterial, - PerspectiveCamera, - Vector3, -} from 'three' +import { PerspectiveCamera, Vector3 } from 'three' import { compareVec2Epsilon2 } from '@src/lang/std/sketch' -export function createGridHelper({ - size, - divisions, -}: { - size: number - divisions: number -}) { - const gridHelperMaterial = new LineBasicMaterial({ - color: 0xaaaaaa, - transparent: true, - opacity: 0.5, - depthTest: false, - }) - const gridHelper = new GridHelper(size, divisions, 0x0000ff, 0xffffff) - gridHelper.material = gridHelperMaterial - gridHelper.rotation.x = Math.PI / 2 - return gridHelper -} const fudgeFactor = 72.66985970437086 export const orthoScale = (cam: OrthographicCamera | PerspectiveCamera) => diff --git a/src/clientSideScene/sceneEntities.ts b/src/clientSideScene/sceneEntities.ts index 95c4552f76..bb9cb9a5d2 100644 --- a/src/clientSideScene/sceneEntities.ts +++ b/src/clientSideScene/sceneEntities.ts @@ -38,7 +38,6 @@ import type { SafeArray } from '@src/lib/utils' import { getAngle, getLength, uuidv4 } from '@src/lib/utils' import { - createGridHelper, isQuaternionVertical, orthoScale, perspScale, @@ -87,7 +86,6 @@ import { SKETCH_LAYER, X_AXIS, Y_AXIS, - getSceneScale, } from '@src/clientSideScene/sceneUtils' import type { SegmentUtils } from '@src/clientSideScene/segments' import { createLineShape } from '@src/clientSideScene/segments' @@ -162,7 +160,7 @@ import type RustContext from '@src/lib/rustContext' import { updateExtraSegments } from '@src/lib/selections' import type { Selections } from '@src/lib/selections' import { getEventForSegmentSelection } from '@src/lib/selections' -import type { Themes } from '@src/lib/theme' +import { getResolvedTheme, Themes } from '@src/lib/theme' import { getThemeColorForThreeJs } from '@src/lib/theme' import { err, reportRejection, trap } from '@src/lib/trap' import { isArray, isOverlap, roundOff } from '@src/lib/utils' @@ -179,6 +177,8 @@ import type { SketchTool, } from '@src/machines/modelingMachine' import { calculateIntersectionOfTwoLines } from 'sketch-helpers' +import type { SettingsType } from '@src/lib/settings/initialSettings' +import { InfiniteGridRenderer } from '@src/clientSideScene/InfiniteGridRenderer' type DraftSegment = 'line' | 'tangentialArc' @@ -200,6 +200,8 @@ export class SceneEntities { draftPointGroups: Group[] = [] currentSketchQuaternion: Quaternion | null = null + getSettings: (() => SettingsType) | null = null + constructor( engineCommandManager: EngineCommandManager, sceneInfra: SceneInfra, @@ -353,6 +355,7 @@ export class SceneEntities { x?.scale.set(1, factor / this.sceneInfra._baseUnitMultiplier, 1) const y = this.axisGroup.getObjectByName(Y_AXIS) y?.scale.set(factor / this.sceneInfra._baseUnitMultiplier, 1, 1) + this.updateGrid() } this.sceneInfra.overlayCallbacks(callbacks) } @@ -416,15 +419,8 @@ export class SceneEntities { yAxisMesh.name = Y_AXIS this.axisGroup = new Group() - const gridHelper = createGridHelper({ size: 100, divisions: 10 }) - gridHelper.position.z = -0.01 - gridHelper.renderOrder = -3 // is this working? + const gridHelper = new InfiniteGridRenderer() gridHelper.name = 'gridHelper' - const sceneScale = getSceneScale( - this.sceneInfra.camControls.camera, - this.sceneInfra.camControls.target - ) - gridHelper.scale.set(sceneScale, sceneScale, sceneScale) const factor = this.sceneInfra.camControls.camera instanceof OrthographicCamera @@ -451,6 +447,50 @@ export class SceneEntities { this.axisGroup.setRotationFromQuaternion(quat) sketchPosition && this.axisGroup.position.set(...sketchPosition) this.sceneInfra.scene.add(this.axisGroup) + + this.updateGrid() + } + + updateGrid() { + const settings = this.getSettings?.() + if (settings) { + const gridHelper = this.sceneInfra.scene + .getObjectByName(AXIS_GROUP) + ?.getObjectByName('gridHelper') + if (gridHelper instanceof InfiniteGridRenderer) { + const majorGridSpacing = settings.modeling.majorGridSpacing.current ?? 1 + const minorGridsPerMajor = + settings.modeling.minorGridsPerMajor.current ?? 4 + + const viewportSize = this.sceneInfra.renderer.getDrawingBufferSize( + new Vector2() + ) + // Choose grid colors based on app theme: dark = existing colors, light = subtle gray + const isLight = getResolvedTheme(this.sceneInfra.theme) === Themes.Light + const majorColor: [number, number, number, number] = isLight + ? [0.3, 0.3, 0.3, 1.0] + : [0.7, 0.7, 0.7, 1.0] + const minorColor: [number, number, number, number] = isLight + ? [0.2, 0.2, 0.2, 1.0] + : [0.9, 0.9, 0.9, 1.0] + + gridHelper.update( + this.sceneInfra.camControls.camera, + majorGridSpacing, + minorGridsPerMajor, + viewportSize.x, + viewportSize.y, + { + majorColor, + minorColor, + fixedSizeGrid: settings.modeling.fixedSizeGrid.current, + majorPxRange: [40, 120], + } + ) + } + } else { + console.error('Settings not available for grid update') + } } getDraftPoint() { @@ -490,7 +530,7 @@ export class SceneEntities { isDraft: true, from: [point.x, point.y], scale, - theme: this.sceneInfra._theme, + theme: this.sceneInfra.theme, // default is 12, this makes the draft point pop a bit more, // especially when snapping to the startProfile handle as it's it was the exact same size size: 16, @@ -504,6 +544,28 @@ export class SceneEntities { if (draftPoint) draftPoint.removeFromParent() } + private snapToGrid(point: Coords2d, event: MouseEvent): Coords2d { + if (event.ctrlKey || event.metaKey) { + // disable snapping with ctrl + return point + } + + const settings = this.getSettings?.() + if (!settings) { + console.error('getSettings not injected!') + return point + } + if (!settings.modeling.snapToGrid.current) return point + + const snapsPerMinor = settings.modeling.snapsPerMinor.current + const minorsPerMajor = settings.modeling.minorGridsPerMajor.current + const multiplier = minorsPerMajor * snapsPerMinor + return [ + Math.round(point[0] * multiplier) / multiplier, + Math.round(point[1] * multiplier) / multiplier, + ] + } + setupNoPointsListener({ sketchDetails, afterClick, @@ -636,16 +698,19 @@ export class SceneEntities { (sceneObject) => sceneObject.object.name === X_AXIS ) - const snappedClickPoint = { - x: yAxisIntersection ? 0 : intersectionPoint.twoD.x, - y: xAxisIntersection ? 0 : intersectionPoint.twoD.y, + let startPoint: Coords2d = [ + yAxisIntersection ? 0 : intersectionPoint.twoD.x, + xAxisIntersection ? 0 : intersectionPoint.twoD.y, + ] + if (!xAxisIntersection && !yAxisIntersection) { + startPoint = this.snapToGrid(startPoint, args.mouseEvent) } const inserted = insertNewStartProfileAt( this.kclManager.ast, sketchDetails.sketchNodePaths, sketchDetails.planeNodePath, - [snappedClickPoint.x, snappedClickPoint.y], + startPoint, 'end' ) @@ -729,7 +794,7 @@ export class SceneEntities { id: sketch.start.__geoMeta.id, pathToNode: segPathToNode, scale, - theme: this.sceneInfra._theme, + theme: this.sceneInfra.theme, isDraft: false, }) _profileStart.layers.set(SKETCH_LAYER) @@ -863,7 +928,7 @@ export class SceneEntities { pathToNode: segPathToNode, isDraftSegment, scale, - theme: this.sceneInfra._theme, + theme: this.sceneInfra.theme, isSelected, sceneInfra: this.sceneInfra, selection, @@ -2746,12 +2811,16 @@ export class SceneEntities { } } - // Snap to the main axes if there was no snapping to tangent direction if (!snappedToTangent) { + // Snap to the main axes if there was no snapping to tangent direction snappedPoint = [ intersectsYAxis ? 0 : snappedPoint[0], intersectsXAxis ? 0 : snappedPoint[1], - ] + ] as const + + if (!intersectsXAxis && !intersectsYAxis) { + snappedPoint = this.snapToGrid(snappedPoint, mouseEvent) + } } return { @@ -3502,7 +3571,7 @@ export class SceneEntities { isSelected ? SEGMENT_BLUE : parent?.userData?.baseColor || - getThemeColorForThreeJs(this.sceneInfra._theme) + getThemeColorForThreeJs(this.sceneInfra.theme) ) updateExtraSegments(parent, 'hoveringLine', false) updateExtraSegments(parent, 'selected', isSelected) @@ -3618,7 +3687,7 @@ export class SceneEntities { } drawDashedLine({ from, to }: { from: Coords2d; to: Coords2d }) { - const baseColor = getThemeColorForThreeJs(this.sceneInfra._theme) + const baseColor = getThemeColorForThreeJs(this.sceneInfra.theme) const color = baseColor const meshType = STRAIGHT_SEGMENT_DASH diff --git a/src/clientSideScene/sceneInfra.ts b/src/clientSideScene/sceneInfra.ts index 789611d875..783172172d 100644 --- a/src/clientSideScene/sceneInfra.ts +++ b/src/clientSideScene/sceneInfra.ts @@ -1,17 +1,10 @@ import * as TWEEN from '@tweenjs/tween.js' -import type { - Group, - Intersection, - MeshBasicMaterial, - Object3D, - Object3DEventMap, -} from 'three' +import type { Group, Intersection, Object3D, Object3DEventMap } from 'three' import { AmbientLight, Color, - GridHelper, - LineBasicMaterial, Mesh, + MeshBasicMaterial, OrthographicCamera, Raycaster, Scene, @@ -31,7 +24,6 @@ import { SKETCH_LAYER, X_AXIS, Y_AXIS, - getSceneScale, } from '@src/clientSideScene/sceneUtils' import type { useModelingContext } from '@src/hooks/useModelingContext' import type { EngineCommandManager } from '@src/lang/std/engineConnection' @@ -101,7 +93,7 @@ export class SceneInfra { readonly camControls: CameraControls isFovAnimationInProgress = false _baseUnitMultiplier = 1 - _theme: Themes = Themes.System + private _theme: Themes = Themes.System lastMouseState: MouseState = { type: 'idle' } onDragStartCallback: (arg: OnDragCallbackArgs) => Voidish = () => {} onDragEndCallback: (arg: OnDragCallbackArgs) => Voidish = () => {} @@ -142,6 +134,10 @@ export class SceneInfra { this._theme = theme } + get theme() { + return this._theme + } + resetMouseListeners = () => { this.setCallbacks({ onDragStart: () => {}, @@ -292,7 +288,6 @@ export class SceneInfra { engineCommandManager, false ) - this.camControls.subscribeToCamChange(() => this.onCameraChange()) this.camControls.camera.layers.enable(SKETCH_LAYER) if (DEBUG_SHOW_INTERSECTION_PLANE) this.camControls.camera.layers.enable(INTERSECTION_PLANE_LAYER) @@ -302,19 +297,20 @@ export class SceneInfra { this.raycaster.layers.disable(0) this.planeRaycaster.layers.enable(INTERSECTION_PLANE_LAYER) - // GRID - const size = 100 - const divisions = 10 - const gridHelperMaterial = new LineBasicMaterial({ - color: 0x0000ff, - transparent: true, - opacity: 0.5, - }) - - const gridHelper = new GridHelper(size, divisions, 0x0000ff, 0xffffff) - gridHelper.material = gridHelperMaterial - gridHelper.rotation.x = Math.PI / 2 - // this.scene.add(gridHelper) // more of a debug thing, but maybe useful + // GRID - more of a debug thing, but maybe useful + // const size = 100 + // const divisions = 10 + // const gridHelperMaterial = new LineBasicMaterial({ + // color: 0x0000ff, + // transparent: true, + // opacity: 0.5, + // }) + // + // This is the GridHelper in the 3D scene, the one in sketching is in sceneEntities.ts + // const gridHelper = new GridHelper(size, divisions, 0x0000ff, 0xffffff) + // gridHelper.material = gridHelperMaterial + // gridHelper.rotation.x = Math.PI / 2 + // this.scene.add(gridHelper) const light = new AmbientLight(0x505050) // soft white light this.scene.add(light) @@ -322,17 +318,6 @@ export class SceneInfra { SceneInfra.instance = this } - onCameraChange = () => { - const scale = getSceneScale( - this.camControls.camera, - this.camControls.target - ) - const axisGroup = this.scene - .getObjectByName(AXIS_GROUP) - ?.getObjectByName('gridHelper') - axisGroup?.name === 'gridHelper' && axisGroup.scale.set(scale, scale, scale) - } - // Called after canvas is attached to the DOM and on each resize. // Note: would be better to use ResizeObserver instead of window.onresize // See: @@ -715,14 +700,16 @@ export class SceneInfra { } axisGroup?.children.forEach((_mesh) => { const mesh = _mesh as Mesh - const mat = mesh.material as MeshBasicMaterial - if (otherSelections.includes(axisMap[mesh.userData?.type])) { - mat.color.set(mesh?.userData?.baseColor) - mat.color.offsetHSL(0, 0, 0.2) - mesh.userData.isSelected = true - } else { - mat.color.set(mesh?.userData?.baseColor) - mesh.userData.isSelected = false + const mat = mesh.material + if (mat instanceof MeshBasicMaterial) { + if (otherSelections.includes(axisMap[mesh.userData?.type])) { + mat.color.set(mesh?.userData?.baseColor) + mat.color.offsetHSL(0, 0, 0.2) + mesh.userData.isSelected = true + } else { + mat.color.set(mesh?.userData?.baseColor) + mesh.userData.isSelected = false + } } }) } diff --git a/src/components/CommandBar/CommandBarArgument.tsx b/src/components/CommandBar/CommandBarArgument.tsx index 817ffe4633..fde3f7e5cf 100644 --- a/src/components/CommandBar/CommandBarArgument.tsx +++ b/src/components/CommandBar/CommandBarArgument.tsx @@ -63,6 +63,7 @@ function ArgumentInput({ stepBack: () => void onSubmit: (event: any) => void }) { + // @ts-ignore switch (arg.inputType) { case 'options': return ( @@ -127,6 +128,15 @@ function ArgumentInput({ onSubmit={onSubmit} /> ) + case 'number': + console.error("'number' input is not implemented for CommandBar yet") + return ( + + ) default: return ( { const { app: { allowOrbitInSketchMode }, - modeling: { defaultUnit, cameraProjection, cameraOrbit }, + modeling: { defaultUnit, cameraProjection, cameraOrbit, snapToGrid }, } = useSettings() const loaderData = useLoaderData() as IndexLoaderData const projects = useFolders() @@ -1551,6 +1553,14 @@ export const ModelingMachineProvider = ({ resetCameraPosition().catch(reportRejection) }) + // Toggle Snap to grid + useHotkeyWrapper([SNAP_TO_GRID_HOTKEY], () => { + settingsActor.send({ + type: 'set.modeling.snapToGrid', + data: { level: 'project', value: !snapToGrid.current }, + }) + }) + useModelingMachineCommands({ machineId: 'modeling', state: modelingState, diff --git a/src/components/Settings/SettingsFieldInput.tsx b/src/components/Settings/SettingsFieldInput.tsx index 545c0279cf..c0864d5ee1 100644 --- a/src/components/Settings/SettingsFieldInput.tsx +++ b/src/components/Settings/SettingsFieldInput.tsx @@ -145,6 +145,40 @@ export function SettingsFieldInput({ }} /> ) + case 'number': + return ( + { + const numValue = parseFloat(e.target.value) + if (!Number.isNaN(numValue)) { + const currentValue = + setting[settingsLevel] !== undefined + ? setting[settingsLevel] + : setting.getFallback(settingsLevel) + if (currentValue !== numValue) { + send({ + type: `set.${category}.${settingName}`, + data: { + level: settingsLevel, + value: numValue, + }, + } as unknown as EventFrom) + } + } + }} + /> + ) } return (

diff --git a/src/components/UnitsMenu.tsx b/src/components/UnitsMenu.tsx index cd08b80496..64b5d6dcf1 100644 --- a/src/components/UnitsMenu.tsx +++ b/src/components/UnitsMenu.tsx @@ -1,5 +1,5 @@ import { Popover } from '@headlessui/react' -import { useEffect, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' import toast from 'react-hot-toast' import { @@ -9,11 +9,60 @@ import { } from '@src/lang/wasm' import { DEFAULT_DEFAULT_LENGTH_UNIT } from '@src/lib/constants' import { baseUnitLabels, baseUnitsUnion } from '@src/lib/settings/settingsTypes' -import { codeManager, kclManager } from '@src/lib/singletons' +import { codeManager, kclManager, sceneInfra } from '@src/lib/singletons' import { err, reportRejection } from '@src/lib/trap' +import { useModelingContext } from '@src/hooks/useModelingContext' +import { OrthographicCamera, Vector2 } from 'three' export function UnitsMenu() { const [fileSettings, setFileSettings] = useState(kclManager.fileSettings) + const { state: modelingState } = useModelingContext() + const inSketchMode = modelingState.matches('Sketch') + + const [rulerWidth, setRulerWidth] = useState(16) + const [rulerLabelValue, setRulerLabelValue] = useState(1) + + const currentUnit = + fileSettings.defaultLengthUnit ?? DEFAULT_DEFAULT_LENGTH_UNIT + + const onCameraChange = useCallback(() => { + if (!inSketchMode) { + return + } + const camera = sceneInfra.camControls.camera + if (!(camera instanceof OrthographicCamera)) { + console.error( + 'Camera is not an OrthographicCamera, skipping ruler recalculation' + ) + return + } + const worldViewportWidth = (camera.right - camera.left) / camera.zoom + const viewportSize = sceneInfra.renderer.getDrawingBufferSize(new Vector2()) + + // one base unit in screen space (pixels) + let rulerWidth = + ((1 / worldViewportWidth) * viewportSize.x) / window.devicePixelRatio + let displayValue = 1 + + if (rulerWidth > 150 || rulerWidth < 20) { + const k = Math.ceil(Math.log10(rulerWidth / 150)) + rulerWidth /= Math.pow(10, k) + displayValue = 1 / Math.pow(10, k) + if (k < 0) { + displayValue = Math.round(displayValue) // 1e5 would become something like 1.0000000000000001e+5 without this + } + } + setRulerWidth(rulerWidth) + setRulerLabelValue(displayValue) + }, [inSketchMode]) + + useEffect(() => { + const unsubscribe = + sceneInfra.camControls.subscribeToCamChange(onCameraChange) + return () => { + unsubscribe() + } + }, [inSketchMode, currentUnit, onCameraChange]) useEffect(() => { setFileSettings(kclManager.fileSettings) // eslint-disable-next-line react-hooks/exhaustive-deps -- TODO: blanket-ignored fix me! @@ -29,12 +78,17 @@ export function UnitsMenu() { text-xs text-primary bg-chalkboard-10/70 dark:bg-chalkboard-100/80 backdrop-blur-sm border !border-primary/50 rounded-full`} > -

+
Current units are:  - {fileSettings.defaultLengthUnit ?? DEFAULT_DEFAULT_LENGTH_UNIT} + {inSketchMode + ? `${rulerLabelValue > 10000 || rulerLabelValue < 0.0001 ? rulerLabelValue.toExponential() : rulerLabelValue.toString()}${currentUnit}` + : currentUnit} [ ...Object.entries(VIEW_NAMES_SEMANTIC).map(([axisName, axisSemantic]) => ( @@ -90,11 +95,37 @@ export function useViewControlMenuItems() { > Start sketch on selection , + ...(sketching + ? [ + , + { + settingsActor.send({ + type: 'set.modeling.snapToGrid', + data: { + level: 'project', + value: !snapToGrid, + }, + }) + }} + > + Snap to grid + , + ] + : []), , , ], // eslint-disable-next-line react-hooks/exhaustive-deps -- TODO: blanket-ignored fix me! - [VIEW_NAMES_SEMANTIC, shouldLockView, selectedPlaneId] + [ + VIEW_NAMES_SEMANTIC, + shouldLockView, + selectedPlaneId, + sketching, + snapToGrid, + ] ) return menuItems } diff --git a/src/lang/modifyAst.ts b/src/lang/modifyAst.ts index 9965618476..e0a5444d75 100644 --- a/src/lang/modifyAst.ts +++ b/src/lang/modifyAst.ts @@ -70,6 +70,7 @@ import { err, trap } from '@src/lib/trap' import { isArray, isOverlap, roundOff } from '@src/lib/utils' import type { ExtrudeFacePlane } from '@src/machines/modelingMachine' import { ARG_AT } from '@src/lang/constants' +import type { Coords2d } from '@src/lang/std/sketch' export function startSketchOnDefault( node: Node, @@ -108,7 +109,7 @@ export function insertNewStartProfileAt( node: Node, sketchNodePaths: PathToNode[], planeNodePath: PathToNode, - at: [number, number], + at: Coords2d, insertType: 'start' | 'end' = 'end' ): | { diff --git a/src/lib/commandTypes.ts b/src/lib/commandTypes.ts index 30d02c7e0a..e39c5f49ab 100644 --- a/src/lib/commandTypes.ts +++ b/src/lib/commandTypes.ts @@ -255,6 +255,23 @@ export type CommandArgumentConfig< ) => OutputType) defaultValueFromContext?: (context: C) => OutputType } + | { + inputType: 'number' + defaultValue?: + | OutputType + | (( + commandBarContext: ContextFrom, + machineContext?: C + ) => OutputType) + defaultValueFromContext?: (context: C) => OutputType + validation?: ({ + data, + context, + }: { + data: any + context: CommandBarContext + }) => Promise + } ) export type CommandArgument< @@ -399,6 +416,22 @@ export type CommandArgument< machineContext?: ContextFrom ) => OutputType) } + | { + inputType: 'number' + defaultValue?: + | OutputType + | (( + commandBarContext: ContextFrom, + machineContext?: ContextFrom + ) => OutputType) + validation?: ({ + data, + context, + }: { + data: any + context: CommandBarContext + }) => Promise + } ) export type CommandArgumentWithName< diff --git a/src/lib/hotkeys.ts b/src/lib/hotkeys.ts new file mode 100644 index 0000000000..3295911c17 --- /dev/null +++ b/src/lib/hotkeys.ts @@ -0,0 +1 @@ +export const SNAP_TO_GRID_HOTKEY = 'mod+g' diff --git a/src/lib/settings/initialSettings.tsx b/src/lib/settings/initialSettings.tsx index 48983811b0..decf6c6789 100644 --- a/src/lib/settings/initialSettings.tsx +++ b/src/lib/settings/initialSettings.tsx @@ -43,6 +43,7 @@ export class Setting { public Component: SettingProps['Component'] public description?: string private validate: (v: T) => boolean + public readonly isEnabled: (c: SettingsType) => boolean private _default: T private _user?: T private _project?: T @@ -51,6 +52,7 @@ export class Setting { this._default = props.defaultValue this.current = props.defaultValue this.validate = props.validate + this.isEnabled = props.isEnabled || (() => true) this.description = props.description this.hideOnLevel = props.hideOnLevel this.hideOnPlatform = props.hideOnPlatform @@ -229,16 +231,6 @@ export function createSettings() { inputType: 'boolean', }, }), - fixedSizeGrid: new Setting({ - defaultValue: true, - hideOnLevel: 'project', - description: - 'When enabled, the grid will use a fixed size based on your selected units rather than automatically scaling with zoom level.', - validate: (v) => typeof v === 'boolean', - commandConfig: { - inputType: 'boolean', - }, - }), /** * Stream resource saving behavior toggle */ @@ -544,6 +536,54 @@ export function createSettings() { }, hideOnLevel: 'project', }), + fixedSizeGrid: new Setting({ + defaultValue: true, + description: + 'When enabled, the grid will use a fixed size based on your selected units rather than automatically scaling with zoom level.', + validate: (v) => typeof v === 'boolean', + commandConfig: { + inputType: 'boolean', + }, + }), + majorGridSpacing: new Setting({ + defaultValue: 1, + description: + 'The space between major grid lines, specified in the current unit', + validate: (v) => typeof v === 'number', + isEnabled: (context) => context.modeling.fixedSizeGrid.current, + commandConfig: { + inputType: 'number', + }, + }), + minorGridsPerMajor: new Setting({ + defaultValue: 4, + description: 'Number of minor grid lines per major grid line', + validate: (v) => typeof v === 'number', + isEnabled: (context) => context.modeling.fixedSizeGrid.current, + commandConfig: { + inputType: 'number', + }, + }), + snapToGrid: new Setting({ + defaultValue: false, + description: + 'Snap the cursor to the unit grid when drawing lines, arcs, and other segment-based tools', + validate: (v) => typeof v === 'boolean', + commandConfig: { + inputType: 'boolean', + }, + }), + snapsPerMinor: new Setting({ + defaultValue: 1, + description: + 'Number of snaps between minor grid lines. 1 means snapping to every minor grid line', + validate: (v) => typeof v === 'number', + isEnabled: (context) => context.modeling.snapToGrid.current, + commandConfig: { + inputType: 'number', + }, + }), + /** * TODO: This setting is not yet implemented. * Whether to turn off animations and other motion effects diff --git a/src/lib/settings/settingsTypes.ts b/src/lib/settings/settingsTypes.ts index 3df0940958..17f708fab2 100644 --- a/src/lib/settings/settingsTypes.ts +++ b/src/lib/settings/settingsTypes.ts @@ -22,11 +22,6 @@ export interface SettingsViaQueryString { cameraOrbit: CameraOrbitType } -export enum UnitSystem { - Imperial = 'imperial', - Metric = 'metric', -} - export const baseUnits = { imperial: ['in', 'ft', 'yd'], metric: ['mm', 'cm', 'm'], @@ -98,6 +93,8 @@ export interface SettingProps { * ``` */ validate: (v: T) => boolean + + isEnabled?: (settings: any) => boolean // TODO any not too nice /** * A command argument configuration for the setting. * If this is provided, the setting will appear in the command bar. diff --git a/src/lib/settings/settingsUtils.ts b/src/lib/settings/settingsUtils.ts index a5a0161358..a2f035723f 100644 --- a/src/lib/settings/settingsUtils.ts +++ b/src/lib/settings/settingsUtils.ts @@ -66,7 +66,6 @@ export function configurationToSettingsPayload( configuration?.settings?.app?.allow_orbit_in_sketch_mode, projectDirectory: configuration?.settings?.project?.directory, showDebugPanel: configuration?.settings?.app?.show_debug_panel, - fixedSizeGrid: configuration?.settings?.app?.fixed_size_grid, }, modeling: { defaultUnit: configuration?.settings?.modeling?.base_unit, @@ -80,6 +79,12 @@ export function configurationToSettingsPayload( highlightEdges: configuration?.settings?.modeling?.highlight_edges, enableSSAO: configuration?.settings?.modeling?.enable_ssao, showScaleGrid: configuration?.settings?.modeling?.show_scale_grid, + fixedSizeGrid: configuration?.settings?.modeling?.fixed_size_grid, + snapToGrid: configuration?.settings?.modeling?.snap_to_grid, + majorGridSpacing: configuration?.settings?.modeling?.major_grid_spacing, + minorGridsPerMajor: + configuration?.settings?.modeling?.minor_grids_per_major, + snapsPerMinor: configuration?.settings?.modeling?.snaps_per_minor, }, textEditor: { textWrapping: configuration?.settings?.text_editor?.text_wrapping, @@ -112,7 +117,6 @@ export function settingsPayloadToConfiguration( stream_idle_mode: configuration?.app?.streamIdleMode, allow_orbit_in_sketch_mode: configuration?.app?.allowOrbitInSketchMode, show_debug_panel: configuration?.app?.showDebugPanel, - fixed_size_grid: configuration?.app?.fixedSizeGrid, }, modeling: { base_unit: configuration?.modeling?.defaultUnit, @@ -125,6 +129,11 @@ export function settingsPayloadToConfiguration( highlight_edges: configuration?.modeling?.highlightEdges, enable_ssao: configuration?.modeling?.enableSSAO, show_scale_grid: configuration?.modeling?.showScaleGrid, + fixed_size_grid: configuration?.modeling?.fixedSizeGrid, + snap_to_grid: configuration?.modeling?.snapToGrid, + major_grid_spacing: configuration?.modeling?.majorGridSpacing, + minor_grids_per_major: configuration?.modeling?.minorGridsPerMajor, + snaps_per_minor: configuration?.modeling?.snapsPerMinor, }, text_editor: { text_wrapping: configuration?.textEditor?.textWrapping, @@ -203,6 +212,12 @@ export function projectConfigurationToSettingsPayload( defaultUnit: configuration?.settings?.modeling?.base_unit, highlightEdges: configuration?.settings?.modeling?.highlight_edges, enableSSAO: configuration?.settings?.modeling?.enable_ssao, + fixedSizeGrid: configuration?.settings?.modeling?.fixed_size_grid, + snapToGrid: configuration?.settings?.modeling?.snap_to_grid, + majorGridSpacing: configuration?.settings?.modeling?.major_grid_spacing, + minorGridsPerMajor: + configuration?.settings?.modeling?.minor_grids_per_major, + snapsPerMinor: configuration?.settings?.modeling?.snaps_per_minor, }, textEditor: { textWrapping: configuration?.settings?.text_editor?.text_wrapping, @@ -240,6 +255,11 @@ export function settingsPayloadToProjectConfiguration( base_unit: configuration?.modeling?.defaultUnit, highlight_edges: configuration?.modeling?.highlightEdges, enable_ssao: configuration?.modeling?.enableSSAO, + fixed_size_grid: configuration?.modeling?.fixedSizeGrid, + snap_to_grid: configuration?.modeling?.snapToGrid, + major_grid_spacing: configuration?.modeling?.majorGridSpacing, + minor_grids_per_major: configuration?.modeling?.minorGridsPerMajor, + snaps_per_minor: configuration?.modeling?.snapsPerMinor, }, text_editor: { text_wrapping: configuration?.textEditor?.textWrapping, @@ -568,9 +588,11 @@ export function shouldShowSettingInput( return ( !shouldHideSetting(setting, settingsLevel) && (setting.Component || - ['string', 'boolean'].some((t) => typeof setting.default === t) || + ['string', 'boolean', 'number'].some( + (t) => typeof setting.default === t + ) || (setting.commandConfig?.inputType && - ['string', 'options', 'boolean'].some( + ['string', 'options', 'boolean', 'number'].some( (t) => setting.commandConfig?.inputType === t ))) ) @@ -584,8 +606,12 @@ export function shouldShowSettingInput( export function getSettingInputType(setting: Setting) { if (setting.Component) return 'component' if (setting.commandConfig) - return setting.commandConfig.inputType as 'string' | 'options' | 'boolean' - return typeof setting.default as 'string' | 'boolean' + return setting.commandConfig.inputType as + | 'string' + | 'options' + | 'boolean' + | 'number' + return typeof setting.default as 'string' | 'boolean' | 'number' } export const jsAppSettings = async (): Promise> => { diff --git a/src/lib/singletons.ts b/src/lib/singletons.ts index bbbc5e9d30..ce3d58241c 100644 --- a/src/lib/singletons.ts +++ b/src/lib/singletons.ts @@ -50,7 +50,6 @@ declare global { window.engineCommandManager = engineCommandManager export const sceneInfra = new SceneInfra(engineCommandManager) -engineCommandManager.camControlsCameraChange = sceneInfra.onCameraChange // This needs to be after sceneInfra and engineCommandManager are is created. export const editorManager = new EditorManager(engineCommandManager) @@ -211,6 +210,7 @@ export const getSettings = () => { // These are all late binding because of their circular dependency. // TODO: proper dependency injection. sceneInfra.camControls.getSettings = getSettings +sceneEntitiesManager.getSettings = getSettings export const useSettings = () => useSelector(settingsActor, (state) => { diff --git a/src/machines/modelingMachine.ts b/src/machines/modelingMachine.ts index 53ef5ec76b..e3a7083313 100644 --- a/src/machines/modelingMachine.ts +++ b/src/machines/modelingMachine.ts @@ -1088,7 +1088,7 @@ export const modelingMachine = setup({ isDraft: true, from: event.data, scale, - theme: sceneInfra._theme, + theme: sceneInfra.theme, }) draftPoint.position.copy(position) sceneInfra.scene.add(draftPoint) diff --git a/src/machines/settingsMachine.ts b/src/machines/settingsMachine.ts index 6e77a9a74e..78dbcbb1b9 100644 --- a/src/machines/settingsMachine.ts +++ b/src/machines/settingsMachine.ts @@ -208,6 +208,7 @@ export const settingsMachine = setup({ } }, setClientTheme: ({ context, self }) => { + console.log('setClientTheme***') const rootContext = self.system.get('root').getSnapshot().context const sceneInfra = rootContext.sceneInfra const sceneEntitiesManager = rootContext.sceneEntitiesManager