Skip to content

#1502 Add snap to grid to sketch mode #7893

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 56 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
d65eb43
add snapToGrid to settings
andrewvarga Jul 24, 2025
a7cc802
snap to default unit
andrewvarga Jul 24, 2025
7b6c5db
Merge branch 'main' into andrewvarga/1502/add-snap-to-grid-to-sketch-…
andrewvarga Jul 24, 2025
3f10524
Merge branch 'main' into andrewvarga/1502/add-snap-to-grid-to-sketch-…
andrewvarga Jul 25, 2025
181579e
resolve circular deps
andrewvarga Jul 25, 2025
8c99440
Merge branch 'main' into andrewvarga/1502/add-snap-to-grid-to-sketch-…
andrewvarga Jul 28, 2025
a0aa376
Merge branch 'main' into andrewvarga/1502/add-snap-to-grid-to-sketch-…
andrewvarga Jul 28, 2025
22ab0bb
Update snapshots
github-actions[bot] Jul 28, 2025
6c5f503
Update snapshots
github-actions[bot] Jul 28, 2025
a297ec2
merge from main
andrewvarga Jul 31, 2025
32bf879
Merge branch 'main' into andrewvarga/1502/add-snap-to-grid-to-sketch-…
andrewvarga Aug 4, 2025
d26b4e0
Merge branch 'main' into andrewvarga/1502/add-snap-to-grid-to-sketch-…
andrewvarga Aug 4, 2025
dcf202e
Merge branch 'main' into andrewvarga/1502/add-snap-to-grid-to-sketch-…
andrewvarga Aug 4, 2025
fb56e74
add new settings for grid, grid snapping
andrewvarga Aug 5, 2025
98c8e25
move fixed_size_grid to grid settings
andrewvarga Aug 5, 2025
5c8f7bf
fmt
andrewvarga Aug 5, 2025
c4c6cbc
Merge branch 'main' into andrewvarga/1502/add-snap-to-grid-to-sketch-…
andrewvarga Aug 6, 2025
bf25267
disable settings based on other settings via new isEnabled()
andrewvarga Aug 6, 2025
31f97b4
remove dead code
andrewvarga Aug 6, 2025
8759cd8
wip infinite grid
andrewvarga Aug 6, 2025
21ac79f
cleanups
andrewvarga Aug 7, 2025
e688196
Update snapshots
github-actions[bot] Aug 7, 2025
72accc5
grid updates
andrewvarga Aug 7, 2025
db8372c
merge remote
andrewvarga Aug 7, 2025
a1a4cc9
grid panning fix, cleanups
andrewvarga Aug 7, 2025
84cb1d7
Merge branch 'main' into andrewvarga/1502/add-snap-to-grid-to-sketch-…
andrewvarga Aug 7, 2025
2aea239
sceneInfra Grid is not actually used
andrewvarga Aug 7, 2025
4ada50a
add snaps per minor
andrewvarga Aug 7, 2025
a17cc8b
fmt
andrewvarga Aug 7, 2025
1f9043f
Merge branch 'main' into andrewvarga/1502/add-snap-to-grid-to-sketch-…
andrewvarga Aug 7, 2025
25ead2c
Update snapshots
github-actions[bot] Aug 7, 2025
cc5ba4a
Update snapshots
github-actions[bot] Aug 7, 2025
88d5765
Update snapshots
github-actions[bot] Aug 7, 2025
582cf91
Update snapshots
github-actions[bot] Aug 7, 2025
54f9461
Update snapshots
github-actions[bot] Aug 7, 2025
665c4c2
snapToGrid fixes
andrewvarga Aug 8, 2025
e12d6b2
Update snapshots
github-actions[bot] Aug 8, 2025
60c4dd3
Update snapshots
github-actions[bot] Aug 8, 2025
8763be4
Update snapshots
github-actions[bot] Aug 8, 2025
804c46e
add snapToGrid to context menu
andrewvarga Aug 8, 2025
797b4d7
add shortcut to disable snap
andrewvarga Aug 8, 2025
f8b05dc
enable hotkey
andrewvarga Aug 8, 2025
d7eea35
merge from remote
andrewvarga Aug 8, 2025
8f0e092
Merge branch 'main' into andrewvarga/1502/add-snap-to-grid-to-sketch-…
andrewvarga Aug 8, 2025
278fe20
grid color
andrewvarga Aug 8, 2025
6b1c07f
Update snapshots
github-actions[bot] Aug 8, 2025
6393d41
add threshold for grid rendering not to become too small
andrewvarga Aug 8, 2025
a4c376d
grid colors based on theme
andrewvarga Aug 8, 2025
1ea8baa
merge remote
andrewvarga Aug 8, 2025
7fa22f5
Merge branch 'main' into andrewvarga/1502/add-snap-to-grid-to-sketch-…
andrewvarga Aug 8, 2025
a39bad1
Update snapshots
github-actions[bot] Aug 8, 2025
a770af9
tmp non fixed grid
andrewvarga Aug 9, 2025
887421e
Add real scale unit to UnitsMenu in sketch mode, grid cleanups
andrewvarga Aug 10, 2025
c5f6a2e
merge from remote
andrewvarga Aug 10, 2025
a3be6f6
Merge branch 'main' into andrewvarga/1502/add-snap-to-grid-to-sketch-…
andrewvarga Aug 10, 2025
38464b9
Merge branch 'main' into andrewvarga/1502/add-snap-to-grid-to-sketch-…
andrewvarga Aug 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/kcl-lang/settings/user.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 4 additions & 1 deletion e2e/playwright/storageStates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@ export const TEST_SETTINGS: DeepPartial<Settings> = {
},
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',
Expand Down
4 changes: 0 additions & 4 deletions e2e/playwright/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion rust/kcl-lib/src/execution/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ impl From<crate::settings::types::Settings> 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,
}
}
}
Expand Down
25 changes: 19 additions & 6 deletions rust/kcl-lib/src/settings/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,6 @@
/// 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.
Expand All @@ -118,7 +114,6 @@
stream_idle_mode: Default::default(),
allow_orbit_in_sketch_mode: Default::default(),
show_debug_panel: Default::default(),
fixed_size_grid: make_it_so(),
}
}
}
Expand Down Expand Up @@ -270,7 +265,7 @@
}

/// 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 {
Expand Down Expand Up @@ -298,6 +293,24 @@
/// 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.

Check warning on line 301 in rust/kcl-lib/src/settings/types/mod.rs

View workflow job for this annotation

GitHub Actions / cargo fmt

Diff in /home/runner/work/modeling-app/modeling-app/rust/kcl-lib/src/settings/types/mod.rs
#[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)]
Expand Down
22 changes: 21 additions & 1 deletion rust/kcl-lib/src/settings/types/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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(),
Expand Down
229 changes: 229 additions & 0 deletions src/clientSideScene/InfiniteGridRenderer.ts
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading
Loading