Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **Add bone** sketch tool (<kbd>Shift</kbd>+<kbd>U</kbd>): click the two circle centers, then radius 1, radius 2, and waist width (like other sketch tools). Waist is the minimum neck thickness (offset from mid when end radii differ). Live length dim only for center-to-center; radii / holes / waist use the geometry preview (<kbd>Tab</kbd> still enters exact values). Options: **Add center nodes** (default on) and **Holes** (**None** / **One radius** / **Two radii**). Waist cutters are tangent to both end circles. Commits the trimmed outline (outer end arcs and inner waist arcs), optional hole circles, and optional permanent **Bone A** / **Bone B** center nodes, then exits to **Sketch inspection** (does not stay in Add bone for another). Remappable as **`mode.add_bone`**.
- **Add bone** sketch tool (<kbd>Shift</kbd>+<kbd>U</kbd>): click the two circle centers, then radius at the second center, radius at the first center, and waist width (like other sketch tools). Waist is the minimum neck thickness (offset from mid when end radii differ). Live length dim only for center-to-center; radii / holes / waist use the geometry preview (<kbd>Tab</kbd> still enters exact values). Options: **Add center nodes** (default on) and **Holes** (**None** / **One radius** / **Two radii**). Waist cutters are tangent to both end circles. Commits the trimmed outline (outer end arcs and inner waist arcs), optional hole circles, and optional permanent **Bone A** / **Bone B** center nodes, then exits to **Sketch inspection** (does not stay in Add bone for another). Remappable as **`mode.add_bone`**.

- **Shape local frame**: Shape List right-click on a solid toggles **Show axes** / **Show plane** / **Show up**, **Reset frame to bbox**, **Set from planar/cylindrical face**, and **Flip up** / **Flip axis (Z)**. Frame display flags persist in `.ezy` as `frameDisplay`. Face picks use `Mode::Shape_set_frame` (no toolbar hotkey).

Expand Down
2 changes: 1 addition & 1 deletion agents/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Root markers: [AGENTS.md](../AGENTS.md) / [agents.md](../agents.md).
| Need | File |
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ASCII / `src/` edits | [conventions/ascii-source.md](conventions/ascii-source.md) — after edits: `python scripts/agent_check.py <paths>` |
| C++ style (full) | [docs/ezycad_code_style.md](../docs/ezycad_code_style.md) — optional local: `python scripts/code_style_check.py` (not CI) |
| C++ style (full) | [docs/ezycad_code_style.md](../docs/ezycad_code_style.md) — Vertical rhythm includes a blank line after an early-return `if`; optional local: `python scripts/code_style_check.py` (not CI) |
| User docs when UI changes | [conventions/user-docs-sync.md](conventions/user-docs-sync.md) |
| Sketch module (dev doc) | [src/doc/sketch.md](../src/doc/sketch.md) — read when editing sketch code; update if API/architecture changes |
| Shape module (dev doc) | [src/doc/shape.md](../src/doc/shape.md) — read when editing `shp_*` code; update if API/operations change |
Expand Down
1 change: 1 addition & 0 deletions docs/ezycad_code_style.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ default: ++result.other_curve_count; break;
Not enforced by clang-format. Treat each **logical beat** as its own short paragraph:

- Put a **blank line between consecutive `if` statements** that have no `else` (each is its own block).
- After an `if` with no `else` whose then-body **exits** (`return` / `continue` / `break` / `CHK_RET`), put a blank line before the next statement in the same block (the success path).
- After a **local lambda or helper** closes (`};`), put a blank line before the first use or following statement.
- Inside a short helper, separate **setup**, **early return**, and **main body** with blank lines when each is a distinct step.
- After a multi-statement `if { ... }` (or single-statement `if` that mutates state), put a blank line before trailing same-row UI such as `ImGui::SameLine`, unit labels, or `GUI_DOC_HELP_`.
Expand Down
4 changes: 2 additions & 2 deletions docs/usage-sketch.md
Original file line number Diff line number Diff line change
Expand Up @@ -720,8 +720,8 @@ The bone tool builds a connecting-rod outline: the outer arcs of the two end cir
1. Select **Add bone** on the toolbar (default <kbd>Shift</kbd>+<kbd>U</kbd>).
2. Click the first circle center (normal sketch snap).
3. Click the second circle center. <kbd>Tab</kbd> sets the center-to-center distance; <kbd>Shift</kbd>+<kbd>Tab</kbd> sets the bone-axis angle.
4. Click to set **radius 1** (distance from the first center). <kbd>Tab</kbd> enters an exact radius.
5. Click to set **radius 2** (distance from the second center). <kbd>Tab</kbd> enters an exact radius.
4. Click to set **radius 2** (distance from the second center, where the cursor just finished). <kbd>Tab</kbd> enters an exact radius.
5. Click to set **radius 1** (distance from the first center). <kbd>Tab</kbd> enters an exact radius.
6. Click a point to set **waist width** (minimum remaining thickness between the waist arcs, perpendicular to the center line). When the end radii differ, the neck is offset toward the smaller end, not at the center midpoint. <kbd>Tab</kbd> enters an exact waist. If **Holes** is **None**, the tool commits after this click.
7. If **Holes** is **One radius**, click once from the first center to set both hole radii. If **Two radii**, click hole radius at end A, then at end B. Each hole must be smaller than that end's outer radius.

Expand Down
36 changes: 35 additions & 1 deletion scripts/code_style_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
"""Optional local check of C++ against docs/ezycad_code_style.md.

Not part of CI or the default agent_check.py run. Start with Vertical rhythm
(blank lines). Do not add groups here to CI unless a rule is as objective as ASCII.
(blank lines), including a blank line after an early-exit `if` (return / continue /
break / CHK_RET) before the next statement in the same block. Do not add groups
here to CI unless a rule is as objective as ASCII.

Usage:
python scripts/code_style_check.py [paths...]
Expand Down Expand Up @@ -46,6 +48,7 @@
IMGUI_TEXT_BEATS = ("ImGui::TextWrapped", "ImGui::TextDisabled")
IMGUI_SPACING = "ImGui::Spacing"
IMGUI_BUTTON = "ImGui::Button"
EARLY_EXIT_IDENTS = frozenset({"return", "continue", "break", "CHK_RET"})


@dataclass(frozen=True)
Expand Down Expand Up @@ -705,6 +708,26 @@ def starts_with_any(code: str, prefixes: tuple[str, ...]) -> bool:
return any(code.startswith(p) for p in prefixes)


def if_then_exits(src: Source, stmt: Stmt) -> bool:
"""True when an `if` (no else required here) then-body ends with return/continue/break/CHK_RET."""
if stmt.kind != "if" or not stmt.bodies:
return False
b0, b1 = stmt.bodies[0]
i = skip_ws_and_pp(src.masked, b0, b1)
if i >= b1:
return False
if src.masked[i] == "{":
close = skip_balanced(src.masked, i, b1, "{", "}")
inner = parse_statements(src, i + 1, close - 1)
else:
inner = parse_statements(src, b0, b1)
if not inner:
return peek_ident(src.masked, i, b1) in EARLY_EXIT_IDENTS
last = inner[-1]
lead = skip_ws_and_pp(src.masked, last.start, last.end)
return peek_ident(src.masked, lead, last.end) in EARLY_EXIT_IDENTS


def check_vertical_rhythm(src: Source) -> list[Finding]:
findings: list[Finding] = []
_check_span(src, 0, len(src.masked), findings)
Expand Down Expand Up @@ -740,6 +763,17 @@ def _check_span(src: Source, start: int, end: int, findings: list[Finding]) -> N
)
continue

if a.kind == "if" and not a.has_else and if_then_exits(src, a):
findings.append(
Finding(
src.path,
line,
"vertical-rhythm",
"blank line required after early-return if before the next statement",
)
)
continue

if a.kind == "if" and starts_with_any(b_lead, SAME_ROW_UI_PREFIXES):
findings.append(
Finding(
Expand Down
7 changes: 7 additions & 0 deletions src/config.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#pragma once

// Compile-time developer extras (not persisted, not a user setting).
// Set to 0 for release builds. Override with -DDEV_MODE=0 if needed.
#ifndef DEV_MODE
#define DEV_MODE 1
#endif
5 changes: 4 additions & 1 deletion src/doc/gui.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ GUI (gui.h / gui.cpp)
+-- scr_lua_console / scr_python_console (scripting UI)
```

Compile-time `DEV_MODE` lives in [`config.h`](../config.h) (default on). When it is 1, Add-bone Options shows **Debug vis** checkboxes for construction overlays (cut circles, tangents, contacts, ...). The flags are session-only.

CMake IDE group: `src\gui` (files matching `gui*` or `occt*` prefix).

## ImGui docking and viewports
Expand Down Expand Up @@ -222,7 +224,8 @@ Tests use `sketch_left_click` to simulate sketch LMB without ImGui mouse positio
| `Sketch_inspection_mode` | `options_sketch_common_` |
| Each sketch tool mode | Matching `options_sketch_*_mode_` |
| `Sketch_operation_axis` | Mirror / Revolve / Clear axis |
| `Sketch_face_extrude` | Both sides, Twist, material; help mentions Settings fast preview |
| `Sketch_face_extrude` | **Extrude** (Both sides, Twist, material) above Sketch options; help mentions Settings fast preview |
| `Sketch_add_bone` | Center nodes, holes; when `DEV_MODE` in `config.h`, **Debug vis** construction overlays (not persisted) |

### Options panel layout (sketch tools)

Expand Down
4 changes: 4 additions & 0 deletions src/doc/sketch.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ Sketch (coordinator: skt.cpp, skt.h)

Supporting (not owned sub-objects):
skt_edge.* Sketch_edge type, linear/arc predicates
skt_bone.* bone outline / waist cutters (`compute_bone_geom`); DEV_MODE debug overlay
skt_ais.* Sketch_AIS_edge, Sketch_AIS_node_mark, Sketch_face_shp
skt_display.cpp visibility, edge styling, list hover, set_current
skt_operations.cpp operation axis, mirror, revolve
Expand Down Expand Up @@ -204,6 +205,7 @@ Prefer these visitors in JSON/delta/topo code over iterating `std::list<Sketch_e
| `skt_node_marks.h` | AIS "+" markers for permanent nodes only |
| `skt_dims.h` | Length dimensions between node pairs; Tab/Shift+Tab input; dimension-tool pick state |
| `skt_tools.h` | Mode-specific click/move/finalize/cancel; shared helpers in `skt_tools.inl` |
| `skt_bone.h` | Bone outline geom (`compute_bone_geom`, `make_bone_wire`); `DEV_MODE` construction overlay (`make_bone_debug_shape`) |
| `skt_tools_bone.cpp` | Add-bone tool (`Sketch_tools` members; Options: center nodes / holes; successful commit exits to `Sketch_inspection_mode`) |
| `skt_underlay.h` | Calibrated raster underlay on the sketch plane |
| `skt_ais.h` | OCCT AIS wrappers tied back to owning `Sketch` |
Expand All @@ -212,6 +214,8 @@ Prefer these visitors in JSON/delta/topo code over iterating `std::list<Sketch_e
| `skt_json.h` | `.ezy` / project JSON for sketches |
| `skt_op_recorder.h` | Undo/redo recorder; `Sketch_op_delta` lives in `.cpp` |

A waist cutter is externally tangent to both end circles, so cutter center, contact, and end center are collinear. `DEV_MODE` Debug vis **Cutter radials** draws the four cutter-radius segments to those contacts; the outline switches from end-cap arc to waist arc there.

## Edge and face model

| Entity | Storage | Notes |
Expand Down
2 changes: 1 addition & 1 deletion src/doc/utility.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ Large module; grouped by concern:
| Area | Examples |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Plane / 2D | `to_2d`, `to_3d`, `xy_plane`, `sketch_reference_plane`, `Plane_side` |
| Profile wires | `make_square_wire`, `make_circle_wire`, `make_slot_wire`, `make_bone_wire`, `create_wire_box` |
| Profile wires | `make_square_wire`, `make_circle_wire`, `make_slot_wire`, `create_wire_box` |
| Sketch dimensions | `Length_dimension_style`, `create_distance_annotation`, `create_angle_annotation`, `apply_length_dimension_style`, `apply_angle_dimension_style` |
| Analysis | `to_boost` (polygon), `to_boost_ls` (edge `linestring_2d`), `get_shape_bbox_center`, `plane_from_face`, `side_of_plane` |
| Tests / debug | `to_wkt_string` (linestring / ring / polygon), `ezy_geom::area`, `is_valid`; Geometry Watch in `scripts/ezycad_graphical_debugging.xml` (`ring_2d` inherits vector as Ring; `linestring_2d` uses named `points` as Linestring). Re-select the XML path in Options after editing it. |
Expand Down
5 changes: 5 additions & 0 deletions src/gui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3452,6 +3452,7 @@ void GUI::dbg_()
return;
}
// Undo / redo stack

ImGui::Text("Undo: %zu (Ctrl+Z) | Redo: %zu (Ctrl+Y) [max 50]", m_view->undo_stack_size(), m_view->redo_stack_size());
ImGui::Separator();
// Get the available content region width
Expand Down Expand Up @@ -4351,12 +4352,14 @@ void GUI::export_file_dialog_(Export_format fmt, Export_unit unit)
show_message(s.message());
return;
}

std::ifstream in(mem_path, std::ios::binary);
if (!in)
{
show_message("Export read failed.");
return;
}

const std::string bytes((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
download_blob_async(download_name, bytes);
show_message("Exported: " + download_name);
Expand Down Expand Up @@ -4523,6 +4526,7 @@ void GUI::save_file_dialog_()
"changed.");
return;
}

std::string default_file =
m_last_saved_path.empty() ? "project.ezy" : std::filesystem::path(m_last_saved_path).filename().string();
save_file_dialog_async("Save EzyCad project", default_file, ezy_bytes);
Expand Down Expand Up @@ -4626,6 +4630,7 @@ std::string format_log_line_(const std::string& base, size_t repeat_count)
{
if (repeat_count <= 1)
return base;

return base + " #" + std::to_string(repeat_count);
}

Expand Down
11 changes: 11 additions & 0 deletions src/gui.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
#include <variant>
#include <vector>

#include "config.h"
#if DEV_MODE
#include "skt_bone.h"
#endif
#include "utl_geom.h"
#include "imgui.h"
#include "imgui_markdown.h"
Expand Down Expand Up @@ -309,6 +313,10 @@ class GUI
bool get_bone_add_center_nodes() const { return m_bone_add_center_nodes; }
/// Add-bone Options: hole clicks after waist (`gui.bone_holes`).
Bone_holes get_bone_holes() const { return m_bone_holes; }
#if DEV_MODE
/// Add-bone construction overlays (DEV_MODE Options checkboxes; not persisted).
const Bone_debug_flags& get_bone_debug_flags() const { return m_bone_debug; }
#endif
bool get_edge_from_center() const { return m_edge_from_center; }
bool get_hide_all_shapes() const { return m_hide_all_shapes; }
void set_hide_all_shapes(bool hide) { m_hide_all_shapes = hide; }
Expand Down Expand Up @@ -662,6 +670,9 @@ class GUI
bool m_add_mid_pt_slot_edges = false;
bool m_bone_add_center_nodes = true;
Bone_holes m_bone_holes = Bone_holes::None;
#if DEV_MODE
Bone_debug_flags m_bone_debug;
#endif
bool m_edge_from_center = false;
/// Degrees per numpad orbit (8/2/4/6) and Blender-style roll (Shift+NumPad 4/6); persisted in `gui.view_roll_step_deg`.
double m_view_roll_step_deg = k_gui_view_roll_step_deg_default;
Expand Down
Loading
Loading